@foxeltech/angular-ui 0.4.0 → 0.7.75

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 (189) hide show
  1. package/archivo/archivo-400-latin-ext.woff2 +0 -0
  2. package/archivo/archivo-400-latin.woff2 +0 -0
  3. package/archivo/archivo-400-vietnamese.woff2 +0 -0
  4. package/archivo/archivo-500-latin-ext.woff2 +0 -0
  5. package/archivo/archivo-500-latin.woff2 +0 -0
  6. package/archivo/archivo-500-vietnamese.woff2 +0 -0
  7. package/archivo/archivo-600-latin-ext.woff2 +0 -0
  8. package/archivo/archivo-600-latin.woff2 +0 -0
  9. package/archivo/archivo-600-vietnamese.woff2 +0 -0
  10. package/archivo/archivo-700-latin-ext.woff2 +0 -0
  11. package/archivo/archivo-700-latin.woff2 +0 -0
  12. package/archivo/archivo-700-vietnamese.woff2 +0 -0
  13. package/components.css +332 -30
  14. package/dialogs.css +2 -2
  15. package/familjen-grotesk/familjen-grotesk-400-latin-ext.woff2 +0 -0
  16. package/familjen-grotesk/familjen-grotesk-400-latin.woff2 +0 -0
  17. package/familjen-grotesk/familjen-grotesk-400-vietnamese.woff2 +0 -0
  18. package/familjen-grotesk/familjen-grotesk-500-latin-ext.woff2 +0 -0
  19. package/familjen-grotesk/familjen-grotesk-500-latin.woff2 +0 -0
  20. package/familjen-grotesk/familjen-grotesk-500-vietnamese.woff2 +0 -0
  21. package/familjen-grotesk/familjen-grotesk-600-latin-ext.woff2 +0 -0
  22. package/familjen-grotesk/familjen-grotesk-600-latin.woff2 +0 -0
  23. package/familjen-grotesk/familjen-grotesk-600-vietnamese.woff2 +0 -0
  24. package/familjen-grotesk/familjen-grotesk-700-latin-ext.woff2 +0 -0
  25. package/familjen-grotesk/familjen-grotesk-700-latin.woff2 +0 -0
  26. package/familjen-grotesk/familjen-grotesk-700-vietnamese.woff2 +0 -0
  27. package/fesm2022/foxeltech-angular-ui-chart.mjs +381 -40
  28. package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
  29. package/fesm2022/foxeltech-angular-ui-flow.mjs +176 -3
  30. package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
  31. package/fesm2022/foxeltech-angular-ui-rich-text.mjs +2 -2
  32. package/fesm2022/foxeltech-angular-ui-rich-text.mjs.map +1 -1
  33. package/fesm2022/foxeltech-angular-ui.mjs +1327 -151
  34. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  35. package/fonts.css +558 -133
  36. package/ibm-plex-mono/ibm-plex-mono-400-cyrillic-ext.woff2 +0 -0
  37. package/ibm-plex-mono/ibm-plex-mono-400-cyrillic.woff2 +0 -0
  38. package/ibm-plex-mono/ibm-plex-mono-400-latin-ext.woff2 +0 -0
  39. package/ibm-plex-mono/ibm-plex-mono-400-latin.woff2 +0 -0
  40. package/ibm-plex-mono/ibm-plex-mono-400-vietnamese.woff2 +0 -0
  41. package/ibm-plex-mono/ibm-plex-mono-500-cyrillic-ext.woff2 +0 -0
  42. package/ibm-plex-mono/ibm-plex-mono-500-cyrillic.woff2 +0 -0
  43. package/ibm-plex-mono/ibm-plex-mono-500-latin-ext.woff2 +0 -0
  44. package/ibm-plex-mono/ibm-plex-mono-500-latin.woff2 +0 -0
  45. package/ibm-plex-mono/ibm-plex-mono-500-vietnamese.woff2 +0 -0
  46. package/ibm-plex-mono/ibm-plex-mono-600-cyrillic-ext.woff2 +0 -0
  47. package/ibm-plex-mono/ibm-plex-mono-600-cyrillic.woff2 +0 -0
  48. package/ibm-plex-mono/ibm-plex-mono-600-latin-ext.woff2 +0 -0
  49. package/ibm-plex-mono/ibm-plex-mono-600-latin.woff2 +0 -0
  50. package/ibm-plex-mono/ibm-plex-mono-600-vietnamese.woff2 +0 -0
  51. package/jetbrains-mono/jetbrains-mono-400-cyrillic-ext.woff2 +0 -0
  52. package/jetbrains-mono/jetbrains-mono-400-cyrillic.woff2 +0 -0
  53. package/jetbrains-mono/jetbrains-mono-400-greek.woff2 +0 -0
  54. package/jetbrains-mono/jetbrains-mono-400-latin-ext.woff2 +0 -0
  55. package/jetbrains-mono/jetbrains-mono-400-latin.woff2 +0 -0
  56. package/jetbrains-mono/jetbrains-mono-400-vietnamese.woff2 +0 -0
  57. package/jetbrains-mono/jetbrains-mono-500-cyrillic-ext.woff2 +0 -0
  58. package/jetbrains-mono/jetbrains-mono-500-cyrillic.woff2 +0 -0
  59. package/jetbrains-mono/jetbrains-mono-500-greek.woff2 +0 -0
  60. package/jetbrains-mono/jetbrains-mono-500-latin-ext.woff2 +0 -0
  61. package/jetbrains-mono/jetbrains-mono-500-latin.woff2 +0 -0
  62. package/jetbrains-mono/jetbrains-mono-500-vietnamese.woff2 +0 -0
  63. package/jetbrains-mono/jetbrains-mono-600-cyrillic-ext.woff2 +0 -0
  64. package/jetbrains-mono/jetbrains-mono-600-cyrillic.woff2 +0 -0
  65. package/jetbrains-mono/jetbrains-mono-600-greek.woff2 +0 -0
  66. package/jetbrains-mono/jetbrains-mono-600-latin-ext.woff2 +0 -0
  67. package/jetbrains-mono/jetbrains-mono-600-latin.woff2 +0 -0
  68. package/jetbrains-mono/jetbrains-mono-600-vietnamese.woff2 +0 -0
  69. package/jetbrains-mono/jetbrains-mono-700-cyrillic-ext.woff2 +0 -0
  70. package/jetbrains-mono/jetbrains-mono-700-cyrillic.woff2 +0 -0
  71. package/jetbrains-mono/jetbrains-mono-700-greek.woff2 +0 -0
  72. package/jetbrains-mono/jetbrains-mono-700-latin-ext.woff2 +0 -0
  73. package/jetbrains-mono/jetbrains-mono-700-latin.woff2 +0 -0
  74. package/jetbrains-mono/jetbrains-mono-700-vietnamese.woff2 +0 -0
  75. package/newsreader/newsreader-400-latin-ext.woff2 +0 -0
  76. package/newsreader/newsreader-400-latin.woff2 +0 -0
  77. package/newsreader/newsreader-400-vietnamese.woff2 +0 -0
  78. package/newsreader/newsreader-500-latin-ext.woff2 +0 -0
  79. package/newsreader/newsreader-500-latin.woff2 +0 -0
  80. package/newsreader/newsreader-500-vietnamese.woff2 +0 -0
  81. package/newsreader/newsreader-600-latin-ext.woff2 +0 -0
  82. package/newsreader/newsreader-600-latin.woff2 +0 -0
  83. package/newsreader/newsreader-600-vietnamese.woff2 +0 -0
  84. package/package.json +5 -2
  85. package/source-serif-4/source-serif-4-400-cyrillic-ext.woff2 +0 -0
  86. package/source-serif-4/source-serif-4-400-cyrillic.woff2 +0 -0
  87. package/source-serif-4/source-serif-4-400-greek.woff2 +0 -0
  88. package/source-serif-4/source-serif-4-400-latin-ext.woff2 +0 -0
  89. package/source-serif-4/source-serif-4-400-latin.woff2 +0 -0
  90. package/source-serif-4/source-serif-4-400-vietnamese.woff2 +0 -0
  91. package/source-serif-4/source-serif-4-600-cyrillic-ext.woff2 +0 -0
  92. package/source-serif-4/source-serif-4-600-cyrillic.woff2 +0 -0
  93. package/source-serif-4/source-serif-4-600-greek.woff2 +0 -0
  94. package/source-serif-4/source-serif-4-600-latin-ext.woff2 +0 -0
  95. package/source-serif-4/source-serif-4-600-latin.woff2 +0 -0
  96. package/source-serif-4/source-serif-4-600-vietnamese.woff2 +0 -0
  97. package/src/lib/styles/components.css +332 -30
  98. package/src/lib/styles/dialogs.css +2 -2
  99. package/src/lib/styles/fonts.css +558 -133
  100. package/src/lib/styles/tailwind.css +13 -1
  101. package/src/lib/styles/theme.css +642 -69
  102. package/src/lib/styles/tokens.css +81 -19
  103. package/src/lib/styles/utilities.css +16 -1
  104. package/src/lib/ui/components/bar-list/bar-list.component.html +29 -0
  105. package/src/lib/ui/components/button/button.component.html +9 -5
  106. package/src/lib/ui/components/checkbox/checkbox.component.html +1 -1
  107. package/src/lib/ui/components/code-block/code-block.component.html +8 -0
  108. package/src/lib/ui/components/drawer/drawer.component.html +35 -12
  109. package/src/lib/ui/components/drawer/drawer.component.scss +106 -18
  110. package/src/lib/ui/components/facets-rail/facets-rail.component.html +37 -0
  111. package/src/lib/ui/components/filter-pills/filter-pills.component.html +16 -0
  112. package/src/lib/ui/components/kanban-board/kanban-board.component.scss +9 -9
  113. package/src/lib/ui/components/list-row/list-row.component.html +20 -0
  114. package/src/lib/ui/components/master-detail/master-detail.component.css +79 -5
  115. package/src/lib/ui/components/master-detail/master-detail.component.html +57 -7
  116. package/src/lib/ui/components/pending-badge/pending-badge.component.html +15 -0
  117. package/src/lib/ui/components/pipeline-stepper/pipeline-stepper.component.html +45 -0
  118. package/src/lib/ui/components/project-card/project-card.component.html +102 -0
  119. package/src/lib/ui/components/radio-button/radio-button.component.html +1 -1
  120. package/src/lib/ui/components/radio-button-toggle/radio-button-toggle.component.html +1 -1
  121. package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +38 -0
  122. package/src/lib/ui/components/route-skeleton/route-skeleton.component.html +184 -0
  123. package/src/lib/ui/components/sample-frame/sample-frame.component.html +18 -0
  124. package/src/lib/ui/components/score-badge/score-badge.component.html +7 -0
  125. package/src/lib/ui/components/screenshot-frame/screenshot-frame.component.html +40 -0
  126. package/src/lib/ui/components/section-card/section-card.component.html +15 -6
  127. package/src/lib/ui/components/segmented/segmented.component.html +42 -0
  128. package/src/lib/ui/components/segmented/segmented.component.scss +12 -0
  129. package/src/lib/ui/components/skeleton-dashboard/skeleton-dashboard.component.html +24 -0
  130. package/src/lib/ui/components/skeleton-detail/skeleton-detail.component.html +8 -8
  131. package/src/lib/ui/components/skeleton-facets/skeleton-facets.component.html +15 -0
  132. package/src/lib/ui/components/skeleton-frame/skeleton-frame.component.html +59 -0
  133. package/src/lib/ui/components/skeleton-list/skeleton-list.component.html +6 -6
  134. package/src/lib/ui/components/slider/slider.component.html +2 -2
  135. package/src/lib/ui/components/sparkbar/sparkbar.component.html +14 -0
  136. package/src/lib/ui/components/stage-dots/stage-dots.component.html +13 -0
  137. package/src/lib/ui/components/stat-cards/stat-cards.component.html +44 -11
  138. package/src/lib/ui/components/status-dot/status-dot.component.html +13 -0
  139. package/src/lib/ui/components/switch/switch.component.html +1 -1
  140. package/src/lib/ui/components/tab-group/tab-group.component.html +37 -8
  141. package/src/lib/ui/components/tab-group/tab-group.component.scss +41 -1
  142. package/src/lib/ui/components/tag/tag.component.html +2 -2
  143. package/src/lib/ui/components/theme-picker/theme-picker.component.html +44 -0
  144. package/src/lib/ui/components/toast/toast.component.html +6 -6
  145. package/src/lib/ui/components/tree-diagram/tree-diagram.component.html +3 -3
  146. package/tailwind.css +13 -1
  147. package/theme.css +642 -69
  148. package/tokens.css +81 -19
  149. package/types/foxeltech-angular-ui-chart.d.ts +90 -3
  150. package/types/foxeltech-angular-ui-flow.d.ts +83 -1
  151. package/types/foxeltech-angular-ui.d.ts +909 -48
  152. package/utilities.css +16 -1
  153. package/BeVietnamPro/BeVietnamPro-Bold.woff2 +0 -0
  154. package/BeVietnamPro/BeVietnamPro-ExtraLight.woff2 +0 -0
  155. package/BeVietnamPro/BeVietnamPro-Light.woff2 +0 -0
  156. package/BeVietnamPro/BeVietnamPro-Medium.woff2 +0 -0
  157. package/BeVietnamPro/BeVietnamPro-Regular.woff2 +0 -0
  158. package/BeVietnamPro/BeVietnamPro-SemiBold.woff2 +0 -0
  159. package/BeVietnamPro/BeVietnamPro-Thin.woff2 +0 -0
  160. package/SpaceMono-Regular.woff2 +0 -0
  161. package/dm-sans/DMSans-Bold.woff2 +0 -0
  162. package/dm-sans/DMSans-Light.woff2 +0 -0
  163. package/dm-sans/DMSans-Medium.woff2 +0 -0
  164. package/dm-sans/DMSans-Regular.woff2 +0 -0
  165. package/dm-sans/DMSans-SemiBold.woff2 +0 -0
  166. package/dm-sans/DMSans-Thin.woff2 +0 -0
  167. package/orbitron/Orbitron-Bold.woff2 +0 -0
  168. package/orbitron/Orbitron-Medium.woff2 +0 -0
  169. package/orbitron/Orbitron-Regular.woff2 +0 -0
  170. package/orbitron/Orbitron-SemiBold.woff2 +0 -0
  171. package/roboto/Roboto-Black.woff2 +0 -0
  172. package/roboto/Roboto-BlackItalic.woff2 +0 -0
  173. package/roboto/Roboto-Bold.woff2 +0 -0
  174. package/roboto/Roboto-BoldCondensed.woff2 +0 -0
  175. package/roboto/Roboto-BoldCondensedItalic.woff2 +0 -0
  176. package/roboto/Roboto-BoldItalic.woff2 +0 -0
  177. package/roboto/Roboto-Condensed.woff2 +0 -0
  178. package/roboto/Roboto-CondensedItalic.woff2 +0 -0
  179. package/roboto/Roboto-Italic.woff2 +0 -0
  180. package/roboto/Roboto-Light.woff2 +0 -0
  181. package/roboto/Roboto-LightItalic.woff2 +0 -0
  182. package/roboto/Roboto-Medium.woff2 +0 -0
  183. package/roboto/Roboto-MediumItalic.woff2 +0 -0
  184. package/roboto/Roboto-Regular.woff2 +0 -0
  185. package/roboto/Roboto-Thin.woff2 +0 -0
  186. package/roboto/Roboto-ThinItalic.woff2 +0 -0
  187. package/src/lib/ui/components/skeleton-table-loading/skeleton-table-loading.component.html +0 -21
  188. package/src/lib/ui/components/skeleton-table-loading/skeleton-table-loading.component.scss +0 -0
  189. package/src/lib/ui/components/tab-component/tab.component.html +0 -1
@@ -1,21 +1,21 @@
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, contentChild } from '@angular/core';
2
+ import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, signal, computed, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, model } from '@angular/core';
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';
6
6
  import { Observable, firstValueFrom, Subject, debounceTime, distinctUntilChanged, BehaviorSubject, throwError, filter, takeUntil } from 'rxjs';
7
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
8
8
  import * as i2 from '@angular/common/http';
9
9
  import { HttpRequest, HttpClient } from '@angular/common/http';
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 }] }] } });
@@ -412,8 +412,16 @@ class FxUtils {
412
412
  }
413
413
  return value ? `rgba(${value} / ${alpha})` : `rgba(255 255 255 / ${alpha})`;
414
414
  }
415
+ /**
416
+ * Status/severity string → tag tone.
417
+ *
418
+ * Coerces instead of trusting the argument: every caller feeds this raw API
419
+ * values, and a numeric or null field used to throw
420
+ * "key.toLowerCase is not a function" and take the whole screen down.
421
+ * Non-string input simply falls through to the default tone.
422
+ */
415
423
  static getTagClass(key, isCriticalExist = true) {
416
- switch (key.toLowerCase()) {
424
+ switch (String(key ?? '').toLowerCase()) {
417
425
  case 'error':
418
426
  case 'failed':
419
427
  case 'inactive':
@@ -796,6 +804,9 @@ class BaseTableComponent extends BaseComponent {
796
804
  autoFocus: 'first-tabbable',
797
805
  width: size,
798
806
  disableClose: conf.disableClose ?? false,
807
+ // Dialogs are created by the root MatDialog: without this the dialog
808
+ // component cannot see route-scoped providers (APIs) — NG0201.
809
+ injector: this.injector,
799
810
  data: {
800
811
  method: 'create',
801
812
  data: this.empty(),
@@ -831,6 +842,7 @@ class BaseTableComponent extends BaseComponent {
831
842
  autoFocus: 'first-tabbable',
832
843
  width: size,
833
844
  disableClose: conf.disableClose ?? false,
845
+ injector: this.injector,
834
846
  data: {
835
847
  method: 'update',
836
848
  data: model,
@@ -866,6 +878,7 @@ class BaseTableComponent extends BaseComponent {
866
878
  autoFocus: 'first-tabbable',
867
879
  width: size,
868
880
  disableClose: conf.disableClose ?? false,
881
+ injector: this.injector,
869
882
  data: {
870
883
  data: model,
871
884
  title: conf.title,
@@ -894,6 +907,7 @@ class BaseTableComponent extends BaseComponent {
894
907
  panelClass: 'fx-dialog',
895
908
  autoFocus: 'first-tabbable',
896
909
  width: '400px',
910
+ injector: this.injector,
897
911
  data: {
898
912
  title: conf?.title,
899
913
  message: conf?.message,
@@ -1169,15 +1183,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1169
1183
  args: [MAT_DIALOG_DATA]
1170
1184
  }] }, { type: FxToastrService }, { type: HttpWrapper }, { type: i1.MatDialog }], propDecorators: { ngForm: [{ type: i0.ViewChild, args: ['f', { isSignal: true }] }] } });
1171
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
+ */
1172
1200
  class FxLoadingService {
1173
- listener = new EventEmitter();
1174
1201
  listenerAction = new EventEmitter();
1175
- start() {
1176
- this.listener.emit(true);
1177
- }
1178
- stop() {
1179
- this.listener.emit(false);
1180
- }
1181
1202
  startAction() {
1182
1203
  this.listenerAction.emit(true);
1183
1204
  }
@@ -1194,16 +1215,97 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1194
1215
  }]
1195
1216
  }] });
1196
1217
 
1197
- class BaseResolver {
1198
- loadingService;
1199
- injector;
1200
- constructor(loadingService, injector) {
1201
- this.loadingService = loadingService;
1202
- 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());
1203
1251
  }
1204
- baseResolverInit() {
1205
- 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
+ });
1206
1278
  }
1279
+ clearTimer() {
1280
+ if (this.timer !== undefined) {
1281
+ clearTimeout(this.timer);
1282
+ this.timer = undefined;
1283
+ }
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;
1207
1309
  }
1208
1310
 
1209
1311
  class FxStorageService {
@@ -1228,11 +1330,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1228
1330
  }] });
1229
1331
 
1230
1332
  /**
1231
- * Functional replacement for the copy-pasted list-resolver class:
1232
- * starts the loading overlay, runs one API request, and on failure stops the
1233
- * overlay and shows a parsed error toast the exact convention every
1234
- * BaseResolver subclass hand-rolls today. BaseResolver stays for existing
1235
- * 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.
1236
1339
  *
1237
1340
  * Usage:
1238
1341
  * resolve: { histories: fxListResolver(ScanManagementApi, (route, state, ctx) =>
@@ -1241,11 +1344,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1241
1344
  function fxListResolver(apiType, request) {
1242
1345
  return async (route, state) => {
1243
1346
  const api = inject(apiType);
1244
- const loading = inject(FxLoadingService);
1245
1347
  const toastr = inject(FxToastrService);
1246
1348
  const storage = inject(FxStorageService);
1247
1349
  try {
1248
- loading.start();
1249
1350
  const ctx = { pageSize: storage.resolve(state.url) || 10 };
1250
1351
  const req = request(route, state, ctx);
1251
1352
  if ((req.method ?? 'get') === 'post') {
@@ -1254,7 +1355,6 @@ function fxListResolver(apiType, request) {
1254
1355
  return await api.get(req.url, req.params);
1255
1356
  }
1256
1357
  catch (err) {
1257
- loading.stop();
1258
1358
  toastr.error(FxUtils.parseError(err), 'Error');
1259
1359
  return null;
1260
1360
  }
@@ -1427,26 +1527,54 @@ class RealtimeTableSync {
1427
1527
 
1428
1528
  const NEUTRAL_TEXT = 'text-text-secondary';
1429
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
+ */
1430
1535
  const TONE_TEXT = {
1431
- critical: 'text-critical',
1432
- danger: 'text-danger',
1433
- warning: 'text-warning',
1434
- success: 'text-success',
1435
- 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',
1436
1542
  };
1543
+ /** Nền ĐẶC — dùng cho trạng thái đang chọn. Xem `.tone-solid-*`. */
1437
1544
  const TONE_BG = {
1438
- critical: 'bg-critical',
1439
- danger: 'bg-danger',
1440
- warning: 'bg-warning',
1441
- success: 'bg-success',
1442
- 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',
1443
1551
  };
1444
- const TONE_BADGE = {
1445
- critical: 'bg-critical/10 text-critical',
1446
- danger: 'bg-danger/10 text-danger',
1447
- warning: 'bg-warning/10 text-warning',
1448
- success: 'bg-success/10 text-success',
1449
- 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',
1450
1578
  };
1451
1579
  function hasVal(v) {
1452
1580
  if (v == null)
@@ -1501,8 +1629,71 @@ function toneBg(tone) {
1501
1629
  return tone ? TONE_BG[tone] : NEUTRAL_BG;
1502
1630
  }
1503
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
+ */
1504
1643
  function toneBadge(tone) {
1505
- 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
+ /** Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền. */
1661
+ function toneTint(tone) {
1662
+ return isTone(tone) ? TONE_TINT[tone] : 'tone-tint-neutral';
1663
+ }
1664
+ /**
1665
+ * CHỮ theo tông — dùng thành viên dễ đọc nhất của mỗi họ severity, KHÔNG phải
1666
+ * ramp chart (ramp có `warning #f59e0b` chỉ 2.15:1 trên nền trắng). Xem
1667
+ * `.tone-fg-*` trong components.css.
1668
+ */
1669
+ function toneFg(tone) {
1670
+ return isTone(tone) ? `tone-fg-${tone}` : 'tone-fg-neutral';
1671
+ }
1672
+ /**
1673
+ * Tông THANG ĐO — cho filter chip severity. Critical ra ĐỎ SẪM (nền mềm pha
1674
+ * từ ramp `--og-sev-*`) thay vì pill đen đặc của `toneTint()`: chip filter là
1675
+ * một thang đo, không phải badge severity của một finding. Xem `.tone-scale-*`.
1676
+ */
1677
+ function toneScale(tone) {
1678
+ return isTone(tone) ? `tone-scale-${tone}` : 'tone-scale-neutral';
1679
+ }
1680
+ /** Viền theo tông — cho panel trạng thái (dropzone upload…). */
1681
+ function toneBorder(tone) {
1682
+ return isTone(tone) ? `tone-border-${tone}` : 'tone-border-neutral';
1683
+ }
1684
+ /**
1685
+ * CHẤM trạng thái trong hàng (`● Confirmed`) — demo dùng màu SEMANTIC
1686
+ * (`.og-statusdot--*`), không phải ramp chart.
1687
+ */
1688
+ function toneDot(tone) {
1689
+ return isTone(tone) ? `tone-dot-${tone}` : 'tone-dot-neutral';
1690
+ }
1691
+ /**
1692
+ * CHẤM LEGEND của chart — phải TRÙNG màu cung donut nên dùng ramp
1693
+ * `--og-sev-*`. Đừng dùng cho chấm trạng thái trong hàng.
1694
+ */
1695
+ function toneDotChart(tone) {
1696
+ return isTone(tone) ? `tone-dot-chart-${tone}` : 'tone-dot-chart-neutral';
1506
1697
  }
1507
1698
  /**
1508
1699
  * Tone → resolved CSS colour string for ECharts / canvas where a Tailwind
@@ -1510,10 +1701,22 @@ function toneBadge(tone) {
1510
1701
  * legacy comma syntax — see FxUtils.convertColorFromVariable.
1511
1702
  */
1512
1703
  function toneColor(tone, alpha = 1) {
1513
- const name = tone ? `--${tone}` : '--text-secondary';
1704
+ // Chart dùng ramp RIÊNG `--og-sev-*` (màu literal đo trên demo), không dùng
1705
+ // `--og-tone-*`. `information` không có trong ramp của demo (demo chỉ có 4
1706
+ // bậc Critical/High/Medium/Low) nên vẫn rơi về tone legacy.
1707
+ const SEV = new Set(['critical', 'danger', 'warning', 'success']);
1708
+ const name = tone ? (SEV.has(tone) ? `--og-sev-${tone}` : `--${tone}`) : '--text-secondary';
1514
1709
  return FxUtils.convertColorFromVariable(name, alpha, { legacyCommaSyntax: true });
1515
1710
  }
1516
1711
 
1712
+ function featureEnabled(flags, name) {
1713
+ return flags?.[name] === true;
1714
+ }
1715
+ /** Convenience for templates that gate on several flags at once. */
1716
+ function anyFeatureEnabled(flags, ...names) {
1717
+ return names.some((name) => featureEnabled(flags, name));
1718
+ }
1719
+
1517
1720
  class FxValidators {
1518
1721
  /**
1519
1722
  * Name fields: firstName, lastName, groupName, projectName, displayName
@@ -1905,37 +2108,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1905
2108
  }]
1906
2109
  }] });
1907
2110
 
2111
+ const OG_THEMES = [
2112
+ 'porcelain',
2113
+ 'charcoal',
2114
+ 'ivory',
2115
+ 'studio',
2116
+ 'blueprint',
2117
+ 'terminal',
2118
+ ];
2119
+ const OG_LIGHT_THEMES = ['porcelain', 'ivory', 'terminal'];
2120
+ const OG_THEME_META = [
2121
+ { name: 'porcelain', label: 'Porcelain', accent: '#4a7dbd', pageBg: '#f2f2f1', dark: false },
2122
+ { name: 'charcoal', label: 'Charcoal', accent: '#8fc866', pageBg: '#1a1811', dark: true },
2123
+ { name: 'ivory', label: 'Ivory', accent: '#d97757', pageBg: '#e8e6e1', dark: false },
2124
+ { name: 'studio', label: 'Studio', accent: '#5a8fd6', pageBg: '#0e0e10', dark: true },
2125
+ { name: 'blueprint', label: 'Blueprint', accent: '#e07a3a', pageBg: '#0e0e10', dark: true },
2126
+ { name: 'terminal', label: 'Terminal', accent: '#3a55d9', pageBg: '#e8e6e1', dark: false },
2127
+ ];
2128
+ /**
2129
+ * Theme NGƯỜI DÙNG được chọn (anh Tú chốt 31/08): chỉ còn một cặp
2130
+ * light/dark. Bốn theme còn lại vẫn tồn tại đầy đủ trong theme.css và
2131
+ * setTheme() vẫn nhận — chúng dùng cho trang /themes của demo và cho
2132
+ * harness screenshot chạy ma trận 6 theme; chỉ **giấu khỏi picker**.
2133
+ * Muốn mở lại: thêm tên vào mảng này, không phải dựng lại token.
2134
+ */
2135
+ const OG_PICKER_THEMES = ['porcelain', 'charcoal'];
2136
+ /** OG_THEME_META đã lọc theo OG_PICKER_THEMES — nguồn duy nhất cho mọi picker. */
2137
+ const OG_PICKER_THEME_META = OG_THEME_META.filter((t) => OG_PICKER_THEMES.includes(t.name));
2138
+ /**
2139
+ * V2 theme switching: one `data-og-theme` attribute on <html> selects the
2140
+ * --og-* token block (theme.css). During the migration window the service
2141
+ * also mirrors the legacy `light_theme` class on <body> so remotes built
2142
+ * against the pre-V2 stylesheet keep resolving the correct palette
2143
+ * (see the SKEW SHIELD note in theme.css).
2144
+ */
1908
2145
  class ThemeService {
1909
2146
  renderer;
1910
2147
  lightClass = 'light_theme';
1911
- theme$ = new BehaviorSubject('light');
2148
+ theme$ = new BehaviorSubject('porcelain');
1912
2149
  themeStorageKey = 'host-theme';
1913
2150
  constructor(rendererFactory) {
1914
2151
  this.renderer = rendererFactory.createRenderer(null, null);
1915
- const savedTheme = localStorage.getItem(this.themeStorageKey);
1916
- if (savedTheme) {
1917
- this.setLightMode(savedTheme === 'light');
1918
- }
1919
- else {
1920
- this.setLightMode(true);
1921
- }
1922
- }
1923
- setLightMode(enable) {
1924
- if (enable) {
2152
+ this.setTheme(this.readSavedTheme());
2153
+ }
2154
+ /** Primary API: activate one of the six V2 themes. */
2155
+ setTheme(theme) {
2156
+ if (!OG_THEMES.includes(theme))
2157
+ theme = 'porcelain';
2158
+ this.renderer.setAttribute(document.documentElement, 'data-og-theme', theme);
2159
+ // Skew shim for not-yet-migrated remote stylesheets (.light_theme era).
2160
+ if (this.isLightTheme(theme)) {
1925
2161
  this.renderer.addClass(document.body, this.lightClass);
1926
- this.theme$.next('light');
1927
- localStorage.setItem(this.themeStorageKey, 'light');
1928
2162
  }
1929
2163
  else {
1930
2164
  this.renderer.removeClass(document.body, this.lightClass);
1931
- this.theme$.next('dark');
1932
- localStorage.setItem(this.themeStorageKey, 'dark');
1933
2165
  }
1934
- window.dispatchEvent(new CustomEvent('theme-changed', { detail: { dark: !enable } }));
1935
- }
1936
- toggleTheme() {
1937
- const isLight = this.theme$.value === 'light';
1938
- this.setLightMode(!isLight);
2166
+ localStorage.setItem(this.themeStorageKey, theme);
2167
+ this.theme$.next(theme);
2168
+ // detail.dark is the pre-V2 contract (chart rebuilds on it); detail.theme
2169
+ // is new. Listeners must re-read colors on EVERY event — two light themes
2170
+ // can differ even though `dark` did not change.
2171
+ window.dispatchEvent(new CustomEvent('theme-changed', { detail: { dark: !this.isLightTheme(theme), theme } }));
1939
2172
  }
1940
2173
  themeChanges() {
1941
2174
  return this.theme$.asObservable();
@@ -1944,11 +2177,31 @@ class ThemeService {
1944
2177
  return this.theme$.value;
1945
2178
  }
1946
2179
  isLight() {
1947
- return this.theme$.value === 'light';
2180
+ return this.isLightTheme(this.theme$.value);
2181
+ }
2182
+ /** @deprecated V2: use setTheme(). Maps light→porcelain, dark→charcoal. */
2183
+ setLightMode(enable) {
2184
+ this.setTheme(enable ? 'porcelain' : 'charcoal');
2185
+ }
2186
+ /** @deprecated V2: use setTheme(). Flips between the two default themes. */
2187
+ toggleTheme() {
2188
+ this.setTheme(this.isLight() ? 'charcoal' : 'porcelain');
1948
2189
  }
1949
2190
  getLogoPath() {
1950
2191
  return 'assets/images/logo.png';
1951
2192
  }
2193
+ isLightTheme(theme) {
2194
+ return OG_LIGHT_THEMES.includes(theme);
2195
+ }
2196
+ readSavedTheme() {
2197
+ const saved = localStorage.getItem(this.themeStorageKey);
2198
+ // Migrate pre-V2 stored values.
2199
+ if (saved === 'light')
2200
+ return 'porcelain';
2201
+ if (saved === 'dark')
2202
+ return 'charcoal';
2203
+ return OG_THEMES.includes(saved) ? saved : 'porcelain';
2204
+ }
1952
2205
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemeService, deps: [{ token: i0.RendererFactory2 }], target: i0.ɵɵFactoryTarget.Injectable });
1953
2206
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemeService, providedIn: 'root' });
1954
2207
  }
@@ -2201,11 +2454,11 @@ class ButtonComponent {
2201
2454
  this.clicked.emit();
2202
2455
  }
2203
2456
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2204
- 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=\"20\"\n height=\"20\"\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]=\"20\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-white': buttonVariant() === 'primary' || buttonVariant() === 'success',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <div>{{ 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 });
2457
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2205
2458
  }
2206
2459
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
2207
2460
  type: Component,
2208
- 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=\"20\"\n height=\"20\"\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]=\"20\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-white': buttonVariant() === 'primary' || buttonVariant() === 'success',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <div>{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2461
+ args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2209
2462
  }], propDecorators: { label: [{
2210
2463
  type: Input
2211
2464
  }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], buttonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonVariant", required: false }] }], icon: [{
@@ -2417,11 +2670,11 @@ class CheckboxComponent extends FxComponent {
2417
2670
  this.onTouched();
2418
2671
  }
2419
2672
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CheckboxComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2420
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CheckboxComponent, isStandalone: true, selector: "fx-ui-checkbox", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-white w-4 h-4 transition-opacity duration-150\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n [style.opacity]=\"value ? 1 : 0\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n </span>\n\n @if (label) {\n <span class=\"mb-0 txt-field-label\">{{ label }}</span>\n }\n</label>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2673
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CheckboxComponent, isStandalone: true, selector: "fx-ui-checkbox", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-accent-contrast w-4 h-4 transition-opacity duration-150\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n [style.opacity]=\"value ? 1 : 0\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n </span>\n\n @if (label) {\n <span class=\"mb-0 txt-field-label\">{{ label }}</span>\n }\n</label>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2421
2674
  }
2422
2675
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CheckboxComponent, decorators: [{
2423
2676
  type: Component,
2424
- args: [{ selector: 'fx-ui-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-white w-4 h-4 transition-opacity duration-150\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n [style.opacity]=\"value ? 1 : 0\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n </span>\n\n @if (label) {\n <span class=\"mb-0 txt-field-label\">{{ label }}</span>\n }\n</label>\n" }]
2677
+ args: [{ selector: 'fx-ui-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-accent-contrast w-4 h-4 transition-opacity duration-150\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n [style.opacity]=\"value ? 1 : 0\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n </span>\n\n @if (label) {\n <span class=\"mb-0 txt-field-label\">{{ label }}</span>\n }\n</label>\n" }]
2425
2678
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2426
2679
  type: Input
2427
2680
  }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
@@ -2559,7 +2812,7 @@ class DatetimePicker extends FxComponent {
2559
2812
  provideNativeDateAdapter(),
2560
2813
  // en-GB display ⇒ DD/MM/YYYY + 24h — matches FxUtils.formatDateTime.
2561
2814
  { provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
2562
- ], usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius:.75rem;border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 10px 10px -5px 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);color:rgb(var(--text-primary));--mat-datepicker-calendar-container-background-color: rgb(var(--bg-primary));--mat-datepicker-calendar-container-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-selected-state-background-color: rgb(var(--primary));--mat-datepicker-calendar-date-selected-state-text-color: #fff;--mat-datepicker-calendar-date-today-outline-color: rgb(var(--primary));--mat-datepicker-calendar-date-hover-state-background-color: rgb(var(--bg-primary-hover));--mat-datepicker-calendar-header-text-color: rgb(var(--text-secondary));--mat-datepicker-calendar-period-button-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-navigation-button-icon-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-disabled-state-text-color: rgb(var(--text-placeholder));--mat-timepicker-container-background-color: rgb(var(--bg-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option{color:rgb(var(--text-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:rgb(var(--bg-primary-hover))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@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-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}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: MatDatepickerModule }, { kind: "component", type: i1$2.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i1$2.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "ngmodule", type: MatTimepickerModule }, { kind: "component", type: i2$2.MatTimepicker, selector: "mat-timepicker", inputs: ["interval", "options", "disableRipple", "aria-label", "aria-labelledby", "panelClass"], outputs: ["selected", "opened", "closed"], exportAs: ["matTimepicker"] }, { kind: "directive", type: i2$2.MatTimepickerInput, selector: "input[matTimepicker]", inputs: ["value", "matTimepicker", "matTimepickerMin", "matTimepickerMax", "matTimepickerOpenOnClick", "disabled"], outputs: ["valueChange"], exportAs: ["matTimepickerInput"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2815
+ ], usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 10px 10px -5px 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);color:rgb(var(--text-primary));--mat-datepicker-calendar-container-background-color: rgb(var(--bg-primary));--mat-datepicker-calendar-container-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-selected-state-background-color: rgb(var(--primary));--mat-datepicker-calendar-date-selected-state-text-color: #fff;--mat-datepicker-calendar-date-today-outline-color: rgb(var(--primary));--mat-datepicker-calendar-date-hover-state-background-color: rgb(var(--bg-primary-hover));--mat-datepicker-calendar-header-text-color: rgb(var(--text-secondary));--mat-datepicker-calendar-period-button-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-navigation-button-icon-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-disabled-state-text-color: rgb(var(--text-placeholder));--mat-timepicker-container-background-color: rgb(var(--bg-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option{color:rgb(var(--text-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:rgb(var(--bg-primary-hover))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@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-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}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: MatDatepickerModule }, { kind: "component", type: i1$2.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i1$2.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "ngmodule", type: MatTimepickerModule }, { kind: "component", type: i2$2.MatTimepicker, selector: "mat-timepicker", inputs: ["interval", "options", "disableRipple", "aria-label", "aria-labelledby", "panelClass"], outputs: ["selected", "opened", "closed"], exportAs: ["matTimepicker"] }, { kind: "directive", type: i2$2.MatTimepickerInput, selector: "input[matTimepicker]", inputs: ["value", "matTimepicker", "matTimepickerMin", "matTimepickerMax", "matTimepickerOpenOnClick", "disabled"], outputs: ["valueChange"], exportAs: ["matTimepickerInput"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2563
2816
  }
2564
2817
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DatetimePicker, decorators: [{
2565
2818
  type: Component,
@@ -2567,7 +2820,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2567
2820
  provideNativeDateAdapter(),
2568
2821
  // en-GB display ⇒ DD/MM/YYYY + 24h — matches FxUtils.formatDateTime.
2569
2822
  { provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
2570
- ], template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius:.75rem;border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 10px 10px -5px 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);color:rgb(var(--text-primary));--mat-datepicker-calendar-container-background-color: rgb(var(--bg-primary));--mat-datepicker-calendar-container-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-selected-state-background-color: rgb(var(--primary));--mat-datepicker-calendar-date-selected-state-text-color: #fff;--mat-datepicker-calendar-date-today-outline-color: rgb(var(--primary));--mat-datepicker-calendar-date-hover-state-background-color: rgb(var(--bg-primary-hover));--mat-datepicker-calendar-header-text-color: rgb(var(--text-secondary));--mat-datepicker-calendar-period-button-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-navigation-button-icon-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-disabled-state-text-color: rgb(var(--text-placeholder));--mat-timepicker-container-background-color: rgb(var(--bg-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option{color:rgb(var(--text-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:rgb(var(--bg-primary-hover))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@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-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}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
2823
+ ], template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 10px 10px -5px 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);color:rgb(var(--text-primary));--mat-datepicker-calendar-container-background-color: rgb(var(--bg-primary));--mat-datepicker-calendar-container-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-selected-state-background-color: rgb(var(--primary));--mat-datepicker-calendar-date-selected-state-text-color: #fff;--mat-datepicker-calendar-date-today-outline-color: rgb(var(--primary));--mat-datepicker-calendar-date-hover-state-background-color: rgb(var(--bg-primary-hover));--mat-datepicker-calendar-header-text-color: rgb(var(--text-secondary));--mat-datepicker-calendar-period-button-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-navigation-button-icon-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-disabled-state-text-color: rgb(var(--text-placeholder));--mat-timepicker-container-background-color: rgb(var(--bg-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option{color:rgb(var(--text-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:rgb(var(--bg-primary-hover))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@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-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}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
2571
2824
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2572
2825
  type: Input
2573
2826
  }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], errorMessages: [{
@@ -2819,11 +3072,11 @@ class RadioButtonToggleComponent extends FxComponent {
2819
3072
  this.onTouched();
2820
3073
  }
2821
3074
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonToggleComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2822
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonToggleComponent, isStandalone: true, selector: "fx-ui-radio-toggle, [fx-ui-radio-toggle]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-gray-900]=\"value !== opt.value\"\n class=\"flex-1 bg-bg-primary flex items-center pl-normal gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value\n }\"\n class=\"radio-container\"\n >\n @if(value === opt.value) {<span class=\"radio-dot\"></span>}\n </span>\n\n <span class=\"txt-field-label mb-0\">{{ opt.label }}</span></button\n >}\n </div>\n\n @if(invalid){\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3075
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonToggleComponent, isStandalone: true, selector: "fx-ui-radio-toggle, [fx-ui-radio-toggle]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 bg-bg-primary flex items-center pl-normal gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value\n }\"\n class=\"radio-container\"\n >\n @if(value === opt.value) {<span class=\"radio-dot\"></span>}\n </span>\n\n <span class=\"txt-field-label mb-0\">{{ opt.label }}</span></button\n >}\n </div>\n\n @if(invalid){\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2823
3076
  }
2824
3077
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonToggleComponent, decorators: [{
2825
3078
  type: Component,
2826
- args: [{ selector: 'fx-ui-radio-toggle, [fx-ui-radio-toggle]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-gray-900]=\"value !== opt.value\"\n class=\"flex-1 bg-bg-primary flex items-center pl-normal gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value\n }\"\n class=\"radio-container\"\n >\n @if(value === opt.value) {<span class=\"radio-dot\"></span>}\n </span>\n\n <span class=\"txt-field-label mb-0\">{{ opt.label }}</span></button\n >}\n </div>\n\n @if(invalid){\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n" }]
3079
+ args: [{ selector: 'fx-ui-radio-toggle, [fx-ui-radio-toggle]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 bg-bg-primary flex items-center pl-normal gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value\n }\"\n class=\"radio-container\"\n >\n @if(value === opt.value) {<span class=\"radio-dot\"></span>}\n </span>\n\n <span class=\"txt-field-label mb-0\">{{ opt.label }}</span></button\n >}\n </div>\n\n @if(invalid){\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n" }]
2827
3080
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2828
3081
  type: Input
2829
3082
  }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], errorMessages: [{
@@ -2855,7 +3108,7 @@ class RadioButtonComponent extends FxComponent {
2855
3108
  this.onTouched();
2856
3109
  }
2857
3110
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2858
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonComponent, isStandalone: true, selector: "fx-ui-radio", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-gray-900]=\"value !== opt.value\"\n class=\"flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n [matTooltip]=\"opt.tooltip || ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value,\n }\"\n class=\"radio-container\"\n >\n @if (value === opt.value) {\n <span class=\"radio-dot\"></span>\n }\n </span>\n <span class=\"txt-default mb-0\">{{ opt.label }}</span>\n </button>\n }\n @if (invalid) {\n <div class=\"txt-invalid\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3111
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonComponent, isStandalone: true, selector: "fx-ui-radio", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n [matTooltip]=\"opt.tooltip || ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value,\n }\"\n class=\"radio-container\"\n >\n @if (value === opt.value) {\n <span class=\"radio-dot\"></span>\n }\n </span>\n <span class=\"txt-default mb-0\">{{ opt.label }}</span>\n </button>\n }\n @if (invalid) {\n <div class=\"txt-invalid\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2859
3112
  }
2860
3113
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonComponent, decorators: [{
2861
3114
  type: Component,
@@ -2863,7 +3116,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2863
3116
  HeroIconComponent,
2864
3117
  MatTooltip,
2865
3118
  NgClass,
2866
- ], template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-gray-900]=\"value !== opt.value\"\n class=\"flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n [matTooltip]=\"opt.tooltip || ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value,\n }\"\n class=\"radio-container\"\n >\n @if (value === opt.value) {\n <span class=\"radio-dot\"></span>\n }\n </span>\n <span class=\"txt-default mb-0\">{{ opt.label }}</span>\n </button>\n }\n @if (invalid) {\n <div class=\"txt-invalid\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n" }]
3119
+ ], template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n [matTooltip]=\"opt.tooltip || ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value,\n }\"\n class=\"radio-container\"\n >\n @if (value === opt.value) {\n <span class=\"radio-dot\"></span>\n }\n </span>\n <span class=\"txt-default mb-0\">{{ opt.label }}</span>\n </button>\n }\n @if (invalid) {\n <div class=\"txt-invalid\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n" }]
2867
3120
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2868
3121
  type: Input
2869
3122
  }], tooltip: [{
@@ -2874,6 +3127,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2874
3127
 
2875
3128
  class SearchBarComponent extends FxComponent {
2876
3129
  ref;
3130
+ /** FxComponent leaves `value` undefined; without ngModel the input would
3131
+ render the literal string "undefined". */
3132
+ value = '';
2877
3133
  placeholder = input('Search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
2878
3134
  search = output();
2879
3135
  valueChange = output();
@@ -3014,17 +3270,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3014
3270
  type: Input
3015
3271
  }], 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"] }] } });
3016
3272
 
3017
- class SkeletonTableLoadingComponent {
3018
- columns = input(5, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
3019
- rows = input(5, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
3020
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonTableLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3021
- 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 });
3022
- }
3023
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonTableLoadingComponent, decorators: [{
3024
- type: Component,
3025
- 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" }]
3026
- }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
3027
-
3028
3273
  class SliderComponent extends FxComponent {
3029
3274
  ref;
3030
3275
  trackRef = viewChild.required('track');
@@ -3155,7 +3400,7 @@ class SliderComponent extends FxComponent {
3155
3400
  : { left: `${this.percent}%`, transform: 'translateX(-50%)' };
3156
3401
  }
3157
3402
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SliderComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3158
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SliderComponent, isStandalone: true, selector: "fx-ui-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { listeners: { "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "trackRef", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "thumbRef", first: true, predicate: ["thumb"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-gray-600 h-2\"\n [ngClass]=\"{ 'w-1 rotate-0 h-full w-auto bg-border-gray-700': vertical() }\"\n >\n @if (!vertical()) {\n <div\n class=\"absolute left-0 top-0 h-full rounded-full bg-primary\"\n [style.width.%]=\"percent\"\n ></div>\n }\n\n @if (vertical()) {\n <div\n class=\"absolute bottom-0 left-0 w-full rounded-full bg-primary\"\n [style.height.%]=\"percent\"\n style=\"transform-origin: bottom;\"\n ></div>\n }\n\n <button\n #thumb\n type=\"button\"\n class=\"absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary\"\n [ngStyle]=\"thumbStyle()\"\n role=\"slider\"\n aria-label=\"Slider\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-disabled]=\"disabled\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n [disabled]=\"disabled\"\n ></button>\n </div>\n </div>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3403
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SliderComponent, isStandalone: true, selector: "fx-ui-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { listeners: { "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "trackRef", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "thumbRef", first: true, predicate: ["thumb"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-track h-2\"\n [ngClass]=\"{ 'w-1 rotate-0 h-full w-auto bg-track': vertical() }\"\n >\n @if (!vertical()) {\n <div\n class=\"absolute left-0 top-0 h-full rounded-full bg-primary\"\n [style.width.%]=\"percent\"\n ></div>\n }\n\n @if (vertical()) {\n <div\n class=\"absolute bottom-0 left-0 w-full rounded-full bg-primary\"\n [style.height.%]=\"percent\"\n style=\"transform-origin: bottom;\"\n ></div>\n }\n\n <button\n #thumb\n type=\"button\"\n class=\"absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary\"\n [ngStyle]=\"thumbStyle()\"\n role=\"slider\"\n aria-label=\"Slider\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-disabled]=\"disabled\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n [disabled]=\"disabled\"\n ></button>\n </div>\n </div>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3159
3404
  }
3160
3405
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SliderComponent, decorators: [{
3161
3406
  type: Component,
@@ -3163,7 +3408,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3163
3408
  NgClass,
3164
3409
  NgStyle,
3165
3410
  InputComponent,
3166
- ], template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-gray-600 h-2\"\n [ngClass]=\"{ 'w-1 rotate-0 h-full w-auto bg-border-gray-700': vertical() }\"\n >\n @if (!vertical()) {\n <div\n class=\"absolute left-0 top-0 h-full rounded-full bg-primary\"\n [style.width.%]=\"percent\"\n ></div>\n }\n\n @if (vertical()) {\n <div\n class=\"absolute bottom-0 left-0 w-full rounded-full bg-primary\"\n [style.height.%]=\"percent\"\n style=\"transform-origin: bottom;\"\n ></div>\n }\n\n <button\n #thumb\n type=\"button\"\n class=\"absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary\"\n [ngStyle]=\"thumbStyle()\"\n role=\"slider\"\n aria-label=\"Slider\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-disabled]=\"disabled\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n [disabled]=\"disabled\"\n ></button>\n </div>\n </div>\n </div>\n</div>\n" }]
3411
+ ], template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-track h-2\"\n [ngClass]=\"{ 'w-1 rotate-0 h-full w-auto bg-track': vertical() }\"\n >\n @if (!vertical()) {\n <div\n class=\"absolute left-0 top-0 h-full rounded-full bg-primary\"\n [style.width.%]=\"percent\"\n ></div>\n }\n\n @if (vertical()) {\n <div\n class=\"absolute bottom-0 left-0 w-full rounded-full bg-primary\"\n [style.height.%]=\"percent\"\n style=\"transform-origin: bottom;\"\n ></div>\n }\n\n <button\n #thumb\n type=\"button\"\n class=\"absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary\"\n [ngStyle]=\"thumbStyle()\"\n role=\"slider\"\n aria-label=\"Slider\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-disabled]=\"disabled\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n [disabled]=\"disabled\"\n ></button>\n </div>\n </div>\n </div>\n</div>\n" }]
3167
3412
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { trackRef: [{ type: i0.ViewChild, args: ['track', { isSignal: true }] }], thumbRef: [{ type: i0.ViewChild, args: ['thumb', { isSignal: true }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], vertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "vertical", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], onKeydown: [{
3168
3413
  type: HostListener,
3169
3414
  args: ['keydown', ['$event']]
@@ -3188,34 +3433,75 @@ class SwitchComponent extends FxComponent {
3188
3433
  this.onTouched();
3189
3434
  }
3190
3435
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SwitchComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3191
- 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 });
3436
+ 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 });
3192
3437
  }
3193
3438
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SwitchComponent, decorators: [{
3194
3439
  type: Component,
3195
- 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" }]
3440
+ 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" }]
3196
3441
  }], propDecorators: { onSwitch: [{ type: i0.Output, args: ["onSwitch"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
3197
3442
 
3443
+ /**
3444
+ * Một tab của `fx-ui-tab-group`.
3445
+ *
3446
+ * ⚠️ Nội dung tab PHẢI bọc trong `<ng-template>`:
3447
+ *
3448
+ * ```html
3449
+ * <fx-ui-tab [label]="'Components'">
3450
+ * <ng-template>
3451
+ * <app-info-components … />
3452
+ * </ng-template>
3453
+ * </fx-ui-tab>
3454
+ * ```
3455
+ *
3456
+ * Vì sao bắt buộc: trước đây tab dùng `<ng-content>` bọc trong `<ng-template>`
3457
+ * của CHÍNH nó. Cách đó **không hoãn được gì** — `<ng-content>` chỉ CHIẾU node,
3458
+ * còn node do view khai báo (component cha) tạo **eager**. Kết quả: mọi tab đều
3459
+ * 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
3460
+ * scan-info: cả 8 cụm cùng fetch). Nhận `TemplateRef` từ cha thì nội dung chỉ
3461
+ * được dựng khi tab-group stamp template đó.
3462
+ *
3463
+ * Hệ quả cần biết: đổi tab sẽ **huỷ** nội dung tab cũ và dựng lại khi quay lại
3464
+ * — đúng bản chất lazy, giống `matTabContent` của Material.
3465
+ *
3466
+ * `descendants: false` để chỉ bắt `<ng-template>` là con TRỰC TIẾP, tránh nuốt
3467
+ * nhầm template lồng sâu bên trong nội dung tab.
3468
+ */
3198
3469
  class TabComponent {
3199
3470
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3200
3471
  icon;
3201
- content = viewChild.required(TemplateRef);
3472
+ /** Trailing count badge (V2 'page' variant); null/undefined hides it. */
3473
+ count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
3474
+ /** Tints the count badge (e.g. vulnerabilities tab shows a danger count). */
3475
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3476
+ content = contentChild.required(TemplateRef, { descendants: false });
3202
3477
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3203
- 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 } }, 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 });
3478
+ 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 });
3204
3479
  }
3205
3480
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, decorators: [{
3206
3481
  type: Component,
3207
- args: [{ selector: 'fx-ui-tab', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template><ng-content></ng-content></ng-template>\n" }]
3482
+ args: [{
3483
+ selector: 'fx-ui-tab',
3484
+ template: '',
3485
+ changeDetection: ChangeDetectionStrategy.OnPush,
3486
+ }]
3208
3487
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{
3209
3488
  type: Input
3210
- }], content: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
3489
+ }], 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 }] }] } });
3211
3490
 
3212
3491
  class TabGroupComponent {
3213
3492
  renderer;
3214
3493
  cdr = inject(ChangeDetectorRef);
3215
3494
  tabs = contentChildren(TabComponent, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
3216
3495
  activeIndex = 0;
3496
+ /** Emitted on user selection — enables [(activeIndex)] two-way binding. */
3497
+ activeIndexChange = output();
3498
+ /** 'page' = the V2 workspace tab strip (larger hit area, count badges). */
3499
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
3217
3500
  tabListRef = viewChild.required('tabList');
3218
- underlineRef = viewChild.required('underline');
3501
+ underlineRef = viewChild('underline', ...(ngDevMode ? [{ debugName: "underlineRef" }] : /* istanbul ignore next */ []));
3502
+ pagePillRef = viewChild('pagePill', ...(ngDevMode ? [{ debugName: "pagePillRef" }] : /* istanbul ignore next */ []));
3503
+ /** Last applied pill geometry — skip DOM writes when nothing moved. */
3504
+ pillState = { left: -1, width: -1, painted: false };
3219
3505
  canScrollLeft = false;
3220
3506
  canScrollRight = false;
3221
3507
  animateDelay = 50;
@@ -3234,8 +3520,12 @@ class TabGroupComponent {
3234
3520
  }
3235
3521
  selectTab(index) {
3236
3522
  this.activeIndex = index;
3523
+ this.activeIndexChange.emit(index);
3237
3524
  this.scrollToActiveTab(true);
3238
3525
  }
3526
+ badgeClass(tab) {
3527
+ return toneBadge(tab.tone());
3528
+ }
3239
3529
  /** Roving tablist keyboard nav: arrows move AND activate (selection follows focus). */
3240
3530
  onTabKeydown(event, index) {
3241
3531
  const count = this.tabs().length;
@@ -3263,9 +3553,52 @@ class TabGroupComponent {
3263
3553
  const buttons = this.tabListRef().nativeElement.querySelectorAll('.tab-btn');
3264
3554
  buttons[next]?.focus();
3265
3555
  }
3556
+ /** Parent-driven activeIndex (deep-link ?tab=, quick-links) must also move the pill. */
3557
+ ngOnChanges(changes) {
3558
+ if (changes['activeIndex'] && !changes['activeIndex'].firstChange) {
3559
+ setTimeout(() => this.scrollToActiveTab(false), 0);
3560
+ }
3561
+ }
3562
+ /**
3563
+ * Re-measure every view check: catches label-width changes (language switch,
3564
+ * count arriving) with cached geometry so the DOM write only happens when
3565
+ * the pill actually moved.
3566
+ */
3567
+ ngAfterViewChecked() {
3568
+ this.updatePagePill();
3569
+ }
3570
+ /** Slide the 'page' pill to the active tab (offset* is scroll-content relative). */
3571
+ updatePagePill() {
3572
+ const pillEl = this.pagePillRef()?.nativeElement;
3573
+ if (!pillEl)
3574
+ return;
3575
+ const tabButtons = this.tabListRef().nativeElement.querySelectorAll('.tab-btn');
3576
+ const activeTab = tabButtons[this.activeIndex];
3577
+ if (!activeTab)
3578
+ return;
3579
+ const left = activeTab.offsetLeft;
3580
+ const width = activeTab.offsetWidth;
3581
+ if (width === 0)
3582
+ return; // hidden (display:none ancestors) — keep last state
3583
+ if (left === this.pillState.left && width === this.pillState.width)
3584
+ return;
3585
+ // First paint positions without transition — no fly-in from x=0 on load.
3586
+ if (!this.pillState.painted) {
3587
+ this.renderer.setStyle(pillEl, 'transition', 'none');
3588
+ requestAnimationFrame(() => this.renderer.removeStyle(pillEl, 'transition'));
3589
+ this.pillState.painted = true;
3590
+ }
3591
+ this.renderer.setStyle(pillEl, 'width', `${width}px`);
3592
+ this.renderer.setStyle(pillEl, 'transform', `translateX(${left}px)`);
3593
+ this.pillState = { ...this.pillState, left, width };
3594
+ }
3266
3595
  updateUnderlinePosition() {
3596
+ // The 'page' variant slides .fx-tabs-pill instead (see updatePagePill);
3597
+ // the underline element only exists on the default variant.
3598
+ const underlineEl = this.underlineRef()?.nativeElement;
3599
+ if (!underlineEl)
3600
+ return;
3267
3601
  const tabListEl = this.tabListRef().nativeElement;
3268
- const underlineEl = this.underlineRef().nativeElement;
3269
3602
  const tabButtons = tabListEl.querySelectorAll('.tab-btn');
3270
3603
  const activeTab = tabButtons[this.activeIndex];
3271
3604
  if (!activeTab || !underlineEl)
@@ -3323,9 +3656,10 @@ class TabGroupComponent {
3323
3656
  }
3324
3657
  onResize() {
3325
3658
  this.updateUnderlinePosition();
3659
+ this.updatePagePill();
3326
3660
  }
3327
3661
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabGroupComponent, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
3328
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TabGroupComponent, isStandalone: true, selector: "fx-ui-tab-group", inputs: { activeIndex: "activeIndex" }, host: { listeners: { "window:resize": "onResize()" } }, queries: [{ propertyName: "tabs", predicate: TabComponent, isSignal: true }], viewQueries: [{ propertyName: "tabListRef", first: true, predicate: ["tabList"], descendants: true, isSignal: true }, { propertyName: "underlineRef", first: true, predicate: ["underline"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n (scroll)=\"onScroll()\"\n >\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n >\n <div class=\"flex flex-row items-center justify-center\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"{\n 'text-gradient-primary-start mb-small': i === activeIndex,\n 'text-text-placeholder': i !== activeIndex,\n }\"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n i === activeIndex ? 'txt-field-label text-gradient-primary-start' : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n </div>\n </button>\n }\n\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--gradient-primary-start));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}@property --tw-duration{syntax: \"*\"; inherits: false;}@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-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\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"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3662
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TabGroupComponent, isStandalone: true, selector: "fx-ui-tab-group", inputs: { activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: false, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeIndexChange: "activeIndexChange" }, host: { listeners: { "window:resize": "onResize()" } }, queries: [{ propertyName: "tabs", predicate: TabComponent, isSignal: true }], viewQueries: [{ propertyName: "tabListRef", first: true, predicate: ["tabList"], descendants: true, isSignal: true }, { propertyName: "underlineRef", first: true, predicate: ["underline"], descendants: true, isSignal: true }, { propertyName: "pagePillRef", first: true, predicate: ["pagePill"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n [class.fx-tabs-frame]=\"variant() === 'page'\"\n (scroll)=\"onScroll()\"\n >\n <!-- 'page' variant: one raised pill that SLIDES to the active tab\n (anh T\u00FA 29/08 \u2014 same motion as fx-ui-segmented) -->\n @if (variant() === 'page') {\n <span #pagePill aria-hidden=\"true\" class=\"fx-tabs-pill\"></span>\n }\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n [class.tab-bar-btn-page]=\"variant() === 'page'\"\n >\n <div class=\"flex flex-row items-center justify-center gap-xs\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"variant() === 'page' ? 14 : 20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-strong'\n : 'text-text-placeholder'\n : i === activeIndex\n ? 'text-accent-fg mb-small'\n : 'text-text-placeholder'\n \"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-[13px] leading-none font-semibold text-strong [text-box:trim-both_cap_alphabetic]'\n : 'text-[13px] leading-none font-medium text-muted [text-box:trim-both_cap_alphabetic]'\n : i === activeIndex\n ? 'txt-field-label text-accent-fg'\n : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n @if (tab.count() !== null) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill\"\n [ngClass]=\"badgeClass(tab)\"\n style=\"font-family: var(--og-font-figure)\"\n >\n {{ tab.count() }}\n </span>\n }\n </div>\n </button>\n }\n\n @if (variant() !== 'page') {\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n }\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--og-accent));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.fx-tabs-frame{width:fit-content;max-width:100%;gap:.125rem;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-sunken));padding:3px}.tab-bar-btn-page{z-index:1;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:transparent;padding-inline:.75rem;padding-block:5px;color:rgb(var(--og-text-muted))}.tab-bar-btn-page[aria-selected=false]:hover{color:rgb(var(--og-text))}.fx-tabs-pill{pointer-events:none;position:absolute;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--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);top:3px;bottom:3px;left:0;width:0;transform:translate(0);transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width}@media(prefers-reduced-motion:reduce){.fx-tabs-pill{transition:none}}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@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-duration: initial;--tw-border-style: solid;--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}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\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"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3329
3663
  }
3330
3664
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabGroupComponent, decorators: [{
3331
3665
  type: Component,
@@ -3334,10 +3668,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3334
3668
  NgClass,
3335
3669
  NgTemplateOutlet,
3336
3670
  InputComponent,
3337
- ], template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n (scroll)=\"onScroll()\"\n >\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n >\n <div class=\"flex flex-row items-center justify-center\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"{\n 'text-gradient-primary-start mb-small': i === activeIndex,\n 'text-text-placeholder': i !== activeIndex,\n }\"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n i === activeIndex ? 'txt-field-label text-gradient-primary-start' : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n </div>\n </button>\n }\n\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--gradient-primary-start));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}@property --tw-duration{syntax: \"*\"; inherits: false;}@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-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3671
+ ], template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n [class.fx-tabs-frame]=\"variant() === 'page'\"\n (scroll)=\"onScroll()\"\n >\n <!-- 'page' variant: one raised pill that SLIDES to the active tab\n (anh T\u00FA 29/08 \u2014 same motion as fx-ui-segmented) -->\n @if (variant() === 'page') {\n <span #pagePill aria-hidden=\"true\" class=\"fx-tabs-pill\"></span>\n }\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n [class.tab-bar-btn-page]=\"variant() === 'page'\"\n >\n <div class=\"flex flex-row items-center justify-center gap-xs\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"variant() === 'page' ? 14 : 20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-strong'\n : 'text-text-placeholder'\n : i === activeIndex\n ? 'text-accent-fg mb-small'\n : 'text-text-placeholder'\n \"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-[13px] leading-none font-semibold text-strong [text-box:trim-both_cap_alphabetic]'\n : 'text-[13px] leading-none font-medium text-muted [text-box:trim-both_cap_alphabetic]'\n : i === activeIndex\n ? 'txt-field-label text-accent-fg'\n : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n @if (tab.count() !== null) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill\"\n [ngClass]=\"badgeClass(tab)\"\n style=\"font-family: var(--og-font-figure)\"\n >\n {{ tab.count() }}\n </span>\n }\n </div>\n </button>\n }\n\n @if (variant() !== 'page') {\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n }\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--og-accent));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.fx-tabs-frame{width:fit-content;max-width:100%;gap:.125rem;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-sunken));padding:3px}.tab-bar-btn-page{z-index:1;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:transparent;padding-inline:.75rem;padding-block:5px;color:rgb(var(--og-text-muted))}.tab-bar-btn-page[aria-selected=false]:hover{color:rgb(var(--og-text))}.fx-tabs-pill{pointer-events:none;position:absolute;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--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);top:3px;bottom:3px;left:0;width:0;transform:translate(0);transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width}@media(prefers-reduced-motion:reduce){.fx-tabs-pill{transition:none}}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@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-duration: initial;--tw-border-style: solid;--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}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3338
3672
  }], ctorParameters: () => [{ type: i0.Renderer2 }], propDecorators: { tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TabComponent), { isSignal: true }] }], activeIndex: [{
3339
3673
  type: Input
3340
- }], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], underlineRef: [{ type: i0.ViewChild, args: ['underline', { isSignal: true }] }], onResize: [{
3674
+ }], activeIndexChange: [{ type: i0.Output, args: ["activeIndexChange"] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], underlineRef: [{ type: i0.ViewChild, args: ['underline', { isSignal: true }] }], pagePillRef: [{ type: i0.ViewChild, args: ['pagePill', { isSignal: true }] }], onResize: [{
3341
3675
  type: HostListener,
3342
3676
  args: ['window:resize']
3343
3677
  }] } });
@@ -3367,11 +3701,11 @@ class TagComponent {
3367
3701
  this.clicked.emit();
3368
3702
  }
3369
3703
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3370
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TagComponent, isStandalone: true, selector: "fx-ui-tag", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-white' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-white font-medium' : ''}`\">\n {{ label() }}\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3704
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TagComponent, isStandalone: true, selector: "fx-ui-tag", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface font-medium' : ''}`\">\n {{ label() }}\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3371
3705
  }
3372
3706
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TagComponent, decorators: [{
3373
3707
  type: Component,
3374
- args: [{ selector: 'fx-ui-tag', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-white' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-white font-medium' : ''}`\">\n {{ label() }}\n </div>\n</div>\n" }]
3708
+ args: [{ selector: 'fx-ui-tag', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface font-medium' : ''}`\">\n {{ label() }}\n </div>\n</div>\n" }]
3375
3709
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], icon: [{
3376
3710
  type: Input
3377
3711
  }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }], clicked: [{
@@ -3464,11 +3798,11 @@ class TreeDiagram {
3464
3798
  this.cdr.detectChanges();
3465
3799
  }
3466
3800
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TreeDiagram, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3467
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TreeDiagram, isStandalone: true, selector: "fx-ui-tree-diagram", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "root", first: true, predicate: ["root"], descendants: true, isSignal: true }, { propertyName: "children", predicate: ["child"], descendants: true }], ngImport: i0, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-blue-600 text-white px-7 py-3 rounded-md font-semibold text-center\"\n >\n {{ data().label }}\n </div>\n\n <!-- CHILDREN -->\n <div class=\"mt-16 flex justify-center gap-16\">\n @for (child of data().children; track child) {\n <div\n #child\n class=\"w-48 bg-rose-600 text-white px-4 py-4 rounded-lg text-center\"\n >\n <div class=\"font-semibold text-lg\">{{ child.label }}</div>\n <div class=\"mt-1 text-sm opacity-90\">{{ child.status }}</div>\n </div>\n }\n </div>\n\n <!-- SVG CONNECTORS -->\n <svg\n class=\"absolute inset-0 pointer-events-none\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n >\n @for (line of lines; track line) {\n <line\n [attr.x1]=\"line.x1\"\n [attr.y1]=\"line.y1\"\n [attr.x2]=\"line.x2\"\n [attr.y2]=\"line.y2\"\n stroke=\"#1f2937\"\n stroke-width=\"2\"\n />\n }\n </svg>\n\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3801
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TreeDiagram, isStandalone: true, selector: "fx-ui-tree-diagram", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "root", first: true, predicate: ["root"], descendants: true, isSignal: true }, { propertyName: "children", predicate: ["child"], descendants: true }], ngImport: i0, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-accent text-accent-contrast px-7 py-3 rounded-md font-semibold text-center\"\n >\n {{ data().label }}\n </div>\n\n <!-- CHILDREN -->\n <div class=\"mt-16 flex justify-center gap-16\">\n @for (child of data().children; track child) {\n <div\n #child\n class=\"w-48 bg-surface-raised text-strong border border-border px-4 py-4 rounded-lg text-center\"\n >\n <div class=\"font-semibold text-lg\">{{ child.label }}</div>\n <div class=\"mt-1 text-sm opacity-90\">{{ child.status }}</div>\n </div>\n }\n </div>\n\n <!-- SVG CONNECTORS -->\n <svg\n class=\"absolute inset-0 pointer-events-none\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n >\n @for (line of lines; track line) {\n <line\n [attr.x1]=\"line.x1\"\n [attr.y1]=\"line.y1\"\n [attr.x2]=\"line.x2\"\n [attr.y2]=\"line.y2\"\n stroke=\"rgb(var(--og-border-strong))\"\n stroke-width=\"2\"\n />\n }\n </svg>\n\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3468
3802
  }
3469
3803
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TreeDiagram, decorators: [{
3470
3804
  type: Component,
3471
- args: [{ selector: 'fx-ui-tree-diagram', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-blue-600 text-white px-7 py-3 rounded-md font-semibold text-center\"\n >\n {{ data().label }}\n </div>\n\n <!-- CHILDREN -->\n <div class=\"mt-16 flex justify-center gap-16\">\n @for (child of data().children; track child) {\n <div\n #child\n class=\"w-48 bg-rose-600 text-white px-4 py-4 rounded-lg text-center\"\n >\n <div class=\"font-semibold text-lg\">{{ child.label }}</div>\n <div class=\"mt-1 text-sm opacity-90\">{{ child.status }}</div>\n </div>\n }\n </div>\n\n <!-- SVG CONNECTORS -->\n <svg\n class=\"absolute inset-0 pointer-events-none\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n >\n @for (line of lines; track line) {\n <line\n [attr.x1]=\"line.x1\"\n [attr.y1]=\"line.y1\"\n [attr.x2]=\"line.x2\"\n [attr.y2]=\"line.y2\"\n stroke=\"#1f2937\"\n stroke-width=\"2\"\n />\n }\n </svg>\n\n</div>\n" }]
3805
+ args: [{ selector: 'fx-ui-tree-diagram', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-accent text-accent-contrast px-7 py-3 rounded-md font-semibold text-center\"\n >\n {{ data().label }}\n </div>\n\n <!-- CHILDREN -->\n <div class=\"mt-16 flex justify-center gap-16\">\n @for (child of data().children; track child) {\n <div\n #child\n class=\"w-48 bg-surface-raised text-strong border border-border px-4 py-4 rounded-lg text-center\"\n >\n <div class=\"font-semibold text-lg\">{{ child.label }}</div>\n <div class=\"mt-1 text-sm opacity-90\">{{ child.status }}</div>\n </div>\n }\n </div>\n\n <!-- SVG CONNECTORS -->\n <svg\n class=\"absolute inset-0 pointer-events-none\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n >\n @for (line of lines; track line) {\n <line\n [attr.x1]=\"line.x1\"\n [attr.y1]=\"line.y1\"\n [attr.x2]=\"line.x2\"\n [attr.y2]=\"line.y2\"\n stroke=\"rgb(var(--og-border-strong))\"\n stroke-width=\"2\"\n />\n }\n </svg>\n\n</div>\n" }]
3472
3806
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], root: [{ type: i0.ViewChild, args: ['root', { isSignal: true }] }], children: [{
3473
3807
  type: ViewChildren,
3474
3808
  args: ['child']
@@ -3509,7 +3843,7 @@ class KanbanBoardComponent {
3509
3843
  });
3510
3844
  }
3511
3845
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: KanbanBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3512
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: KanbanBoardComponent, isStandalone: true, selector: "fx-ui-kanban-board", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { columnChanged: "columnChanged" }, ngImport: i0, template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:.5rem;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));flex-shrink:0}@media(prefers-color-scheme:dark){.kanban-column{background-color:var(--color-gray-800, oklch(27.8% .033 256.848))}}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200, oklch(92.8% .006 264.531));padding:calc(var(--spacing, .25rem) * 4)}@media(prefers-color-scheme:dark){.kanban-column-header{border-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:var(--color-gray-900, oklch(21% .034 264.665))}@media(prefers-color-scheme:dark){.kanban-column-title{color:var(--color-gray-100, oklch(96.7% .003 264.542))}}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:var(--color-gray-200, oklch(92.8% .006 264.531));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-gray-700, oklch(37.3% .034 259.733))}@media(prefers-color-scheme:dark){.kanban-column-count{background-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}@media(prefers-color-scheme:dark){.kanban-column-count{color:var(--color-gray-300, oklch(87.2% .01 258.338))}}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:.5rem;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200, oklch(92.8% .006 264.531));background-color:var(--color-white, #fff);--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);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}@media(prefers-color-scheme:dark){.kanban-item{border-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}@media(prefers-color-scheme:dark){.kanban-item{background-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}.kanban-item: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)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:var(--color-gray-900, oklch(21% .034 264.665))}@media(prefers-color-scheme:dark){.kanban-item-default{color:var(--color-gray-100, oklch(96.7% .003 264.542))}}.kanban-item-placeholder{border-radius:.5rem;border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:var(--color-gray-400, oklch(70.7% .022 261.325));background-color:var(--color-gray-200, oklch(92.8% .006 264.531));opacity:50%;min-height:60px}@media(prefers-color-scheme:dark){.kanban-item-placeholder{border-color:var(--color-gray-500, oklch(55.1% .027 264.364))}}@media(prefers-color-scheme:dark){.kanban-item-placeholder{background-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-400, oklch(70.7% .022 261.325));border-radius:.5rem;border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:var(--color-gray-300, oklch(87.2% .01 258.338))}@media(prefers-color-scheme:dark){.kanban-column-empty{color:var(--color-gray-500, oklch(55.1% .027 264.364))}}@media(prefers-color-scheme:dark){.kanban-column-empty{border-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}.cdk-drag-preview{border-radius:.5rem;opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px 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)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{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-duration{syntax: \"*\"; inherits: false;}@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-font-weight: initial;--tw-space-y-reverse: 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-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3846
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: KanbanBoardComponent, isStandalone: true, selector: "fx-ui-kanban-board", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { columnChanged: "columnChanged" }, ngImport: i0, template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:var(--og-radius-card);background-color:rgb(var(--og-surface-sunken));flex-shrink:0}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgb(var(--og-border));padding:calc(var(--spacing, .25rem) * 4)}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text))}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:rgb(var(--og-neutral-soft-bg));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--og-neutral-soft-fg))}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--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);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}.kanban-item: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)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:rgb(var(--og-text))}.kanban-item-placeholder{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--border-strong));background-color:rgb(var(--og-neutral-soft-bg));opacity:50%;min-height:60px}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:rgb(var(--og-text-faint));border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--og-border))}.cdk-drag-preview{border-radius:var(--og-radius-card);opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px 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)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{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-duration{syntax: \"*\"; inherits: false;}@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-font-weight: initial;--tw-space-y-reverse: 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-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3513
3847
  }
3514
3848
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: KanbanBoardComponent, decorators: [{
3515
3849
  type: Component,
@@ -3518,7 +3852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3518
3852
  CdkDrag,
3519
3853
  CdkDragPlaceholder,
3520
3854
  NgTemplateOutlet,
3521
- ], template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:.5rem;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));flex-shrink:0}@media(prefers-color-scheme:dark){.kanban-column{background-color:var(--color-gray-800, oklch(27.8% .033 256.848))}}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200, oklch(92.8% .006 264.531));padding:calc(var(--spacing, .25rem) * 4)}@media(prefers-color-scheme:dark){.kanban-column-header{border-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:var(--color-gray-900, oklch(21% .034 264.665))}@media(prefers-color-scheme:dark){.kanban-column-title{color:var(--color-gray-100, oklch(96.7% .003 264.542))}}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:var(--color-gray-200, oklch(92.8% .006 264.531));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-gray-700, oklch(37.3% .034 259.733))}@media(prefers-color-scheme:dark){.kanban-column-count{background-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}@media(prefers-color-scheme:dark){.kanban-column-count{color:var(--color-gray-300, oklch(87.2% .01 258.338))}}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:.5rem;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200, oklch(92.8% .006 264.531));background-color:var(--color-white, #fff);--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);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}@media(prefers-color-scheme:dark){.kanban-item{border-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}@media(prefers-color-scheme:dark){.kanban-item{background-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}.kanban-item: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)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:var(--color-gray-900, oklch(21% .034 264.665))}@media(prefers-color-scheme:dark){.kanban-item-default{color:var(--color-gray-100, oklch(96.7% .003 264.542))}}.kanban-item-placeholder{border-radius:.5rem;border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:var(--color-gray-400, oklch(70.7% .022 261.325));background-color:var(--color-gray-200, oklch(92.8% .006 264.531));opacity:50%;min-height:60px}@media(prefers-color-scheme:dark){.kanban-item-placeholder{border-color:var(--color-gray-500, oklch(55.1% .027 264.364))}}@media(prefers-color-scheme:dark){.kanban-item-placeholder{background-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-400, oklch(70.7% .022 261.325));border-radius:.5rem;border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:var(--color-gray-300, oklch(87.2% .01 258.338))}@media(prefers-color-scheme:dark){.kanban-column-empty{color:var(--color-gray-500, oklch(55.1% .027 264.364))}}@media(prefers-color-scheme:dark){.kanban-column-empty{border-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}.cdk-drag-preview{border-radius:.5rem;opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px 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)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{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-duration{syntax: \"*\"; inherits: false;}@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-font-weight: initial;--tw-space-y-reverse: 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-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3855
+ ], template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:var(--og-radius-card);background-color:rgb(var(--og-surface-sunken));flex-shrink:0}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgb(var(--og-border));padding:calc(var(--spacing, .25rem) * 4)}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text))}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:rgb(var(--og-neutral-soft-bg));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--og-neutral-soft-fg))}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--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);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}.kanban-item: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)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:rgb(var(--og-text))}.kanban-item-placeholder{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--border-strong));background-color:rgb(var(--og-neutral-soft-bg));opacity:50%;min-height:60px}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:rgb(var(--og-text-faint));border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--og-border))}.cdk-drag-preview{border-radius:var(--og-radius-card);opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px 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)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{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-duration{syntax: \"*\"; inherits: false;}@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-font-weight: initial;--tw-space-y-reverse: 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-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3522
3856
  }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], itemTemplate: [{
3523
3857
  type: Input
3524
3858
  }], columnChanged: [{ type: i0.Output, args: ["columnChanged"] }] } });
@@ -3527,8 +3861,15 @@ class DrawerComponent {
3527
3861
  router;
3528
3862
  cdr = inject(ChangeDetectorRef);
3529
3863
  items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
3864
+ /** V2: bottom-pinned section (Administration group) — same item rendering,
3865
+ separated by a rule, above the [drawerFooter] slot and version. */
3866
+ footerItems = input([], ...(ngDevMode ? [{ debugName: "footerItems" }] : /* istanbul ignore next */ []));
3530
3867
  isExpanded = true;
3531
3868
  logo;
3869
+ /** Wordmark cạnh logo, chỉ hiện khi drawer mở. Bỏ trống = chỉ có mark. */
3870
+ logoText;
3871
+ /** Dòng phụ dưới logoText (tagline/workspace). Chỉ hiện khi drawer mở. */
3872
+ logoSubtext;
3532
3873
  version;
3533
3874
  toggleExpanded = output();
3534
3875
  itemClicked = output();
@@ -3620,6 +3961,7 @@ class DrawerComponent {
3620
3961
  return false;
3621
3962
  };
3622
3963
  expandParents(this.items());
3964
+ expandParents(this.footerItems());
3623
3965
  }
3624
3966
  getAccordionState(itemId) {
3625
3967
  return this.isAccordionExpanded(itemId) ? 'expanded' : 'collapsed';
@@ -3628,7 +3970,7 @@ class DrawerComponent {
3628
3970
  return this.isExpanded ? 'expanded' : 'collapsed';
3629
3971
  }
3630
3972
  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 });
3631
- 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 }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: false, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", 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\n href=\"/\"\n class=\"h-[60px]\"\n [ngClass]=\"{ 'flex items-center justify-center': !isExpanded, 'p-4': isExpanded }\"\n >\n @if (logo) {\n <img\n [src]=\"logo\"\n alt=\"Home\"\n class=\"h-[20px] w-auto transition-all duration-300 ease-in-out\"\n [ngClass]=\"{ 'scale-90': !isExpanded, 'ml-5': isExpanded }\"\n />\n } @else {\n <div class=\"h-[30px] w-auto transition-all duration-300 ease-in-out\"></div>\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 <!-- 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 <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</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;height:100%;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default))}.drawer-container{display:flex;height:100%;flex-direction:column;overflow:auto;background-color:rgb(var(--bg-primary));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-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:.125rem;background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.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:.25rem;--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);background-color:rgb(var(--bg-primary-hover));color:rgb(var(--text-primary))}.drawer-item.active{background-image:linear-gradient(135deg,rgb(var(--gradient-primary-start)),rgb(var(--gradient-primary-end)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #fff)}.drawer-item.active .item-icon{color:var(--color-white, #fff)}.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:Roboto,monospace}.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))}.drawer-container[style*=\"width: 64px\"] .drawer-item{justify-content:center;padding-left:16px!important}.drawer-container[style*=\"width: 64px\"] .item-content{justify-content:center}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@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-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;--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: [
3973
+ 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: [
3632
3974
  trigger('drawerExpand', [
3633
3975
  state('collapsed', style({ width: '70px' })),
3634
3976
  state('expanded', style({ width: '260px' })),
@@ -3655,16 +3997,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3655
3997
  transition('collapsed <=> expanded', animate('300ms cubic-bezier(0.4, 0, 0.2, 1)')),
3656
3998
  ]),
3657
3999
  ], changeDetection: ChangeDetectionStrategy.OnPush, imports: [
3658
- NgClass,
3659
4000
  NgTemplateOutlet,
3660
4001
  MatTooltip,
3661
4002
  HeroIconComponent,
3662
4003
  RouterLink,
3663
- ], template: "<div class=\"drawer-host-wrapper\">\n <a\n href=\"/\"\n class=\"h-[60px]\"\n [ngClass]=\"{ 'flex items-center justify-center': !isExpanded, 'p-4': isExpanded }\"\n >\n @if (logo) {\n <img\n [src]=\"logo\"\n alt=\"Home\"\n class=\"h-[20px] w-auto transition-all duration-300 ease-in-out\"\n [ngClass]=\"{ 'scale-90': !isExpanded, 'ml-5': isExpanded }\"\n />\n } @else {\n <div class=\"h-[30px] w-auto transition-all duration-300 ease-in-out\"></div>\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 <!-- 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 <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</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;height:100%;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default))}.drawer-container{display:flex;height:100%;flex-direction:column;overflow:auto;background-color:rgb(var(--bg-primary));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-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:.125rem;background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.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:.25rem;--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);background-color:rgb(var(--bg-primary-hover));color:rgb(var(--text-primary))}.drawer-item.active{background-image:linear-gradient(135deg,rgb(var(--gradient-primary-start)),rgb(var(--gradient-primary-end)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #fff)}.drawer-item.active .item-icon{color:var(--color-white, #fff)}.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:Roboto,monospace}.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))}.drawer-container[style*=\"width: 64px\"] .drawer-item{justify-content:center;padding-left:16px!important}.drawer-container[style*=\"width: 64px\"] .item-content{justify-content:center}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@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;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@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-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;--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3664
- }], ctorParameters: () => [{ type: i2$1.Router }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], isExpanded: [{
4004
+ ], 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"] }]
4005
+ }], 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: [{
3665
4006
  type: Input
3666
4007
  }], logo: [{
3667
4008
  type: Input
4009
+ }], logoText: [{
4010
+ type: Input
4011
+ }], logoSubtext: [{
4012
+ type: Input
3668
4013
  }], version: [{
3669
4014
  type: Input
3670
4015
  }], toggleExpanded: [{ type: i0.Output, args: ["toggleExpanded"] }], itemClicked: [{ type: i0.Output, args: ["itemClicked"] }] } });
@@ -3672,40 +4017,80 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3672
4017
  /** Row-card skeleton placeholder for master-detail list panes. */
3673
4018
  class SkeletonListComponent {
3674
4019
  rows = input(5, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
3675
- range(n) {
3676
- return Array.from({ length: Number(n) || 0 }, (_, i) => i);
3677
- }
4020
+ /** computed, không phải method: method trả mảng MỚI mỗi lần render. */
4021
+ rowList = computed(() => range$5(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
3678
4022
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3679
- 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 });
4023
+ 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 });
3680
4024
  }
3681
4025
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonListComponent, decorators: [{
3682
4026
  type: Component,
3683
- 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" }]
4027
+ 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" }]
3684
4028
  }], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
4029
+ function range$5(n) {
4030
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
4031
+ }
3685
4032
 
4033
+ const ROW_WIDTHS = [90, 70, 80, 60];
3686
4034
  /** Card-shaped skeleton placeholder for detail panels. */
3687
4035
  class SkeletonDetailComponent {
3688
4036
  /** Number of content cards rendered below the header card. */
3689
4037
  cards = input(3, ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
3690
4038
  /** Rows per content card. */
3691
4039
  rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
3692
- range(n) {
3693
- return Array.from({ length: Number(n) || 0 }, (_, i) => i);
3694
- }
4040
+ /** computed, không phải method: method trả mảng MỚI mỗi lần render. */
4041
+ cardList = computed(() => range$4(this.cards()), ...(ngDevMode ? [{ debugName: "cardList" }] : /* istanbul ignore next */ []));
4042
+ rowList = computed(() => range$4(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
3695
4043
  rowWidth(i) {
3696
- return [90, 70, 80, 60][i % 4];
4044
+ return ROW_WIDTHS[i % ROW_WIDTHS.length];
3697
4045
  }
3698
4046
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3699
- 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 });
4047
+ 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 });
3700
4048
  }
3701
4049
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDetailComponent, decorators: [{
3702
4050
  type: Component,
3703
- 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" }]
4051
+ 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" }]
3704
4052
  }], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
4053
+ function range$4(n) {
4054
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
4055
+ }
4056
+
4057
+ /**
4058
+ * V2 sparkbar — inline mini bar-trend (risk trend on project cards, deltas in
4059
+ * stat grids). Pure DOM, token-coloured; no chart engine.
4060
+ *
4061
+ * <fx-ui-sparkbar [values]="[3, 5, 2, 8, 6]" />
4062
+ */
4063
+ class SparkbarComponent {
4064
+ values = input([], ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
4065
+ /** Bar colour; null renders the accent. */
4066
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4067
+ height = input(28, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4068
+ bars = computed(() => {
4069
+ const values = this.values();
4070
+ const max = Math.max(...values, 1);
4071
+ // 8% floor so zero values still show a base mark.
4072
+ return values.map((v) => Math.max((v / max) * 100, 8));
4073
+ }, ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
4074
+ barClass() {
4075
+ return this.tone() ? toneBg(this.tone()) : 'bg-accent';
4076
+ }
4077
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SparkbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4078
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SparkbarComponent, isStandalone: true, selector: "fx-ui-sparkbar", inputs: { values: { classPropertyName: "values", publicName: "values", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"flex items-end gap-[2px]\"\n [style.height.px]=\"height()\"\n role=\"img\"\n [attr.aria-label]=\"'Trend: ' + values().join(', ')\"\n>\n @for (bar of bars(); track $index) {\n <div\n class=\"flex-1 min-w-[3px] rounded-[1px] opacity-80\"\n [ngClass]=\"barClass()\"\n [style.height.%]=\"bar\"\n ></div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4079
+ }
4080
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SparkbarComponent, decorators: [{
4081
+ type: Component,
4082
+ args: [{ selector: 'fx-ui-sparkbar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div\n class=\"flex items-end gap-[2px]\"\n [style.height.px]=\"height()\"\n role=\"img\"\n [attr.aria-label]=\"'Trend: ' + values().join(', ')\"\n>\n @for (bar of bars(); track $index) {\n <div\n class=\"flex-1 min-w-[3px] rounded-[1px] opacity-80\"\n [ngClass]=\"barClass()\"\n [style.height.%]=\"bar\"\n ></div>\n }\n</div>\n" }]
4083
+ }], propDecorators: { values: [{ type: i0.Input, args: [{ isSignal: true, alias: "values", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
3705
4084
 
3706
4085
  /** KPI tile row: big number + tinted icon box + caption, in a responsive grid. */
3707
4086
  class StatCardsComponent {
3708
4087
  cards = input([], ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
4088
+ /**
4089
+ * 'tile' (default): tinted icon box + number + caption — the pre-V2 look.
4090
+ * 'figure': the V2 design's KPI column — caption on top, large figure-font
4091
+ * number, toned sub-caption underneath (no icon box).
4092
+ */
4093
+ variant = input('tile', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
3709
4094
  /** Grid columns; defaults to one column per card. */
3710
4095
  cols;
3711
4096
  get gridColumns() {
@@ -3719,10 +4104,17 @@ class StatCardsComponent {
3719
4104
  return card.tone ? toneText(card.tone) : 'text-text-primary';
3720
4105
  }
3721
4106
  boxClass(card) {
3722
- return card.tone ? toneBadge(card.tone) : 'bg-bg-hover text-text-secondary';
4107
+ // Ô icon vuông: chỉ lấy MÀU, không lấy hình pill của badge.
4108
+ return toneTint(card.tone ?? null);
4109
+ }
4110
+ deltaClass(card) {
4111
+ return card.delta?.tone ? toneText(card.delta.tone) : 'text-muted';
4112
+ }
4113
+ subClass(card) {
4114
+ return card.subTone ? toneText(card.subTone) : 'text-text-secondary';
3723
4115
  }
3724
4116
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3725
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatCardsComponent, isStandalone: true, selector: "fx-ui-stat-cards", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0\">\n <span class=\"txt-heading-04\" [ngClass]=\"numClass(card)\">{{ card.value }}</span>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4117
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatCardsComponent, isStandalone: true, selector: "fx-ui-stat-cards", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3726
4118
  }
3727
4119
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, decorators: [{
3728
4120
  type: Component,
@@ -3730,8 +4122,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3730
4122
  NgStyle,
3731
4123
  NgClass,
3732
4124
  HeroIconComponent,
3733
- ], template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0\">\n <span class=\"txt-heading-04\" [ngClass]=\"numClass(card)\">{{ card.value }}</span>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n </div>\n </div>\n }\n</div>\n" }]
3734
- }], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], cols: [{
4125
+ SparkbarComponent,
4126
+ ], template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n" }]
4127
+ }], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], cols: [{
3735
4128
  type: Input
3736
4129
  }] } });
3737
4130
 
@@ -3771,18 +4164,24 @@ class FilterChipsComponent {
3771
4164
  }
3772
4165
  chipClass(option) {
3773
4166
  const active = (this.value ?? '') === option.value;
4167
+ // Chip "All" (value rỗng) khi ĐANG CHỌN dùng **accent** (xanh), không phải
4168
+ // nền đen — khớp demo (ref 11: pill "All 40" là accent). Nền đen chỉ dành
4169
+ // cho badge severity Critical (anh Tú chốt 03/09).
3774
4170
  if (!option.value) {
3775
4171
  return active
3776
- ? 'bg-text-primary text-text-focus'
3777
- : 'bg-bg-hover text-text-secondary hover:bg-gradient-primary-start/10 hover:text-gradient-primary-start';
4172
+ ? 'bg-accent text-accent-contrast'
4173
+ : 'bg-bg-hover text-text-secondary hover:bg-accent-soft hover:text-accent-fg';
3778
4174
  }
3779
4175
  const tone = option.value in this.toneOverrides
3780
4176
  ? this.toneOverrides[option.value]
3781
4177
  : severityTone(option.value);
3782
4178
  if (!tone) {
3783
- return active ? 'bg-text-primary text-text-focus' : `${toneBadge(null)} hover:opacity-50`;
4179
+ return active ? 'bg-accent text-accent-contrast' : `${toneScale(null)} hover:opacity-50`;
3784
4180
  }
3785
- return active ? `${toneBg(tone)} text-text-focus` : `${toneBadge(tone)} hover:opacity-50`;
4181
+ // Chip tự hình dạng riêng → chỉ lấy MÀU. Dùng thang đo (`tone-scale-*`)
4182
+ // chứ KHÔNG dùng `toneTint`: Critical ở filter phải là đỏ sẫm, không phải
4183
+ // pill đen đặc (anh Tú báo 03/09).
4184
+ return active ? `${toneBg(tone)} text-text-focus` : `${toneScale(tone)} hover:opacity-50`;
3786
4185
  }
3787
4186
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterChipsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3788
4187
  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 });
@@ -3798,6 +4197,129 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3798
4197
  type: Input
3799
4198
  }] } });
3800
4199
 
4200
+ /**
4201
+ * V2 filter pills — the quick-filter row above ranked lists (design refs 02,
4202
+ * 03, 11): outlined capsules with a muted count, the active one filled with
4203
+ * the primary text colour.
4204
+ *
4205
+ * Distinct from `fx-ui-filter-chips`, which tints each chip by its severity
4206
+ * tone and carries no count. Presentational: the caller owns the filter state.
4207
+ *
4208
+ * <fx-ui-filter-pills [options]="pills" [value]="filters.status"
4209
+ * (valueChange)="filters.status = $event; refresh()" />
4210
+ */
4211
+ class FilterPillsComponent {
4212
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4213
+ /** Accepts undefined so screens can bind an optional filter-bag key. */
4214
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4215
+ valueChange = output();
4216
+ isActive(option) {
4217
+ return (this.value() ?? '') === option.value;
4218
+ }
4219
+ /**
4220
+ * Pill ĐANG CHỌN dùng **accent** (xanh), không phải nền đen — khớp demo:
4221
+ * design ref 11 vẽ pill "All 40" là pill accent. Nền đen `--og-text` chỉ
4222
+ * dành cho badge severity Critical (`.og-badge--solid`), không dùng cho
4223
+ * trạng thái chọn của filter (anh Tú chốt 03/09).
4224
+ */
4225
+ pillClass(option) {
4226
+ return this.isActive(option)
4227
+ ? 'bg-accent text-accent-contrast border-accent'
4228
+ : 'border-border-default text-text-secondary hover:border-border-strong';
4229
+ }
4230
+ countClass(option) {
4231
+ return this.isActive(option) ? 'opacity-70' : 'text-text-placeholder';
4232
+ }
4233
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4234
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FilterPillsComponent, isStandalone: true, selector: "fx-ui-filter-pills", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4235
+ }
4236
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, decorators: [{
4237
+ type: Component,
4238
+ args: [{ selector: 'fx-ui-filter-pills', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n" }]
4239
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
4240
+
4241
+ /**
4242
+ * V2 stage progress dots — the walkthrough position indicator on the attack
4243
+ * chain toolbar (design refs 04, 07): one dot per stage, the current one drawn
4244
+ * as a wider accent bar, visited ones solid, the rest faint.
4245
+ *
4246
+ * <fx-ui-stage-dots [count]="8" [current]="stageIndex()"
4247
+ * (dotClick)="goToStage($event)" />
4248
+ */
4249
+ class StageDotsComponent {
4250
+ count = input(0, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
4251
+ /** 0-based current stage; -1 (default) means none selected yet. */
4252
+ current = input(-1, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
4253
+ /** Emit the clicked stage index; wire it to make the dots navigable. */
4254
+ dotClick = output();
4255
+ dots = computed(() => Array.from({ length: Math.max(0, this.count()) }, (_, i) => i), ...(ngDevMode ? [{ debugName: "dots" }] : /* istanbul ignore next */ []));
4256
+ dotClass(index) {
4257
+ const current = this.current();
4258
+ if (index === current)
4259
+ return 'w-5 bg-accent';
4260
+ if (current >= 0 && index < current)
4261
+ return 'w-1.5 bg-text-placeholder';
4262
+ return 'w-1.5 bg-border-strong';
4263
+ }
4264
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StageDotsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4265
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StageDotsComponent, isStandalone: true, selector: "fx-ui-stage-dots", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dotClick: "dotClick" }, ngImport: i0, template: "<div class=\"flex items-center gap-xs\" role=\"tablist\">\n @for (i of dots(); track i) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === current()\"\n [attr.aria-label]=\"'Stage ' + (i + 1)\"\n class=\"h-1.5 rounded-full cursor-pointer transition-all duration-150 hover:opacity-70\"\n [ngClass]=\"dotClass(i)\"\n (click)=\"dotClick.emit(i)\"\n ></button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4266
+ }
4267
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StageDotsComponent, decorators: [{
4268
+ type: Component,
4269
+ args: [{ selector: 'fx-ui-stage-dots', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex items-center gap-xs\" role=\"tablist\">\n @for (i of dots(); track i) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === current()\"\n [attr.aria-label]=\"'Stage ' + (i + 1)\"\n class=\"h-1.5 rounded-full cursor-pointer transition-all duration-150 hover:opacity-70\"\n [ngClass]=\"dotClass(i)\"\n (click)=\"dotClick.emit(i)\"\n ></button>\n }\n</div>\n" }]
4270
+ }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }], dotClick: [{ type: i0.Output, args: ["dotClick"] }] } });
4271
+
4272
+ /**
4273
+ * V2 evidence code block — the mono panel that carries a process tree, a
4274
+ * command line or any captured text (design ref 05, "Process tree").
4275
+ *
4276
+ * Whitespace is preserved and the box scrolls on its own rather than pushing
4277
+ * the page wide, so ASCII trees survive narrow viewports.
4278
+ *
4279
+ * <fx-ui-code-block [label]="'analyze.processTree' | translate" [code]="tree" />
4280
+ */
4281
+ class CodeBlockComponent {
4282
+ code = input('', ...(ngDevMode ? [{ debugName: "code" }] : /* istanbul ignore next */ []));
4283
+ /** Caller-translated caption above the box; omit for a bare block. */
4284
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4285
+ /** Scroll cap; empty grows with the content. */
4286
+ maxHeight = input('', ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
4287
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CodeBlockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4288
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CodeBlockComponent, isStandalone: true, selector: "fx-ui-code-block", inputs: { code: { classPropertyName: "code", publicName: "code", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<pre\n class=\"m-0 p-large rounded-control border border-border bg-surface overflow-auto txt-body-compact-01 text-strong\"\n [style.max-height]=\"maxHeight() || null\"\n style=\"font-family: var(--og-font-mono); white-space: pre; tab-size: 2\"\n>{{ code() }}</pre>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
4289
+ }
4290
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CodeBlockComponent, decorators: [{
4291
+ type: Component,
4292
+ args: [{ selector: 'fx-ui-code-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<pre\n class=\"m-0 p-large rounded-control border border-border bg-surface overflow-auto txt-body-compact-01 text-strong\"\n [style.max-height]=\"maxHeight() || null\"\n style=\"font-family: var(--og-font-mono); white-space: pre; tab-size: 2\"\n>{{ code() }}</pre>\n" }]
4293
+ }], propDecorators: { code: [{ type: i0.Input, args: [{ isSignal: true, alias: "code", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }] } });
4294
+
4295
+ /**
4296
+ * V2 captured-evidence frame — a faux window chrome (traffic lights + mono file
4297
+ * name + timestamp) around a screenshot, with a placeholder when the image is
4298
+ * missing (design ref 05, "Screenshot").
4299
+ *
4300
+ * <fx-ui-screenshot-frame [label]="'analyze.screenshot' | translate"
4301
+ * fileName="chain-05.png" timestamp="09:42:18"
4302
+ * [src]="url" [caption]="caption" />
4303
+ */
4304
+ class ScreenshotFrameComponent {
4305
+ /** Caller-translated caption above the frame; omit for a bare frame. */
4306
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4307
+ fileName = input('', ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
4308
+ timestamp = input('', ...(ngDevMode ? [{ debugName: "timestamp" }] : /* istanbul ignore next */ []));
4309
+ /** Image URL; empty renders the placeholder body. */
4310
+ src = input('', ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
4311
+ /** Caller-translated line under (or inside) the frame. */
4312
+ caption = input('', ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
4313
+ alt = input('', ...(ngDevMode ? [{ debugName: "alt" }] : /* istanbul ignore next */ []));
4314
+ height = input('180px', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4315
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScreenshotFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4316
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ScreenshotFrameComponent, isStandalone: true, selector: "fx-ui-screenshot-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fileName: { classPropertyName: "fileName", publicName: "fileName", isSignal: true, isRequired: false, transformFunction: null }, timestamp: { classPropertyName: "timestamp", publicName: "timestamp", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<div class=\"rounded-control border border-border bg-surface overflow-hidden\">\n <!-- Window chrome: traffic lights + mono file name + capture time -->\n <div class=\"flex items-center gap-small px-normal py-small border-b border-border bg-surface-sunken\">\n <span class=\"flex items-center gap-xs shrink-0\">\n <span class=\"w-2 h-2 rounded-full bg-critical/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-warning/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-success/70\"></span>\n </span>\n @if (fileName()) {\n <span class=\"txt-utility-caption text-text-secondary truncate\" style=\"font-family: var(--og-font-mono)\">\n {{ fileName() }}\n </span>\n }\n @if (timestamp()) {\n <span class=\"txt-utility-caption text-text-placeholder shrink-0\" style=\"font-family: var(--og-font-mono)\">\n \u00B7 {{ timestamp() }}\n </span>\n }\n </div>\n\n @if (src()) {\n <img [src]=\"src()\" [alt]=\"alt() || fileName()\" class=\"block w-full object-cover\" [style.height]=\"height()\" />\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-normal px-large text-center\"\n [style.height]=\"height()\"\n >\n <fx-ui-hero-icon icon=\"camera\" [size]=\"22\" class=\"flex text-text-placeholder\"></fx-ui-hero-icon>\n @if (caption()) {\n <span class=\"txt-utility-caption text-text-placeholder max-w-[280px]\">{{ caption() }}</span>\n }\n </div>\n }\n</div>\n@if (src() && caption()) {\n <div class=\"txt-utility-caption text-text-placeholder mt-normal\">{{ caption() }}</div>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4317
+ }
4318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScreenshotFrameComponent, decorators: [{
4319
+ type: Component,
4320
+ args: [{ selector: 'fx-ui-screenshot-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<div class=\"rounded-control border border-border bg-surface overflow-hidden\">\n <!-- Window chrome: traffic lights + mono file name + capture time -->\n <div class=\"flex items-center gap-small px-normal py-small border-b border-border bg-surface-sunken\">\n <span class=\"flex items-center gap-xs shrink-0\">\n <span class=\"w-2 h-2 rounded-full bg-critical/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-warning/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-success/70\"></span>\n </span>\n @if (fileName()) {\n <span class=\"txt-utility-caption text-text-secondary truncate\" style=\"font-family: var(--og-font-mono)\">\n {{ fileName() }}\n </span>\n }\n @if (timestamp()) {\n <span class=\"txt-utility-caption text-text-placeholder shrink-0\" style=\"font-family: var(--og-font-mono)\">\n \u00B7 {{ timestamp() }}\n </span>\n }\n </div>\n\n @if (src()) {\n <img [src]=\"src()\" [alt]=\"alt() || fileName()\" class=\"block w-full object-cover\" [style.height]=\"height()\" />\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-normal px-large text-center\"\n [style.height]=\"height()\"\n >\n <fx-ui-hero-icon icon=\"camera\" [size]=\"22\" class=\"flex text-text-placeholder\"></fx-ui-hero-icon>\n @if (caption()) {\n <span class=\"txt-utility-caption text-text-placeholder max-w-[280px]\">{{ caption() }}</span>\n }\n </div>\n }\n</div>\n@if (src() && caption()) {\n <div class=\"txt-utility-caption text-text-placeholder mt-normal\">{{ caption() }}</div>\n}\n" }]
4321
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileName", required: false }] }], timestamp: [{ type: i0.Input, args: [{ isSignal: true, alias: "timestamp", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4322
+
3801
4323
  /** Thin tone-coloured progress/score bar with optional label and caption. */
3802
4324
  class MeterComponent {
3803
4325
  value = 0;
@@ -3830,26 +4352,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3830
4352
  type: Input
3831
4353
  }] } });
3832
4354
 
3833
- /** Detail-panel section: card with an icon + heading header and projected body. */
4355
+ /**
4356
+ * Panel section: card with a header and projected body.
4357
+ * - titleStyle 'heading' (default): icon + heading, the pre-V2 look
4358
+ * - titleStyle 'eyebrow': the V2 design's panel header — small tracked
4359
+ * caption on the left, muted [aside] text on the right
4360
+ */
3834
4361
  class SectionCardComponent {
3835
4362
  /** Caller-translated heading. */
3836
4363
  title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3837
4364
  /** hero-icon name; omitted → text-only header. */
3838
4365
  icon;
3839
4366
  iconTone = input(null, ...(ngDevMode ? [{ debugName: "iconTone" }] : /* istanbul ignore next */ []));
4367
+ titleStyle = input('heading', ...(ngDevMode ? [{ debugName: "titleStyle" }] : /* istanbul ignore next */ []));
4368
+ /** Caller-translated right-aligned hint (eyebrow style). */
4369
+ aside = input('', ...(ngDevMode ? [{ debugName: "aside" }] : /* istanbul ignore next */ []));
3840
4370
  get iconClass() {
3841
4371
  const iconTone = this.iconTone();
3842
4372
  return iconTone ? toneText(iconTone) : 'text-text-secondary';
3843
4373
  }
3844
4374
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3845
- 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 } }, ngImport: i0, template: "<div class=\"card-common\">\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 <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 });
4375
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3846
4376
  }
3847
4377
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, decorators: [{
3848
4378
  type: Component,
3849
- args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\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 <ng-content></ng-content>\n</div>\n" }]
4379
+ args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
3850
4380
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{
3851
4381
  type: Input
3852
- }], iconTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTone", required: false }] }] } });
4382
+ }], 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 }] }] } });
3853
4383
 
3854
4384
  /**
3855
4385
  * Label-over-value field for detail panels. The label uses txt-field-label
@@ -3938,11 +4468,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3938
4468
 
3939
4469
  let nextUid = 0;
3940
4470
  /**
3941
- * Two-column master-detail shell driven by a BaseMasterDetailComponent.
3942
- * The host page projects its own header/filters and supplies templates for
3943
- * the parts that vary per entity:
4471
+ * Master-detail shell driven by a BaseMasterDetailComponent. Two-column by
4472
+ * default; projecting a [mdFacets] block (V2 lookup screens) adds a third
4473
+ * facets-rail column before the list. The host page projects its own
4474
+ * header/filters and supplies templates for the parts that vary per entity:
3944
4475
  *
3945
4476
  * <fx-ui-master-detail [table]="this" [emptyMessage]="'noData' | translate">
4477
+ * <div mdFacets>…<fx-ui-facets-rail …/>…</div> <!-- optional, V2 -->
3946
4478
  * <div mdHeader>…title / stat cards…</div>
3947
4479
  * <div mdFilters>…search bar / filter chips…</div>
3948
4480
  * <ng-template #row let-item let-selected="selected">…row body…</ng-template>
@@ -3953,19 +4485,38 @@ let nextUid = 0;
3953
4485
  * tab stop, ArrowUp/Down/Home/End move the selection.
3954
4486
  */
3955
4487
  class MasterDetailComponent {
4488
+ hostEl = inject((ElementRef));
4489
+ /** True when the host projected a [mdFacets] block — switches to 3 columns. */
4490
+ hasFacets = false;
3956
4491
  table;
3957
4492
  /** Fixed list-pane width on lg+ viewports (stacked full-width below). */
3958
4493
  listWidth = input('420px', ...(ngDevMode ? [{ debugName: "listWidth" }] : /* istanbul ignore next */ []));
3959
- maxListHeight = input('640px', ...(ngDevMode ? [{ debugName: "maxListHeight" }] : /* istanbul ignore next */ []));
4494
+ /**
4495
+ * Chiều cao CHUNG của cả ba cụm (facets / list / detail). Mặc định trừ phần
4496
+ * chrome phía trên (topbar + tab strip + context line) khỏi viewport để
4497
+ * bảng ba cột vừa đúng một màn, không kéo trang cuộn theo.
4498
+ * Chỉ có tác dụng từ 1080px — xem `.fx-md-row` trong CSS.
4499
+ */
4500
+ paneHeight = input('calc(100vh - 300px)', ...(ngDevMode ? [{ debugName: "paneHeight" }] : /* istanbul ignore next */ []));
3960
4501
  pageSizeOptions = input([5, 10, 20, 50], ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : /* istanbul ignore next */ []));
3961
4502
  /** Labels come in via inputs — the lib carries no i18n. */
3962
4503
  emptyMessage = input('No data', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
3963
4504
  listLabel = input('', ...(ngDevMode ? [{ debugName: "listLabel" }] : /* istanbul ignore next */ []));
3964
4505
  skeletonCards = input(3, ...(ngDevMode ? [{ debugName: "skeletonCards" }] : /* istanbul ignore next */ []));
4506
+ /** 'prevnext' renders the V2 "X of Y" + Prev/Next controls instead of the
4507
+ Material paginator (which stays in the DOM, hidden — it is the paging
4508
+ engine BaseTable subscribes to). */
4509
+ paginationStyle = input('paginator', ...(ngDevMode ? [{ debugName: "paginationStyle" }] : /* istanbul ignore next */ []));
4510
+ /** Caller-translated Prev/Next labels ("X of Y" is numeric). */
4511
+ prevLabel = input('Prev', ...(ngDevMode ? [{ debugName: "prevLabel" }] : /* istanbul ignore next */ []));
4512
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
3965
4513
  rowTemplate = contentChild.required('row');
3966
4514
  detailTemplate = contentChild.required('detail');
3967
4515
  paginator = viewChild.required(MatPaginator);
3968
4516
  uid = `fx-md-${nextUid++}`;
4517
+ ngAfterContentInit() {
4518
+ this.hasFacets = !!this.hostEl.nativeElement.querySelector('[mdFacets]');
4519
+ }
3969
4520
  ngAfterViewInit() {
3970
4521
  // The paginator lives in this view, not the host page's, so the host's
3971
4522
  // @ViewChild(MatPaginator) never finds it. Hand it to the table so
@@ -3974,6 +4525,17 @@ class MasterDetailComponent {
3974
4525
  this.table.paginator = this.paginator();
3975
4526
  }
3976
4527
  }
4528
+ /** "X of Y" range text for the prevnext pagination style. */
4529
+ rangeText() {
4530
+ const total = this.table.total ?? 0;
4531
+ if (!total)
4532
+ return '0 of 0';
4533
+ const page = this.paginator().pageIndex;
4534
+ const size = this.table.pageSize;
4535
+ const from = page * size + 1;
4536
+ const to = Math.min((page + 1) * size, total);
4537
+ return `${from}–${to} of ${total}`;
4538
+ }
3977
4539
  isSelected(row) {
3978
4540
  return this.table.idOf(row) === this.table.selectedId;
3979
4541
  }
@@ -4012,7 +4574,7 @@ class MasterDetailComponent {
4012
4574
  el?.scrollIntoView({ block: 'nearest' });
4013
4575
  }
4014
4576
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4015
- 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 } }, 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 <!-- 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-gradient-primary-start bg-gradient-primary-start/5'\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 [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\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}}\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"] }] });
4577
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }] });
4016
4578
  }
4017
4579
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
4018
4580
  type: Component,
@@ -4023,10 +4585,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4023
4585
  EmptyStateComponent,
4024
4586
  MatPaginator,
4025
4587
  SkeletonDetailComponent,
4026
- ], template: "<div class=\"flex flex-col lg:flex-row items-start gap-xl\" [style.--fx-md-list-width]=\"listWidth()\">\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-gradient-primary-start bg-gradient-primary-start/5'\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 [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\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}}\n"] }]
4588
+ ], template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"] }]
4027
4589
  }], propDecorators: { table: [{
4028
4590
  type: Input
4029
- }], 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 }] }], 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 }] }] } });
4591
+ }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4030
4592
 
4031
4593
  /**
4032
4594
  * Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook
@@ -4047,13 +4609,13 @@ class MenuComponent {
4047
4609
  switch (item.tone) {
4048
4610
  case 'critical':
4049
4611
  case 'danger':
4050
- return 'text-danger';
4612
+ return 'tone-fg-danger';
4051
4613
  case 'warning':
4052
- return 'text-warning';
4614
+ return 'tone-fg-warning';
4053
4615
  case 'success':
4054
- return 'text-success';
4616
+ return 'tone-fg-success';
4055
4617
  case 'information':
4056
- return 'text-information';
4618
+ return 'tone-fg-information';
4057
4619
  default:
4058
4620
  return 'text-text-primary';
4059
4621
  }
@@ -4066,6 +4628,601 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4066
4628
  args: [{ selector: 'fx-ui-menu', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatMenuModule, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-lg p-1 hover:bg-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n [matMenuTriggerFor]=\"menu\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <ng-content></ng-content>\n</button>\n\n<mat-menu #menu=\"matMenu\">\n @for (item of items(); track item.label) {\n <button\n mat-menu-item\n type=\"button\"\n [disabled]=\"item.disabled\"\n (click)=\"select.emit(item)\"\n >\n <span class=\"flex items-center gap-normal\" [class]=\"toneClass(item)\">\n @if (item.icon) {\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span>{{ item.label }}</span>\n </span>\n </button>\n }\n</mat-menu>\n" }]
4067
4629
  }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
4068
4630
 
4631
+ /**
4632
+ * V2 theme picker — dropdown các theme OgeniumDashboards cho chọn
4633
+ * (OG_PICKER_THEME_META: porcelain/charcoal), mỗi dòng có swatch accent +
4634
+ * page. Drives ThemeService.setTheme(); replaces the pre-V2 sun/moon toggle
4635
+ * (behavior-layer rule: mat-menu stays inside the lib, apps never import
4636
+ * @angular/material directly).
4637
+ *
4638
+ * <fx-ui-theme-picker />
4639
+ */
4640
+ class ThemePickerComponent {
4641
+ themeService = inject(ThemeService);
4642
+ themes = OG_PICKER_THEME_META;
4643
+ current = toSignal(this.themeService.themeChanges(), {
4644
+ initialValue: this.themeService.getCurrentTheme(),
4645
+ });
4646
+ /** Tra trên bộ ĐẦY ĐỦ: theme đang active có thể là một trong 4 theme đã ẩn
4647
+ khỏi picker (deep-link /themes, localStorage cũ) — nhãn vẫn phải đúng. */
4648
+ currentMeta() {
4649
+ return OG_THEME_META.find((t) => t.name === this.current()) ?? this.themes[0];
4650
+ }
4651
+ select(theme) {
4652
+ this.themeService.setTheme(theme);
4653
+ }
4654
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4655
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ThemePickerComponent, isStandalone: true, selector: "fx-ui-theme-picker", ngImport: i0, template: "<button\n type=\"button\"\n [matMenuTriggerFor]=\"themeMenu\"\n class=\"flex items-center gap-small px-normal py-small rounded-control border border-border-default bg-transparent cursor-pointer hover:bg-bg-hover transition-colors\"\n aria-label=\"Change theme\"\n>\n <span\n class=\"w-4 h-4 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' +\n currentMeta().accent +\n ' 50%, ' +\n currentMeta().pageBg +\n ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 hidden md:inline\">{{ currentMeta().label }}</span>\n <fx-ui-hero-icon icon=\"chevron-down\" [size]=\"14\" class=\"flex text-text-secondary\" />\n</button>\n\n<mat-menu #themeMenu=\"matMenu\">\n @for (theme of themes; track theme.name) {\n <button\n mat-menu-item\n type=\"button\"\n (click)=\"select(theme.name)\"\n [attr.aria-current]=\"theme.name === current() ? 'true' : null\"\n >\n <div class=\"flex items-center gap-normal min-w-40\">\n <span\n class=\"w-5 h-5 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' + theme.accent + ' 50%, ' + theme.pageBg + ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 flex-1\">{{ theme.label }}</span>\n <span class=\"txt-utility-description\">{{ theme.dark ? 'Dark' : 'Light' }}</span>\n @if (theme.name === current()) {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex text-text-selected\" />\n }\n </div>\n </button>\n }\n</mat-menu>\n", dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4656
+ }
4657
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemePickerComponent, decorators: [{
4658
+ type: Component,
4659
+ args: [{ selector: 'fx-ui-theme-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatMenuModule, HeroIconComponent], template: "<button\n type=\"button\"\n [matMenuTriggerFor]=\"themeMenu\"\n class=\"flex items-center gap-small px-normal py-small rounded-control border border-border-default bg-transparent cursor-pointer hover:bg-bg-hover transition-colors\"\n aria-label=\"Change theme\"\n>\n <span\n class=\"w-4 h-4 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' +\n currentMeta().accent +\n ' 50%, ' +\n currentMeta().pageBg +\n ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 hidden md:inline\">{{ currentMeta().label }}</span>\n <fx-ui-hero-icon icon=\"chevron-down\" [size]=\"14\" class=\"flex text-text-secondary\" />\n</button>\n\n<mat-menu #themeMenu=\"matMenu\">\n @for (theme of themes; track theme.name) {\n <button\n mat-menu-item\n type=\"button\"\n (click)=\"select(theme.name)\"\n [attr.aria-current]=\"theme.name === current() ? 'true' : null\"\n >\n <div class=\"flex items-center gap-normal min-w-40\">\n <span\n class=\"w-5 h-5 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' + theme.accent + ' 50%, ' + theme.pageBg + ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 flex-1\">{{ theme.label }}</span>\n <span class=\"txt-utility-description\">{{ theme.dark ? 'Dark' : 'Light' }}</span>\n @if (theme.name === current()) {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex text-text-selected\" />\n }\n </div>\n </button>\n }\n</mat-menu>\n" }]
4660
+ }] });
4661
+
4662
+ /**
4663
+ * "Waiting on the backend" marker (anh Tú 29/08).
4664
+ *
4665
+ * Put this on any UI the V2 design calls for but no endpoint feeds yet, so the
4666
+ * gap is visible in the product instead of living only in a backlog file.
4667
+ * `grep -r "fx-ui-pending-badge"` then lists every outstanding field.
4668
+ *
4669
+ * <fx-ui-pending-badge [field]="'confirmed-chains weekly series'" />
4670
+ *
4671
+ * Keep the copy factual: it states that data is missing, never fakes a value.
4672
+ */
4673
+ class PendingBadgeComponent {
4674
+ /** Caller-translated chip text. */
4675
+ label = input('Pending API', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4676
+ /**
4677
+ * What exactly is missing — shown as the title tooltip so a reviewer can read
4678
+ * the required field without opening the code.
4679
+ */
4680
+ field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4681
+ /** 'chip' (default) sits inline; 'dot' is a bare marker for dense rows. */
4682
+ variant = input('chip', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4683
+ get tooltip() {
4684
+ return this.field() ? `${this.label()} — ${this.field()}` : this.label();
4685
+ }
4686
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4687
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4688
+ }
4689
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, decorators: [{
4690
+ type: Component,
4691
+ args: [{ selector: 'fx-ui-pending-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n" }]
4692
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4693
+
4694
+ /**
4695
+ * Wraps UI that is rendered from PLACEHOLDER data so the layout can be
4696
+ * reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
4697
+ * trước để review").
4698
+ *
4699
+ * The frame makes the status unmistakable — dashed outline, a corner label and
4700
+ * the pending marker naming the missing field — so sample shapes can never be
4701
+ * mistaken for measured values. Drop the wrapper (and the sample input) the
4702
+ * moment the API lands.
4703
+ *
4704
+ * The pending chip is inlined rather than composed from fx-ui-pending-badge:
4705
+ * one lib component importing another inside the same barrel tripped NG0919
4706
+ * (metadata read before definition) in consuming apps.
4707
+ *
4708
+ * <fx-ui-sample-frame [field]="'weekly activity'" [label]="'Sample layout'">
4709
+ * …chart rendered from sample series…
4710
+ * </fx-ui-sample-frame>
4711
+ */
4712
+ class SampleFrameComponent {
4713
+ /** Caller-translated corner label. */
4714
+ label = input('Sample layout', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4715
+ /** Field the backend still owes — shown on the pending badge tooltip. */
4716
+ field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4717
+ /** Caller-translated pending-chip text. */
4718
+ pendingLabel = input('Pending API', ...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
4719
+ get tooltip() {
4720
+ return this.field() ? `${this.pendingLabel()} — ${this.field()}` : this.pendingLabel();
4721
+ }
4722
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4723
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4724
+ }
4725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, decorators: [{
4726
+ type: Component,
4727
+ args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
4728
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], pendingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingLabel", required: false }] }] } });
4729
+
4730
+ /**
4731
+ * V2 score badge — the risk/score chip used on attack-path rows, dashboard
4732
+ * panels and project tiles. Colour comes from the 0–100 risk band unless an
4733
+ * explicit tone is given (bands: ≥90 critical, ≥70 danger, ≥40 warning, else
4734
+ * success — same thresholds as value-tone's scoreTone on a 0–100 scale).
4735
+ */
4736
+ class ScoreBadgeComponent {
4737
+ score = input.required(...(ngDevMode ? [{ debugName: "score" }] : /* istanbul ignore next */ []));
4738
+ /** Denominator shown as "98 / 100"; omit for a bare score. */
4739
+ outOf = input(null, ...(ngDevMode ? [{ debugName: "outOf" }] : /* istanbul ignore next */ []));
4740
+ /** Overrides the band-derived tone. */
4741
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4742
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4743
+ resolvedTone = computed(() => {
4744
+ const explicit = this.tone();
4745
+ if (explicit)
4746
+ return explicit;
4747
+ const value = Number(this.score());
4748
+ if (!isFinite(value))
4749
+ return null;
4750
+ if (value >= 90)
4751
+ return 'critical';
4752
+ if (value >= 70)
4753
+ return 'danger';
4754
+ if (value >= 40)
4755
+ return 'warning';
4756
+ return 'success';
4757
+ }, ...(ngDevMode ? [{ debugName: "resolvedTone" }] : /* istanbul ignore next */ []));
4758
+ badgeClass() {
4759
+ return toneBadge(this.resolvedTone());
4760
+ }
4761
+ sizeClass() {
4762
+ switch (this.size()) {
4763
+ case 'sm':
4764
+ return 'text-xs px-normal py-[1px]';
4765
+ case 'lg':
4766
+ return 'text-2xl px-semi py-small font-semibold';
4767
+ default:
4768
+ return 'text-sm px-semi py-xs font-semibold';
4769
+ }
4770
+ }
4771
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScoreBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4772
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ScoreBadgeComponent, isStandalone: true, selector: "fx-ui-score-badge", inputs: { score: { classPropertyName: "score", publicName: "score", isSignal: true, isRequired: true, transformFunction: null }, outOf: { classPropertyName: "outOf", publicName: "outOf", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<span\n class=\"inline-flex items-center justify-center rounded-control whitespace-nowrap\"\n [ngClass]=\"[badgeClass(), sizeClass()]\"\n style=\"font-family: var(--og-font-figure)\"\n>\n {{ score() }}@if (outOf() !== null) {<span class=\"opacity-60\">&nbsp;/&nbsp;{{ outOf() }}</span>}\n</span>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4773
+ }
4774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScoreBadgeComponent, decorators: [{
4775
+ type: Component,
4776
+ args: [{ selector: 'fx-ui-score-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<span\n class=\"inline-flex items-center justify-center rounded-control whitespace-nowrap\"\n [ngClass]=\"[badgeClass(), sizeClass()]\"\n style=\"font-family: var(--og-font-figure)\"\n>\n {{ score() }}@if (outOf() !== null) {<span class=\"opacity-60\">&nbsp;/&nbsp;{{ outOf() }}</span>}\n</span>\n" }]
4777
+ }], propDecorators: { score: [{ type: i0.Input, args: [{ isSignal: true, alias: "score", required: true }] }], outOf: [{ type: i0.Input, args: [{ isSignal: true, alias: "outOf", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
4778
+
4779
+ /**
4780
+ * V2 StatusDot — tiny tone-coloured indicator used in list rows, project
4781
+ * cards and stat grids. Optional pulse for "live" states (running scans).
4782
+ *
4783
+ * <fx-ui-status-dot tone="success" />
4784
+ * <fx-ui-status-dot tone="warning" [pulse]="true" />
4785
+ */
4786
+ class StatusDotComponent {
4787
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4788
+ /** Diameter in px. */
4789
+ size = input(8, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4790
+ pulse = input(false, ...(ngDevMode ? [{ debugName: "pulse" }] : /* istanbul ignore next */ []));
4791
+ dotClass() {
4792
+ return toneBg(this.tone());
4793
+ }
4794
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatusDotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4795
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatusDotComponent, isStandalone: true, selector: "fx-ui-status-dot", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, pulse: { classPropertyName: "pulse", publicName: "pulse", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<span\n class=\"relative inline-flex shrink-0\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n>\n @if (pulse()) {\n <span\n class=\"absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping motion-reduce:animate-none\"\n [ngClass]=\"dotClass()\"\n ></span>\n }\n <span class=\"relative inline-flex h-full w-full rounded-full\" [ngClass]=\"dotClass()\"></span>\n</span>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4796
+ }
4797
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatusDotComponent, decorators: [{
4798
+ type: Component,
4799
+ args: [{ selector: 'fx-ui-status-dot', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<span\n class=\"relative inline-flex shrink-0\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n>\n @if (pulse()) {\n <span\n class=\"absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping motion-reduce:animate-none\"\n [ngClass]=\"dotClass()\"\n ></span>\n }\n <span class=\"relative inline-flex h-full w-full rounded-full\" [ngClass]=\"dotClass()\"></span>\n</span>\n" }]
4800
+ }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], pulse: [{ type: i0.Input, args: [{ isSignal: true, alias: "pulse", required: false }] }] } });
4801
+
4802
+ /**
4803
+ * V2 segmented control — mode toggles (Essentials/Advanced), Catalog/Table
4804
+ * view switches, sub-navigation inside a tab. Generic n-option control; the
4805
+ * semantics live with the caller.
4806
+ *
4807
+ * Rendered as a compact pill group (anh Tú 29/08): equal-width options inside
4808
+ * a rounded-full track, with a single "thumb" that SLIDES to the active
4809
+ * option (transform transition — no per-button background repaint).
4810
+ *
4811
+ * <fx-ui-segmented [options]="modes" [(value)]="mode" />
4812
+ */
4813
+ class SegmentedComponent {
4814
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4815
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4816
+ /** Accessible name for the group. */
4817
+ ariaLabel = input('View mode', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
4818
+ /**
4819
+ * Both variants share the pill shape; they differ in the thumb fill:
4820
+ * 'default': raised surface thumb — view switches (Catalog/Table, ref 10).
4821
+ * 'pill': accent-filled thumb — the Essential/Advanced depth toggle
4822
+ * (design refs 04/05).
4823
+ */
4824
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4825
+ /** Index the sliding thumb sits on; -1 hides it (no/unknown selection). */
4826
+ activeIndex = computed(() => this.options().findIndex((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4827
+ /** Equal-width tracks → the thumb is 1/n of the inner width and slides by
4828
+ * multiples of its own width. Pure CSS transform, nothing to measure. */
4829
+ thumbWidth = computed(() => `calc((100% - 6px) / ${this.options().length || 1})`, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4830
+ thumbTransform = computed(() => `translateX(${this.activeIndex() * 100}%)`, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
4831
+ select(option) {
4832
+ if (option.disabled || option.value === this.value())
4833
+ return;
4834
+ this.value.set(option.value);
4835
+ }
4836
+ thumbClass() {
4837
+ return this.variant() === 'pill'
4838
+ ? 'bg-accent'
4839
+ : 'bg-surface-raised border border-border shadow-sm';
4840
+ }
4841
+ optionClass(option) {
4842
+ if (option.value !== this.value()) {
4843
+ return 'text-muted hover:text-strong';
4844
+ }
4845
+ return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
4846
+ }
4847
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4848
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4849
+ }
4850
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
4851
+ type: Component,
4852
+ args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
4853
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4854
+
4855
+ /**
4856
+ * V2 ListRow — presentational body for master-detail #row templates and other
4857
+ * selectable lists: status dot + title, trailing badges, meta line. Extra
4858
+ * content projects below the meta line.
4859
+ *
4860
+ * <ng-template #row let-item>
4861
+ * <fx-ui-list-row
4862
+ * [title]="item.name"
4863
+ * [meta]="item.version + ' · ' + item.date"
4864
+ * [dotTone]="statusTone(item)"
4865
+ * [badges]="[{ label: item.severity, tone: severityTone(item.severity) }]"
4866
+ * />
4867
+ * </ng-template>
4868
+ */
4869
+ class ListRowComponent {
4870
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
4871
+ meta = input('', ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
4872
+ badges = input([], ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
4873
+ dotTone = input(undefined, ...(ngDevMode ? [{ debugName: "dotTone" }] : /* istanbul ignore next */ []));
4874
+ dotPulse = input(false, ...(ngDevMode ? [{ debugName: "dotPulse" }] : /* istanbul ignore next */ []));
4875
+ badgeClass(badge) {
4876
+ return toneBadge(badge.tone ?? null);
4877
+ }
4878
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ListRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4879
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ListRowComponent, isStandalone: true, selector: "fx-ui-list-row", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, badges: { classPropertyName: "badges", publicName: "badges", isSignal: true, isRequired: false, transformFunction: null }, dotTone: { classPropertyName: "dotTone", publicName: "dotTone", isSignal: true, isRequired: false, transformFunction: null }, dotPulse: { classPropertyName: "dotPulse", publicName: "dotPulse", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex items-center gap-normal min-w-0\">\n @if (dotTone() !== undefined) {\n <fx-ui-status-dot [tone]=\"dotTone() ?? null\" [pulse]=\"dotPulse()\" />\n }\n <span class=\"txt-body-medium-01 truncate flex-1\">{{ title() }}</span>\n @for (badge of badges(); track badge.label) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"badgeClass(badge)\"\n >\n {{ badge.label }}\n </span>\n }\n</div>\n@if (meta()) {\n <div class=\"txt-utility-description mt-xs truncate\" [class.ml-large]=\"dotTone() !== undefined\">\n {{ meta() }}\n </div>\n}\n<ng-content></ng-content>\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 });
4880
+ }
4881
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ListRowComponent, decorators: [{
4882
+ type: Component,
4883
+ args: [{ selector: 'fx-ui-list-row', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, StatusDotComponent], template: "<div class=\"flex items-center gap-normal min-w-0\">\n @if (dotTone() !== undefined) {\n <fx-ui-status-dot [tone]=\"dotTone() ?? null\" [pulse]=\"dotPulse()\" />\n }\n <span class=\"txt-body-medium-01 truncate flex-1\">{{ title() }}</span>\n @for (badge of badges(); track badge.label) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"badgeClass(badge)\"\n >\n {{ badge.label }}\n </span>\n }\n</div>\n@if (meta()) {\n <div class=\"txt-utility-description mt-xs truncate\" [class.ml-large]=\"dotTone() !== undefined\">\n {{ meta() }}\n </div>\n}\n<ng-content></ng-content>\n" }]
4884
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], badges: [{ type: i0.Input, args: [{ isSignal: true, alias: "badges", required: false }] }], dotTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotTone", required: false }] }], dotPulse: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotPulse", required: false }] }] } });
4885
+
4886
+ /**
4887
+ * V2 BarList — ranked horizontal bars ("Top projects by risk", "Top critical
4888
+ * attack paths"). Pure DOM (accessible, token-native, clickable) — charts via
4889
+ * ECharts are for time series, not rankings.
4890
+ *
4891
+ * <fx-ui-bar-list [items]="topRisk" (select)="open($event)" />
4892
+ */
4893
+ class BarListComponent {
4894
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
4895
+ /** Rows render as buttons only when true (a (select) listener exists). */
4896
+ interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
4897
+ select = output();
4898
+ max = computed(() => Math.max(...this.items().map((i) => i.value), 1), ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
4899
+ widthOf(item) {
4900
+ return Math.max((item.value / this.max()) * 100, 2);
4901
+ }
4902
+ barClass(item) {
4903
+ return item.tone ? toneBg(item.tone) : 'bg-accent';
4904
+ }
4905
+ onSelect(item) {
4906
+ if (this.interactive())
4907
+ this.select.emit(item);
4908
+ }
4909
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BarListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4910
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: BarListComponent, isStandalone: true, selector: "fx-ui-bar-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, ngImport: i0, template: "<div class=\"flex flex-col gap-small\">\n @for (item of items(); track item.label) {\n <button\n type=\"button\"\n class=\"text-left rounded-control px-normal py-small -mx-normal transition-colors\"\n [class.hover:bg-surface-sunken]=\"interactive()\"\n [class.cursor-default]=\"!interactive()\"\n [tabindex]=\"interactive() ? 0 : -1\"\n (click)=\"onSelect(item)\"\n >\n <div class=\"flex items-baseline justify-between gap-normal\">\n <span class=\"txt-body-medium-01 truncate\">{{ item.label }}</span>\n <span class=\"text-sm font-semibold text-strong shrink-0\" style=\"font-family: var(--og-font-figure)\">\n {{ item.valueLabel ?? item.value }}\n </span>\n </div>\n @if (item.meta) {\n <div class=\"txt-utility-description truncate\">{{ item.meta }}</div>\n }\n <div class=\"mt-xs h-[var(--og-track-h)] rounded-pill bg-track overflow-hidden\">\n <div\n class=\"h-full rounded-pill transition-all duration-500\"\n [ngClass]=\"barClass(item)\"\n [style.width.%]=\"widthOf(item)\"\n ></div>\n </div>\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4911
+ }
4912
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BarListComponent, decorators: [{
4913
+ type: Component,
4914
+ args: [{ selector: 'fx-ui-bar-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-col gap-small\">\n @for (item of items(); track item.label) {\n <button\n type=\"button\"\n class=\"text-left rounded-control px-normal py-small -mx-normal transition-colors\"\n [class.hover:bg-surface-sunken]=\"interactive()\"\n [class.cursor-default]=\"!interactive()\"\n [tabindex]=\"interactive() ? 0 : -1\"\n (click)=\"onSelect(item)\"\n >\n <div class=\"flex items-baseline justify-between gap-normal\">\n <span class=\"txt-body-medium-01 truncate\">{{ item.label }}</span>\n <span class=\"text-sm font-semibold text-strong shrink-0\" style=\"font-family: var(--og-font-figure)\">\n {{ item.valueLabel ?? item.value }}\n </span>\n </div>\n @if (item.meta) {\n <div class=\"txt-utility-description truncate\">{{ item.meta }}</div>\n }\n <div class=\"mt-xs h-[var(--og-track-h)] rounded-pill bg-track overflow-hidden\">\n <div\n class=\"h-full rounded-pill transition-all duration-500\"\n [ngClass]=\"barClass(item)\"\n [style.width.%]=\"widthOf(item)\"\n ></div>\n </div>\n </button>\n }\n</div>\n" }]
4915
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
4916
+
4917
+ /**
4918
+ * V2 facets rail — the first column of 3-column lookup screens: filter groups
4919
+ * with counts + a Reset link. Presentational: filtering itself stays in the
4920
+ * page (feed the emitted selection into BaseMasterDetail filters).
4921
+ *
4922
+ * <div mdFacets>
4923
+ * <fx-ui-facets-rail [groups]="facetGroups" [selection]="facets"
4924
+ * (selectionChange)="onFacets($event)" />
4925
+ * </div>
4926
+ */
4927
+ class FacetsRailComponent {
4928
+ groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
4929
+ selection = input({}, ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
4930
+ /** Caller-translated label for the reset affordance. */
4931
+ resetLabel = input('Reset', ...(ngDevMode ? [{ debugName: "resetLabel" }] : /* istanbul ignore next */ []));
4932
+ selectionChange = output();
4933
+ hasSelection = computed(() => Object.values(this.selection()).some((values) => values?.length), ...(ngDevMode ? [{ debugName: "hasSelection" }] : /* istanbul ignore next */ []));
4934
+ isSelected(group, option) {
4935
+ return (this.selection()[group.id] ?? []).includes(option.value);
4936
+ }
4937
+ toggle(group, option) {
4938
+ const current = this.selection()[group.id] ?? [];
4939
+ let next;
4940
+ if (group.multi) {
4941
+ next = current.includes(option.value)
4942
+ ? current.filter((v) => v !== option.value)
4943
+ : [...current, option.value];
4944
+ }
4945
+ else {
4946
+ next = current.includes(option.value) ? [] : [option.value];
4947
+ }
4948
+ this.selectionChange.emit({ ...this.selection(), [group.id]: next });
4949
+ }
4950
+ reset() {
4951
+ this.selectionChange.emit({});
4952
+ }
4953
+ toneClass(option) {
4954
+ return option.tone ? toneText(option.tone) : '';
4955
+ }
4956
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4957
+ 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 });
4958
+ }
4959
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, decorators: [{
4960
+ type: Component,
4961
+ 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" }]
4962
+ }], 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"] }] } });
4963
+
4964
+ /** Trend blocks are softened solids — whole literals (Tailwind v4 has no safelist). */
4965
+ const TREND_BLOCK = {
4966
+ critical: 'tone-dot-chart-critical',
4967
+ danger: 'tone-dot-chart-danger',
4968
+ warning: 'tone-dot-chart-warning',
4969
+ success: 'tone-dot-chart-success',
4970
+ information: 'tone-dot-chart-information',
4971
+ };
4972
+ /**
4973
+ * V2 project card — the Project Management catalog tile (design 12): initials
4974
+ * avatar + name + schedule line, toned score chip, risk-trend blocks, metric
4975
+ * columns and a "Last analysis · … / Open ›" footer.
4976
+ *
4977
+ * <fx-ui-project-card [card]="project" (open)="goTo(project)" />
4978
+ */
4979
+ class ProjectCardComponent {
4980
+ card = input.required(...(ngDevMode ? [{ debugName: "card" }] : /* istanbul ignore next */ []));
4981
+ open = output();
4982
+ scoreChipClass() {
4983
+ return toneBadge(this.card().scoreTone ?? null);
4984
+ }
4985
+ chipClass(chip) {
4986
+ return toneBadge(chip.tone ?? null);
4987
+ }
4988
+ blockClass(tone) {
4989
+ return tone ? TREND_BLOCK[tone] : 'bg-bg-hover';
4990
+ }
4991
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4992
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4993
+ }
4994
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, decorators: [{
4995
+ type: Component,
4996
+ args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
4997
+ }], propDecorators: { card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: true }] }], open: [{ type: i0.Output, args: ["open"] }] } });
4998
+
4999
+ /**
5000
+ * V2 pipeline stepper — the New Analysis 7-stage pipeline header. Renders the
5001
+ * stage strip only; the page owns the stage bodies (project the current one
5002
+ * below). Stage states are data — the parent decides what is done/active.
5003
+ *
5004
+ * <fx-ui-pipeline-stepper [stages]="stages" (stageClick)="goTo($event)">
5005
+ * …current stage body…
5006
+ * </fx-ui-pipeline-stepper>
5007
+ */
5008
+ class PipelineStepperComponent {
5009
+ stages = input([], ...(ngDevMode ? [{ debugName: "stages" }] : /* istanbul ignore next */ []));
5010
+ /** Only done stages are click-navigable (review a completed step). */
5011
+ clickable = input(true, ...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
5012
+ stageClick = output();
5013
+ stateOf(stage) {
5014
+ return stage.state ?? 'pending';
5015
+ }
5016
+ circleClass(stage) {
5017
+ switch (this.stateOf(stage)) {
5018
+ case 'done':
5019
+ return 'bg-accent text-accent-contrast border-accent';
5020
+ case 'active':
5021
+ return 'bg-accent-soft text-accent-soft-fg border-accent';
5022
+ case 'error':
5023
+ return 'tone-scale-danger tone-border-danger';
5024
+ default:
5025
+ return 'bg-surface text-faint border-border';
5026
+ }
5027
+ }
5028
+ labelClass(stage) {
5029
+ switch (this.stateOf(stage)) {
5030
+ case 'active':
5031
+ return 'text-strong font-semibold';
5032
+ case 'done':
5033
+ return 'text-body';
5034
+ case 'error':
5035
+ return 'tone-fg-danger';
5036
+ default:
5037
+ return 'text-faint';
5038
+ }
5039
+ }
5040
+ connectorClass(stage) {
5041
+ return this.stateOf(stage) === 'done' ? 'bg-accent' : 'bg-border';
5042
+ }
5043
+ canClick(stage) {
5044
+ return this.clickable() && this.stateOf(stage) === 'done';
5045
+ }
5046
+ onStageClick(stage, index) {
5047
+ if (this.canClick(stage))
5048
+ this.stageClick.emit(index);
5049
+ }
5050
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PipelineStepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5051
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PipelineStepperComponent, isStandalone: true, selector: "fx-ui-pipeline-stepper", inputs: { stages: { classPropertyName: "stages", publicName: "stages", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stageClick: "stageClick" }, ngImport: i0, 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", 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 });
5052
+ }
5053
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PipelineStepperComponent, decorators: [{
5054
+ type: Component,
5055
+ 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" }]
5056
+ }], 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"] }] } });
5057
+
5058
+ /** Facets-rail skeleton: N nhóm, mỗi nhóm một eyebrow + vài dòng lựa chọn. */
5059
+ class SkeletonFacetsComponent {
5060
+ groups = input(3, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
5061
+ rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
5062
+ // computed, không phải method — xem [[skeleton-ng0956-array-holes]].
5063
+ groupList = computed(() => range$3(this.groups()), ...(ngDevMode ? [{ debugName: "groupList" }] : /* istanbul ignore next */ []));
5064
+ rowList = computed(() => range$3(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
5065
+ rowWidth(i) {
5066
+ return [80, 65, 72, 58][i % 4];
5067
+ }
5068
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFacetsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5069
+ 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 });
5070
+ }
5071
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFacetsComponent, decorators: [{
5072
+ type: Component,
5073
+ 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" }]
5074
+ }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
5075
+ function range$3(n) {
5076
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5077
+ }
5078
+
5079
+ /** Dashboard skeleton: một hàng KPI + N panel có vùng chart. */
5080
+ class SkeletonDashboardComponent {
5081
+ kpis = input(4, ...(ngDevMode ? [{ debugName: "kpis" }] : /* istanbul ignore next */ []));
5082
+ panels = input(2, ...(ngDevMode ? [{ debugName: "panels" }] : /* istanbul ignore next */ []));
5083
+ kpiList = computed(() => range$2(this.kpis()), ...(ngDevMode ? [{ debugName: "kpiList" }] : /* istanbul ignore next */ []));
5084
+ panelList = computed(() => range$2(this.panels()), ...(ngDevMode ? [{ debugName: "panelList" }] : /* istanbul ignore next */ []));
5085
+ barList = computed(() => range$2(7), ...(ngDevMode ? [{ debugName: "barList" }] : /* istanbul ignore next */ []));
5086
+ /** Chiều cao cột giả — dựng hình chart mà không cần dữ liệu. */
5087
+ barHeight(i) {
5088
+ return [45, 70, 35, 85, 55, 75, 40][i % 7];
5089
+ }
5090
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5091
+ 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 });
5092
+ }
5093
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDashboardComponent, decorators: [{
5094
+ type: Component,
5095
+ 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" }]
5096
+ }], propDecorators: { kpis: [{ type: i0.Input, args: [{ isSignal: true, alias: "kpis", required: false }] }], panels: [{ type: i0.Input, args: [{ isSignal: true, alias: "panels", required: false }] }] } });
5097
+ function range$2(n) {
5098
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5099
+ }
5100
+
5101
+ const PRESETS = {
5102
+ table: ['heading', 'toolbar', 'table'],
5103
+ panels: [
5104
+ 'heading',
5105
+ 'stat-cards',
5106
+ { kind: 'panels', count: 3, cols: 3 },
5107
+ 'toolbar',
5108
+ { kind: 'cards', count: 6, cols: 3 },
5109
+ ],
5110
+ dashboard: [
5111
+ 'heading',
5112
+ 'kpi-strip',
5113
+ { kind: 'panels', count: 2 },
5114
+ { kind: 'panels', count: 2 },
5115
+ 'card',
5116
+ ],
5117
+ 'tabs-facets': ['tabs', 'facets'],
5118
+ facets: ['facets'],
5119
+ detail: ['detail'],
5120
+ form: ['heading', 'form'],
5121
+ };
5122
+ /**
5123
+ * Skeleton cho vùng NỘI DUNG trong lúc resolver của route đang chạy.
5124
+ *
5125
+ * ⚠️ Vì sao skeleton phải nằm ở shell chứ không ở từng màn: Angular **không
5126
+ * tạo component cho tới khi resolver xong**, nên một màn không thể tự vẽ
5127
+ * skeleton của chính nó trong lúc resolver của nó chạy. Thứ vẽ được phải tồn
5128
+ * tại TRƯỚC component đó → shell + router event. Xem `FxRouteSkeletonService`.
5129
+ */
5130
+ class RouteSkeletonComponent {
5131
+ /** Tên preset, hoặc mảng block mô tả đúng layout của màn. */
5132
+ hint = input('table', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
5133
+ blocks = computed(() => {
5134
+ const value = this.hint();
5135
+ // Mảng rỗng / tên preset lạ / null đều rơi về preset bảng: mục tiêu của cả
5136
+ // cơ chế này là KHÔNG BAO GIỜ để vùng nội dung trắng, nên vẽ hình generic
5137
+ // vẫn hơn không vẽ gì.
5138
+ const list = Array.isArray(value) && value.length
5139
+ ? value
5140
+ : (PRESETS[value] ?? PRESETS.table);
5141
+ return list.map((b) => (typeof b === 'string' ? { kind: b } : b));
5142
+ }, ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
5143
+ cols(b) {
5144
+ return range$1(b.columns ?? 6);
5145
+ }
5146
+ rows(b) {
5147
+ return range$1(b.rows ?? 8);
5148
+ }
5149
+ items(b, fallback) {
5150
+ return range$1(b.count ?? fallback);
5151
+ }
5152
+ /** Lớp lưới cho `panels`/`cards` — Tailwind v4 không có safelist nên phải
5153
+ * là chuỗi tĩnh, không ghép động. */
5154
+ gridClass(b) {
5155
+ switch (b.cols ?? 2) {
5156
+ case 1:
5157
+ return 'grid-cols-1';
5158
+ case 3:
5159
+ return 'grid-cols-1 md:grid-cols-3';
5160
+ case 4:
5161
+ return 'grid-cols-2 md:grid-cols-4';
5162
+ default:
5163
+ return 'grid-cols-1 md:grid-cols-2';
5164
+ }
5165
+ }
5166
+ /** Header ngắn hơn ô dữ liệu, cột đầu rộng nhất (thường là tên). */
5167
+ headWidth(c) {
5168
+ return c === 0 ? 55 : [40, 34, 46, 30, 38][c % 5];
5169
+ }
5170
+ /**
5171
+ * Bề rộng đổi theo cả hàng và cột — cùng bề rộng thì khối skeleton trông
5172
+ * như một hình chữ nhật đặc, mất cảm giác "từng field".
5173
+ */
5174
+ cellWidth(r, c) {
5175
+ if (c === 0)
5176
+ return [92, 78, 85, 70][r % 4];
5177
+ return [62, 48, 74, 55, 66, 42][(r + c) % 6];
5178
+ }
5179
+ barHeight(i) {
5180
+ return [45, 70, 35, 85, 55, 75, 40][i % 7];
5181
+ }
5182
+ bars = range$1(7);
5183
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5184
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RouteSkeletonComponent, isStandalone: true, selector: "fx-ui-route-skeleton", inputs: { hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: SkeletonFacetsComponent, selector: "fx-ui-skeleton-facets", inputs: ["groups", "rows"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5185
+ }
5186
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, decorators: [{
5187
+ type: Component,
5188
+ args: [{ selector: 'fx-ui-route-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SkeletonListComponent, SkeletonDetailComponent, SkeletonFacetsComponent], template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"] }]
5189
+ }], propDecorators: { hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
5190
+ function range$1(n) {
5191
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5192
+ }
5193
+
5194
+ /**
5195
+ * Skeleton của cả AppFrame (drawer + topbar + vùng nội dung).
5196
+ *
5197
+ * Chỉ dùng cho giai đoạn **bootstrap**, khi shell của host còn CHƯA tồn tại
5198
+ * (App.ngOnInit đang kiểm tenant + session, router chưa initialNavigation).
5199
+ * Mọi điều hướng SAU đó đã có shell thật → dùng `fx-ui-route-skeleton` bên
5200
+ * trong vùng nội dung, đừng vẽ lại cả frame.
5201
+ *
5202
+ * ⚠️ `contentHint` là thứ làm F5 không còn vẽ hình generic. Lần điều hướng ĐẦU
5203
+ * tạo ra chính `HomeComponent` — nơi chứa `fx-ui-route-skeleton` — nên vùng nội
5204
+ * dung không thể tự vẽ skeleton của route ở lần đó. Truyền hint vào đây để
5205
+ * frame dựng đúng composition của màn đích ngay từ F5.
5206
+ */
5207
+ class SkeletonFrameComponent {
5208
+ navItems = input(7, ...(ngDevMode ? [{ debugName: "navItems" }] : /* istanbul ignore next */ []));
5209
+ /** Composition của route đích; null thì vẽ khối generic. */
5210
+ contentHint = input(null, ...(ngDevMode ? [{ debugName: "contentHint" }] : /* istanbul ignore next */ []));
5211
+ navList = computed(() => range(this.navItems()), ...(ngDevMode ? [{ debugName: "navList" }] : /* istanbul ignore next */ []));
5212
+ navWidth(i) {
5213
+ return [70, 55, 62, 48, 66, 52, 58][i % 7];
5214
+ }
5215
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5216
+ 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 });
5217
+ }
5218
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFrameComponent, decorators: [{
5219
+ type: Component,
5220
+ 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" }]
5221
+ }], propDecorators: { navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], contentHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentHint", required: false }] }] } });
5222
+ function range(n) {
5223
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5224
+ }
5225
+
4069
5226
  const FX_COMPONENTS = [
4070
5227
  InputComponent,
4071
5228
  SelectComponent,
@@ -4085,7 +5242,6 @@ const FX_COMPONENTS = [
4085
5242
  TagComponent,
4086
5243
  SliderComponent,
4087
5244
  SwitchComponent,
4088
- SkeletonTableLoadingComponent,
4089
5245
  CircleProgressBar,
4090
5246
  TreeDiagram,
4091
5247
  TableCell,
@@ -4096,12 +5252,32 @@ const FX_COMPONENTS = [
4096
5252
  StatCardsComponent,
4097
5253
  EmptyStateComponent,
4098
5254
  FilterChipsComponent,
5255
+ FilterPillsComponent,
5256
+ StageDotsComponent,
5257
+ CodeBlockComponent,
5258
+ ScreenshotFrameComponent,
4099
5259
  MeterComponent,
4100
5260
  SectionCardComponent,
4101
5261
  InfoFieldComponent,
4102
5262
  SeverityBadgesComponent,
4103
5263
  MasterDetailComponent,
4104
5264
  MenuComponent,
5265
+ ThemePickerComponent,
5266
+ PendingBadgeComponent,
5267
+ SampleFrameComponent,
5268
+ ScoreBadgeComponent,
5269
+ StatusDotComponent,
5270
+ SparkbarComponent,
5271
+ SegmentedComponent,
5272
+ ListRowComponent,
5273
+ BarListComponent,
5274
+ FacetsRailComponent,
5275
+ ProjectCardComponent,
5276
+ PipelineStepperComponent,
5277
+ SkeletonFacetsComponent,
5278
+ SkeletonDashboardComponent,
5279
+ SkeletonFrameComponent,
5280
+ RouteSkeletonComponent,
4105
5281
  ];
4106
5282
 
4107
5283
  const DIALOGS_COMPONENT = [
@@ -4125,14 +5301,14 @@ class UiModule {
4125
5301
  ReactiveFormsModule,
4126
5302
  RouterModule,
4127
5303
  HasPermissionDirective,
4128
- 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, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, 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, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ConfirmationDialogComponent, FormsModule,
5304
+ TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent, FormsModule,
4129
5305
  ReactiveFormsModule,
4130
5306
  RouterModule,
4131
5307
  HasPermissionDirective,
4132
5308
  TrimOnBlurDirective] });
4133
5309
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiModule, imports: [FormsModule,
4134
5310
  ReactiveFormsModule,
4135
- RouterModule, SelectComponent, DatetimePicker, MasterDetailComponent, MenuComponent, FormsModule,
5311
+ RouterModule, SelectComponent, DatetimePicker, MasterDetailComponent, MenuComponent, ThemePickerComponent, FormsModule,
4136
5312
  ReactiveFormsModule,
4137
5313
  RouterModule] });
4138
5314
  }
@@ -4168,5 +5344,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4168
5344
  * Generated bundle index. Do not edit.
4169
5345
  */
4170
5346
 
4171
- export { AuthInterceptor, AuthStateService, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseResolver, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FilterChipsComponent, FxComponent, FxDialogService, FxLoadingService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, PermissionGuard, PermissionService, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, SearchBarComponent, SectionCardComponent, SelectComponent, SeverityBadgesComponent, SkeletonDetailComponent, SkeletonListComponent, SkeletonTableLoadingComponent, SliderComponent, StatCardsComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, countTone, createApi, fxListResolver, scoreTone, severityTone, toneBadge, toneBg, toneColor, toneText };
5347
+ export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FX_SKELETON_DATA_KEY, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxDialogService, FxLoadingService, FxRouteSkeletonService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, ListRowComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, OG_LIGHT_THEMES, OG_PICKER_THEMES, OG_PICKER_THEME_META, OG_THEMES, OG_THEME_META, PendingBadgeComponent, PermissionGuard, PermissionService, PipelineStepperComponent, ProjectCardComponent, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, RouteSkeletonComponent, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDashboardComponent, SkeletonDetailComponent, SkeletonFacetsComponent, SkeletonFrameComponent, SkeletonListComponent, SliderComponent, SparkbarComponent, StageDotsComponent, StatCardsComponent, StatusDotComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemePickerComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, anyFeatureEnabled, countTone, createApi, featureEnabled, fxListResolver, scoreTone, severityTone, toneBadge, toneBadgeSolid, toneBg, toneBorder, toneColor, toneDot, toneDotChart, toneFg, toneScale, toneText, toneTint };
4172
5348
  //# sourceMappingURL=foxeltech-angular-ui.mjs.map