@foxeltech/angular-ui 0.4.0 → 0.7.44

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 (165) 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 +12 -12
  14. package/dialogs.css +2 -2
  15. package/fesm2022/foxeltech-angular-ui-chart.mjs +185 -17
  16. package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
  17. package/fesm2022/foxeltech-angular-ui-flow.mjs +176 -3
  18. package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
  19. package/fesm2022/foxeltech-angular-ui-rich-text.mjs +2 -2
  20. package/fesm2022/foxeltech-angular-ui-rich-text.mjs.map +1 -1
  21. package/fesm2022/foxeltech-angular-ui.mjs +849 -69
  22. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  23. package/fonts.css +462 -138
  24. package/ibm-plex-mono/ibm-plex-mono-400-cyrillic-ext.woff2 +0 -0
  25. package/ibm-plex-mono/ibm-plex-mono-400-cyrillic.woff2 +0 -0
  26. package/ibm-plex-mono/ibm-plex-mono-400-latin-ext.woff2 +0 -0
  27. package/ibm-plex-mono/ibm-plex-mono-400-latin.woff2 +0 -0
  28. package/ibm-plex-mono/ibm-plex-mono-400-vietnamese.woff2 +0 -0
  29. package/ibm-plex-mono/ibm-plex-mono-500-cyrillic-ext.woff2 +0 -0
  30. package/ibm-plex-mono/ibm-plex-mono-500-cyrillic.woff2 +0 -0
  31. package/ibm-plex-mono/ibm-plex-mono-500-latin-ext.woff2 +0 -0
  32. package/ibm-plex-mono/ibm-plex-mono-500-latin.woff2 +0 -0
  33. package/ibm-plex-mono/ibm-plex-mono-500-vietnamese.woff2 +0 -0
  34. package/ibm-plex-mono/ibm-plex-mono-600-cyrillic-ext.woff2 +0 -0
  35. package/ibm-plex-mono/ibm-plex-mono-600-cyrillic.woff2 +0 -0
  36. package/ibm-plex-mono/ibm-plex-mono-600-latin-ext.woff2 +0 -0
  37. package/ibm-plex-mono/ibm-plex-mono-600-latin.woff2 +0 -0
  38. package/ibm-plex-mono/ibm-plex-mono-600-vietnamese.woff2 +0 -0
  39. package/jetbrains-mono/jetbrains-mono-400-cyrillic-ext.woff2 +0 -0
  40. package/jetbrains-mono/jetbrains-mono-400-cyrillic.woff2 +0 -0
  41. package/jetbrains-mono/jetbrains-mono-400-greek.woff2 +0 -0
  42. package/jetbrains-mono/jetbrains-mono-400-latin-ext.woff2 +0 -0
  43. package/jetbrains-mono/jetbrains-mono-400-latin.woff2 +0 -0
  44. package/jetbrains-mono/jetbrains-mono-400-vietnamese.woff2 +0 -0
  45. package/jetbrains-mono/jetbrains-mono-500-cyrillic-ext.woff2 +0 -0
  46. package/jetbrains-mono/jetbrains-mono-500-cyrillic.woff2 +0 -0
  47. package/jetbrains-mono/jetbrains-mono-500-greek.woff2 +0 -0
  48. package/jetbrains-mono/jetbrains-mono-500-latin-ext.woff2 +0 -0
  49. package/jetbrains-mono/jetbrains-mono-500-latin.woff2 +0 -0
  50. package/jetbrains-mono/jetbrains-mono-500-vietnamese.woff2 +0 -0
  51. package/jetbrains-mono/jetbrains-mono-600-cyrillic-ext.woff2 +0 -0
  52. package/jetbrains-mono/jetbrains-mono-600-cyrillic.woff2 +0 -0
  53. package/jetbrains-mono/jetbrains-mono-600-greek.woff2 +0 -0
  54. package/jetbrains-mono/jetbrains-mono-600-latin-ext.woff2 +0 -0
  55. package/jetbrains-mono/jetbrains-mono-600-latin.woff2 +0 -0
  56. package/jetbrains-mono/jetbrains-mono-600-vietnamese.woff2 +0 -0
  57. package/jetbrains-mono/jetbrains-mono-700-cyrillic-ext.woff2 +0 -0
  58. package/jetbrains-mono/jetbrains-mono-700-cyrillic.woff2 +0 -0
  59. package/jetbrains-mono/jetbrains-mono-700-greek.woff2 +0 -0
  60. package/jetbrains-mono/jetbrains-mono-700-latin-ext.woff2 +0 -0
  61. package/jetbrains-mono/jetbrains-mono-700-latin.woff2 +0 -0
  62. package/jetbrains-mono/jetbrains-mono-700-vietnamese.woff2 +0 -0
  63. package/newsreader/newsreader-400-latin-ext.woff2 +0 -0
  64. package/newsreader/newsreader-400-latin.woff2 +0 -0
  65. package/newsreader/newsreader-400-vietnamese.woff2 +0 -0
  66. package/newsreader/newsreader-500-latin-ext.woff2 +0 -0
  67. package/newsreader/newsreader-500-latin.woff2 +0 -0
  68. package/newsreader/newsreader-500-vietnamese.woff2 +0 -0
  69. package/newsreader/newsreader-600-latin-ext.woff2 +0 -0
  70. package/newsreader/newsreader-600-latin.woff2 +0 -0
  71. package/newsreader/newsreader-600-vietnamese.woff2 +0 -0
  72. package/package.json +5 -2
  73. package/source-serif-4/source-serif-4-400-cyrillic-ext.woff2 +0 -0
  74. package/source-serif-4/source-serif-4-400-cyrillic.woff2 +0 -0
  75. package/source-serif-4/source-serif-4-400-greek.woff2 +0 -0
  76. package/source-serif-4/source-serif-4-400-latin-ext.woff2 +0 -0
  77. package/source-serif-4/source-serif-4-400-latin.woff2 +0 -0
  78. package/source-serif-4/source-serif-4-400-vietnamese.woff2 +0 -0
  79. package/source-serif-4/source-serif-4-600-cyrillic-ext.woff2 +0 -0
  80. package/source-serif-4/source-serif-4-600-cyrillic.woff2 +0 -0
  81. package/source-serif-4/source-serif-4-600-greek.woff2 +0 -0
  82. package/source-serif-4/source-serif-4-600-latin-ext.woff2 +0 -0
  83. package/source-serif-4/source-serif-4-600-latin.woff2 +0 -0
  84. package/source-serif-4/source-serif-4-600-vietnamese.woff2 +0 -0
  85. package/src/lib/styles/components.css +12 -12
  86. package/src/lib/styles/dialogs.css +2 -2
  87. package/src/lib/styles/fonts.css +462 -138
  88. package/src/lib/styles/tailwind.css +1 -1
  89. package/src/lib/styles/theme.css +596 -69
  90. package/src/lib/styles/tokens.css +56 -19
  91. package/src/lib/styles/utilities.css +4 -1
  92. package/src/lib/ui/components/bar-list/bar-list.component.html +29 -0
  93. package/src/lib/ui/components/button/button.component.html +7 -5
  94. package/src/lib/ui/components/checkbox/checkbox.component.html +1 -1
  95. package/src/lib/ui/components/code-block/code-block.component.html +8 -0
  96. package/src/lib/ui/components/drawer/drawer.component.html +35 -12
  97. package/src/lib/ui/components/drawer/drawer.component.scss +104 -18
  98. package/src/lib/ui/components/facets-rail/facets-rail.component.html +42 -0
  99. package/src/lib/ui/components/filter-pills/filter-pills.component.html +16 -0
  100. package/src/lib/ui/components/kanban-board/kanban-board.component.scss +9 -9
  101. package/src/lib/ui/components/list-row/list-row.component.html +20 -0
  102. package/src/lib/ui/components/master-detail/master-detail.component.css +22 -0
  103. package/src/lib/ui/components/master-detail/master-detail.component.html +33 -1
  104. package/src/lib/ui/components/pending-badge/pending-badge.component.html +15 -0
  105. package/src/lib/ui/components/pipeline-stepper/pipeline-stepper.component.html +45 -0
  106. package/src/lib/ui/components/project-card/project-card.component.html +102 -0
  107. package/src/lib/ui/components/radio-button/radio-button.component.html +1 -1
  108. package/src/lib/ui/components/radio-button-toggle/radio-button-toggle.component.html +1 -1
  109. package/src/lib/ui/components/sample-frame/sample-frame.component.html +18 -0
  110. package/src/lib/ui/components/score-badge/score-badge.component.html +7 -0
  111. package/src/lib/ui/components/screenshot-frame/screenshot-frame.component.html +40 -0
  112. package/src/lib/ui/components/section-card/section-card.component.html +19 -6
  113. package/src/lib/ui/components/segmented/segmented.component.html +42 -0
  114. package/src/lib/ui/components/segmented/segmented.component.scss +12 -0
  115. package/src/lib/ui/components/slider/slider.component.html +2 -2
  116. package/src/lib/ui/components/sparkbar/sparkbar.component.html +14 -0
  117. package/src/lib/ui/components/stage-dots/stage-dots.component.html +13 -0
  118. package/src/lib/ui/components/stat-cards/stat-cards.component.html +44 -11
  119. package/src/lib/ui/components/status-dot/status-dot.component.html +13 -0
  120. package/src/lib/ui/components/tab-group/tab-group.component.html +37 -8
  121. package/src/lib/ui/components/tab-group/tab-group.component.scss +41 -1
  122. package/src/lib/ui/components/tag/tag.component.html +2 -2
  123. package/src/lib/ui/components/theme-picker/theme-picker.component.html +44 -0
  124. package/src/lib/ui/components/tree-diagram/tree-diagram.component.html +3 -3
  125. package/tailwind.css +1 -1
  126. package/theme.css +596 -69
  127. package/tokens.css +56 -19
  128. package/types/foxeltech-angular-ui-chart.d.ts +14 -2
  129. package/types/foxeltech-angular-ui-flow.d.ts +83 -1
  130. package/types/foxeltech-angular-ui.d.ts +636 -20
  131. package/utilities.css +4 -1
  132. package/BeVietnamPro/BeVietnamPro-Bold.woff2 +0 -0
  133. package/BeVietnamPro/BeVietnamPro-ExtraLight.woff2 +0 -0
  134. package/BeVietnamPro/BeVietnamPro-Light.woff2 +0 -0
  135. package/BeVietnamPro/BeVietnamPro-Medium.woff2 +0 -0
  136. package/BeVietnamPro/BeVietnamPro-Regular.woff2 +0 -0
  137. package/BeVietnamPro/BeVietnamPro-SemiBold.woff2 +0 -0
  138. package/BeVietnamPro/BeVietnamPro-Thin.woff2 +0 -0
  139. package/SpaceMono-Regular.woff2 +0 -0
  140. package/dm-sans/DMSans-Bold.woff2 +0 -0
  141. package/dm-sans/DMSans-Light.woff2 +0 -0
  142. package/dm-sans/DMSans-Medium.woff2 +0 -0
  143. package/dm-sans/DMSans-Regular.woff2 +0 -0
  144. package/dm-sans/DMSans-SemiBold.woff2 +0 -0
  145. package/dm-sans/DMSans-Thin.woff2 +0 -0
  146. package/orbitron/Orbitron-Bold.woff2 +0 -0
  147. package/orbitron/Orbitron-Medium.woff2 +0 -0
  148. package/orbitron/Orbitron-Regular.woff2 +0 -0
  149. package/orbitron/Orbitron-SemiBold.woff2 +0 -0
  150. package/roboto/Roboto-Black.woff2 +0 -0
  151. package/roboto/Roboto-BlackItalic.woff2 +0 -0
  152. package/roboto/Roboto-Bold.woff2 +0 -0
  153. package/roboto/Roboto-BoldCondensed.woff2 +0 -0
  154. package/roboto/Roboto-BoldCondensedItalic.woff2 +0 -0
  155. package/roboto/Roboto-BoldItalic.woff2 +0 -0
  156. package/roboto/Roboto-Condensed.woff2 +0 -0
  157. package/roboto/Roboto-CondensedItalic.woff2 +0 -0
  158. package/roboto/Roboto-Italic.woff2 +0 -0
  159. package/roboto/Roboto-Light.woff2 +0 -0
  160. package/roboto/Roboto-LightItalic.woff2 +0 -0
  161. package/roboto/Roboto-Medium.woff2 +0 -0
  162. package/roboto/Roboto-MediumItalic.woff2 +0 -0
  163. package/roboto/Roboto-Regular.woff2 +0 -0
  164. package/roboto/Roboto-Thin.woff2 +0 -0
  165. package/roboto/Roboto-ThinItalic.woff2 +0 -0
@@ -1,10 +1,10 @@
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, NgModule, HostListener, output, ChangeDetectorRef, TemplateRef, contentChildren, Output, ViewChildren, computed, ElementRef, contentChild, 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';
@@ -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,
@@ -1514,6 +1528,14 @@ function toneColor(tone, alpha = 1) {
1514
1528
  return FxUtils.convertColorFromVariable(name, alpha, { legacyCommaSyntax: true });
1515
1529
  }
1516
1530
 
1531
+ function featureEnabled(flags, name) {
1532
+ return flags?.[name] === true;
1533
+ }
1534
+ /** Convenience for templates that gate on several flags at once. */
1535
+ function anyFeatureEnabled(flags, ...names) {
1536
+ return names.some((name) => featureEnabled(flags, name));
1537
+ }
1538
+
1517
1539
  class FxValidators {
1518
1540
  /**
1519
1541
  * Name fields: firstName, lastName, groupName, projectName, displayName
@@ -1905,37 +1927,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1905
1927
  }]
1906
1928
  }] });
1907
1929
 
1930
+ const OG_THEMES = [
1931
+ 'porcelain',
1932
+ 'charcoal',
1933
+ 'ivory',
1934
+ 'studio',
1935
+ 'blueprint',
1936
+ 'terminal',
1937
+ ];
1938
+ const OG_LIGHT_THEMES = ['porcelain', 'ivory', 'terminal'];
1939
+ const OG_THEME_META = [
1940
+ { name: 'porcelain', label: 'Porcelain', accent: '#7c3aed', pageBg: '#f2f2f1', dark: false },
1941
+ { name: 'charcoal', label: 'Charcoal', accent: '#ff6b4a', pageBg: '#0e0e10', dark: true },
1942
+ { name: 'ivory', label: 'Ivory', accent: '#d97757', pageBg: '#e8e6e1', dark: false },
1943
+ { name: 'studio', label: 'Studio', accent: '#5a8fd6', pageBg: '#0e0e10', dark: true },
1944
+ { name: 'blueprint', label: 'Blueprint', accent: '#e07a3a', pageBg: '#0e0e10', dark: true },
1945
+ { name: 'terminal', label: 'Terminal', accent: '#3a55d9', pageBg: '#e8e6e1', dark: false },
1946
+ ];
1947
+ /**
1948
+ * Theme NGƯỜI DÙNG được chọn (anh Tú chốt 31/08): chỉ còn một cặp
1949
+ * light/dark. Bốn theme còn lại vẫn tồn tại đầy đủ trong theme.css và
1950
+ * setTheme() vẫn nhận — chúng dùng cho trang /themes của demo và cho
1951
+ * harness screenshot chạy ma trận 6 theme; chỉ **giấu khỏi picker**.
1952
+ * Muốn mở lại: thêm tên vào mảng này, không phải dựng lại token.
1953
+ */
1954
+ const OG_PICKER_THEMES = ['porcelain', 'charcoal'];
1955
+ /** OG_THEME_META đã lọc theo OG_PICKER_THEMES — nguồn duy nhất cho mọi picker. */
1956
+ const OG_PICKER_THEME_META = OG_THEME_META.filter((t) => OG_PICKER_THEMES.includes(t.name));
1957
+ /**
1958
+ * V2 theme switching: one `data-og-theme` attribute on <html> selects the
1959
+ * --og-* token block (theme.css). During the migration window the service
1960
+ * also mirrors the legacy `light_theme` class on <body> so remotes built
1961
+ * against the pre-V2 stylesheet keep resolving the correct palette
1962
+ * (see the SKEW SHIELD note in theme.css).
1963
+ */
1908
1964
  class ThemeService {
1909
1965
  renderer;
1910
1966
  lightClass = 'light_theme';
1911
- theme$ = new BehaviorSubject('light');
1967
+ theme$ = new BehaviorSubject('porcelain');
1912
1968
  themeStorageKey = 'host-theme';
1913
1969
  constructor(rendererFactory) {
1914
1970
  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) {
1971
+ this.setTheme(this.readSavedTheme());
1972
+ }
1973
+ /** Primary API: activate one of the six V2 themes. */
1974
+ setTheme(theme) {
1975
+ if (!OG_THEMES.includes(theme))
1976
+ theme = 'porcelain';
1977
+ this.renderer.setAttribute(document.documentElement, 'data-og-theme', theme);
1978
+ // Skew shim for not-yet-migrated remote stylesheets (.light_theme era).
1979
+ if (this.isLightTheme(theme)) {
1925
1980
  this.renderer.addClass(document.body, this.lightClass);
1926
- this.theme$.next('light');
1927
- localStorage.setItem(this.themeStorageKey, 'light');
1928
1981
  }
1929
1982
  else {
1930
1983
  this.renderer.removeClass(document.body, this.lightClass);
1931
- this.theme$.next('dark');
1932
- localStorage.setItem(this.themeStorageKey, 'dark');
1933
1984
  }
1934
- window.dispatchEvent(new CustomEvent('theme-changed', { detail: { dark: !enable } }));
1935
- }
1936
- toggleTheme() {
1937
- const isLight = this.theme$.value === 'light';
1938
- this.setLightMode(!isLight);
1985
+ localStorage.setItem(this.themeStorageKey, theme);
1986
+ this.theme$.next(theme);
1987
+ // detail.dark is the pre-V2 contract (chart rebuilds on it); detail.theme
1988
+ // is new. Listeners must re-read colors on EVERY event — two light themes
1989
+ // can differ even though `dark` did not change.
1990
+ window.dispatchEvent(new CustomEvent('theme-changed', { detail: { dark: !this.isLightTheme(theme), theme } }));
1939
1991
  }
1940
1992
  themeChanges() {
1941
1993
  return this.theme$.asObservable();
@@ -1944,11 +1996,31 @@ class ThemeService {
1944
1996
  return this.theme$.value;
1945
1997
  }
1946
1998
  isLight() {
1947
- return this.theme$.value === 'light';
1999
+ return this.isLightTheme(this.theme$.value);
2000
+ }
2001
+ /** @deprecated V2: use setTheme(). Maps light→porcelain, dark→charcoal. */
2002
+ setLightMode(enable) {
2003
+ this.setTheme(enable ? 'porcelain' : 'charcoal');
2004
+ }
2005
+ /** @deprecated V2: use setTheme(). Flips between the two default themes. */
2006
+ toggleTheme() {
2007
+ this.setTheme(this.isLight() ? 'charcoal' : 'porcelain');
1948
2008
  }
1949
2009
  getLogoPath() {
1950
2010
  return 'assets/images/logo.png';
1951
2011
  }
2012
+ isLightTheme(theme) {
2013
+ return OG_LIGHT_THEMES.includes(theme);
2014
+ }
2015
+ readSavedTheme() {
2016
+ const saved = localStorage.getItem(this.themeStorageKey);
2017
+ // Migrate pre-V2 stored values.
2018
+ if (saved === 'light')
2019
+ return 'porcelain';
2020
+ if (saved === 'dark')
2021
+ return 'charcoal';
2022
+ return OG_THEMES.includes(saved) ? saved : 'porcelain';
2023
+ }
1952
2024
  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
2025
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemeService, providedIn: 'root' });
1954
2026
  }
@@ -2201,11 +2273,11 @@ class ButtonComponent {
2201
2273
  this.clicked.emit();
2202
2274
  }
2203
2275
  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 });
2276
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2205
2277
  }
2206
2278
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
2207
2279
  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" }]
2280
+ args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2209
2281
  }], propDecorators: { label: [{
2210
2282
  type: Input
2211
2283
  }], 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 +2489,11 @@ class CheckboxComponent extends FxComponent {
2417
2489
  this.onTouched();
2418
2490
  }
2419
2491
  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 });
2492
+ 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
2493
  }
2422
2494
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CheckboxComponent, decorators: [{
2423
2495
  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" }]
2496
+ 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
2497
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2426
2498
  type: Input
2427
2499
  }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
@@ -2559,7 +2631,7 @@ class DatetimePicker extends FxComponent {
2559
2631
  provideNativeDateAdapter(),
2560
2632
  // en-GB display ⇒ DD/MM/YYYY + 24h — matches FxUtils.formatDateTime.
2561
2633
  { 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 });
2634
+ ], 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
2635
  }
2564
2636
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DatetimePicker, decorators: [{
2565
2637
  type: Component,
@@ -2567,7 +2639,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2567
2639
  provideNativeDateAdapter(),
2568
2640
  // en-GB display ⇒ DD/MM/YYYY + 24h — matches FxUtils.formatDateTime.
2569
2641
  { 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"] }]
2642
+ ], 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
2643
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2572
2644
  type: Input
2573
2645
  }], 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 +2891,11 @@ class RadioButtonToggleComponent extends FxComponent {
2819
2891
  this.onTouched();
2820
2892
  }
2821
2893
  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 });
2894
+ 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
2895
  }
2824
2896
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonToggleComponent, decorators: [{
2825
2897
  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" }]
2898
+ 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
2899
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2828
2900
  type: Input
2829
2901
  }], 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 +2927,7 @@ class RadioButtonComponent extends FxComponent {
2855
2927
  this.onTouched();
2856
2928
  }
2857
2929
  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 });
2930
+ 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
2931
  }
2860
2932
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonComponent, decorators: [{
2861
2933
  type: Component,
@@ -2863,7 +2935,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2863
2935
  HeroIconComponent,
2864
2936
  MatTooltip,
2865
2937
  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" }]
2938
+ ], 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
2939
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
2868
2940
  type: Input
2869
2941
  }], tooltip: [{
@@ -2874,6 +2946,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2874
2946
 
2875
2947
  class SearchBarComponent extends FxComponent {
2876
2948
  ref;
2949
+ /** FxComponent leaves `value` undefined; without ngModel the input would
2950
+ render the literal string "undefined". */
2951
+ value = '';
2877
2952
  placeholder = input('Search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
2878
2953
  search = output();
2879
2954
  valueChange = output();
@@ -3155,7 +3230,7 @@ class SliderComponent extends FxComponent {
3155
3230
  : { left: `${this.percent}%`, transform: 'translateX(-50%)' };
3156
3231
  }
3157
3232
  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 });
3233
+ 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
3234
  }
3160
3235
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SliderComponent, decorators: [{
3161
3236
  type: Component,
@@ -3163,7 +3238,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3163
3238
  NgClass,
3164
3239
  NgStyle,
3165
3240
  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" }]
3241
+ ], 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
3242
  }], 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
3243
  type: HostListener,
3169
3244
  args: ['keydown', ['$event']]
@@ -3198,24 +3273,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3198
3273
  class TabComponent {
3199
3274
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3200
3275
  icon;
3276
+ /** Trailing count badge (V2 'page' variant); null/undefined hides it. */
3277
+ count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
3278
+ /** Tints the count badge (e.g. vulnerabilities tab shows a danger count). */
3279
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3201
3280
  content = viewChild.required(TemplateRef);
3202
3281
  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 });
3282
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.20", type: TabComponent, isStandalone: true, selector: "fx-ui-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "content", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template><ng-content></ng-content></ng-template>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
3204
3283
  }
3205
3284
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, decorators: [{
3206
3285
  type: Component,
3207
3286
  args: [{ selector: 'fx-ui-tab', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template><ng-content></ng-content></ng-template>\n" }]
3208
3287
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{
3209
3288
  type: Input
3210
- }], content: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
3289
+ }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], content: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
3211
3290
 
3212
3291
  class TabGroupComponent {
3213
3292
  renderer;
3214
3293
  cdr = inject(ChangeDetectorRef);
3215
3294
  tabs = contentChildren(TabComponent, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
3216
3295
  activeIndex = 0;
3296
+ /** Emitted on user selection — enables [(activeIndex)] two-way binding. */
3297
+ activeIndexChange = output();
3298
+ /** 'page' = the V2 workspace tab strip (larger hit area, count badges). */
3299
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
3217
3300
  tabListRef = viewChild.required('tabList');
3218
- underlineRef = viewChild.required('underline');
3301
+ underlineRef = viewChild('underline', ...(ngDevMode ? [{ debugName: "underlineRef" }] : /* istanbul ignore next */ []));
3302
+ pagePillRef = viewChild('pagePill', ...(ngDevMode ? [{ debugName: "pagePillRef" }] : /* istanbul ignore next */ []));
3303
+ /** Last applied pill geometry — skip DOM writes when nothing moved. */
3304
+ pillState = { left: -1, width: -1, painted: false };
3219
3305
  canScrollLeft = false;
3220
3306
  canScrollRight = false;
3221
3307
  animateDelay = 50;
@@ -3234,8 +3320,12 @@ class TabGroupComponent {
3234
3320
  }
3235
3321
  selectTab(index) {
3236
3322
  this.activeIndex = index;
3323
+ this.activeIndexChange.emit(index);
3237
3324
  this.scrollToActiveTab(true);
3238
3325
  }
3326
+ badgeClass(tab) {
3327
+ return toneBadge(tab.tone());
3328
+ }
3239
3329
  /** Roving tablist keyboard nav: arrows move AND activate (selection follows focus). */
3240
3330
  onTabKeydown(event, index) {
3241
3331
  const count = this.tabs().length;
@@ -3263,9 +3353,52 @@ class TabGroupComponent {
3263
3353
  const buttons = this.tabListRef().nativeElement.querySelectorAll('.tab-btn');
3264
3354
  buttons[next]?.focus();
3265
3355
  }
3356
+ /** Parent-driven activeIndex (deep-link ?tab=, quick-links) must also move the pill. */
3357
+ ngOnChanges(changes) {
3358
+ if (changes['activeIndex'] && !changes['activeIndex'].firstChange) {
3359
+ setTimeout(() => this.scrollToActiveTab(false), 0);
3360
+ }
3361
+ }
3362
+ /**
3363
+ * Re-measure every view check: catches label-width changes (language switch,
3364
+ * count arriving) with cached geometry so the DOM write only happens when
3365
+ * the pill actually moved.
3366
+ */
3367
+ ngAfterViewChecked() {
3368
+ this.updatePagePill();
3369
+ }
3370
+ /** Slide the 'page' pill to the active tab (offset* is scroll-content relative). */
3371
+ updatePagePill() {
3372
+ const pillEl = this.pagePillRef()?.nativeElement;
3373
+ if (!pillEl)
3374
+ return;
3375
+ const tabButtons = this.tabListRef().nativeElement.querySelectorAll('.tab-btn');
3376
+ const activeTab = tabButtons[this.activeIndex];
3377
+ if (!activeTab)
3378
+ return;
3379
+ const left = activeTab.offsetLeft;
3380
+ const width = activeTab.offsetWidth;
3381
+ if (width === 0)
3382
+ return; // hidden (display:none ancestors) — keep last state
3383
+ if (left === this.pillState.left && width === this.pillState.width)
3384
+ return;
3385
+ // First paint positions without transition — no fly-in from x=0 on load.
3386
+ if (!this.pillState.painted) {
3387
+ this.renderer.setStyle(pillEl, 'transition', 'none');
3388
+ requestAnimationFrame(() => this.renderer.removeStyle(pillEl, 'transition'));
3389
+ this.pillState.painted = true;
3390
+ }
3391
+ this.renderer.setStyle(pillEl, 'width', `${width}px`);
3392
+ this.renderer.setStyle(pillEl, 'transform', `translateX(${left}px)`);
3393
+ this.pillState = { ...this.pillState, left, width };
3394
+ }
3266
3395
  updateUnderlinePosition() {
3396
+ // The 'page' variant slides .fx-tabs-pill instead (see updatePagePill);
3397
+ // the underline element only exists on the default variant.
3398
+ const underlineEl = this.underlineRef()?.nativeElement;
3399
+ if (!underlineEl)
3400
+ return;
3267
3401
  const tabListEl = this.tabListRef().nativeElement;
3268
- const underlineEl = this.underlineRef().nativeElement;
3269
3402
  const tabButtons = tabListEl.querySelectorAll('.tab-btn');
3270
3403
  const activeTab = tabButtons[this.activeIndex];
3271
3404
  if (!activeTab || !underlineEl)
@@ -3323,9 +3456,10 @@ class TabGroupComponent {
3323
3456
  }
3324
3457
  onResize() {
3325
3458
  this.updateUnderlinePosition();
3459
+ this.updatePagePill();
3326
3460
  }
3327
3461
  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 });
3462
+ 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
3463
  }
3330
3464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabGroupComponent, decorators: [{
3331
3465
  type: Component,
@@ -3334,10 +3468,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3334
3468
  NgClass,
3335
3469
  NgTemplateOutlet,
3336
3470
  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"] }]
3471
+ ], 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
3472
  }], ctorParameters: () => [{ type: i0.Renderer2 }], propDecorators: { tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TabComponent), { isSignal: true }] }], activeIndex: [{
3339
3473
  type: Input
3340
- }], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], underlineRef: [{ type: i0.ViewChild, args: ['underline', { isSignal: true }] }], onResize: [{
3474
+ }], 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
3475
  type: HostListener,
3342
3476
  args: ['window:resize']
3343
3477
  }] } });
@@ -3367,11 +3501,11 @@ class TagComponent {
3367
3501
  this.clicked.emit();
3368
3502
  }
3369
3503
  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 });
3504
+ 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
3505
  }
3372
3506
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TagComponent, decorators: [{
3373
3507
  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" }]
3508
+ 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
3509
  }], 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
3510
  type: Input
3377
3511
  }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }], clicked: [{
@@ -3464,11 +3598,11 @@ class TreeDiagram {
3464
3598
  this.cdr.detectChanges();
3465
3599
  }
3466
3600
  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 });
3601
+ 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
3602
  }
3469
3603
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TreeDiagram, decorators: [{
3470
3604
  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" }]
3605
+ 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
3606
  }], 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
3607
  type: ViewChildren,
3474
3608
  args: ['child']
@@ -3509,7 +3643,7 @@ class KanbanBoardComponent {
3509
3643
  });
3510
3644
  }
3511
3645
  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 });
3646
+ 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
3647
  }
3514
3648
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: KanbanBoardComponent, decorators: [{
3515
3649
  type: Component,
@@ -3518,7 +3652,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3518
3652
  CdkDrag,
3519
3653
  CdkDragPlaceholder,
3520
3654
  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"] }]
3655
+ ], 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
3656
  }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], itemTemplate: [{
3523
3657
  type: Input
3524
3658
  }], columnChanged: [{ type: i0.Output, args: ["columnChanged"] }] } });
@@ -3527,8 +3661,15 @@ class DrawerComponent {
3527
3661
  router;
3528
3662
  cdr = inject(ChangeDetectorRef);
3529
3663
  items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
3664
+ /** V2: bottom-pinned section (Administration group) — same item rendering,
3665
+ separated by a rule, above the [drawerFooter] slot and version. */
3666
+ footerItems = input([], ...(ngDevMode ? [{ debugName: "footerItems" }] : /* istanbul ignore next */ []));
3530
3667
  isExpanded = true;
3531
3668
  logo;
3669
+ /** Wordmark cạnh logo, chỉ hiện khi drawer mở. Bỏ trống = chỉ có mark. */
3670
+ logoText;
3671
+ /** Dòng phụ dưới logoText (tagline/workspace). Chỉ hiện khi drawer mở. */
3672
+ logoSubtext;
3532
3673
  version;
3533
3674
  toggleExpanded = output();
3534
3675
  itemClicked = output();
@@ -3620,6 +3761,7 @@ class DrawerComponent {
3620
3761
  return false;
3621
3762
  };
3622
3763
  expandParents(this.items());
3764
+ expandParents(this.footerItems());
3623
3765
  }
3624
3766
  getAccordionState(itemId) {
3625
3767
  return this.isAccordionExpanded(itemId) ? 'expanded' : 'collapsed';
@@ -3628,7 +3770,7 @@ class DrawerComponent {
3628
3770
  return this.isExpanded ? 'expanded' : 'collapsed';
3629
3771
  }
3630
3772
  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: [
3773
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DrawerComponent, isStandalone: true, selector: "fx-ui-drawer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, footerItems: { classPropertyName: "footerItems", publicName: "footerItems", isSignal: true, isRequired: false, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: false, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: false, isRequired: false, transformFunction: null }, logoText: { classPropertyName: "logoText", publicName: "logoText", isSignal: false, isRequired: false, transformFunction: null }, logoSubtext: { classPropertyName: "logoSubtext", publicName: "logoSubtext", isSignal: false, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { toggleExpanded: "toggleExpanded", itemClicked: "itemClicked" }, ngImport: i0, template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:11px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));text-transform:uppercase;-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], animations: [
3632
3774
  trigger('drawerExpand', [
3633
3775
  state('collapsed', style({ width: '70px' })),
3634
3776
  state('expanded', style({ width: '260px' })),
@@ -3655,16 +3797,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3655
3797
  transition('collapsed <=> expanded', animate('300ms cubic-bezier(0.4, 0, 0.2, 1)')),
3656
3798
  ]),
3657
3799
  ], changeDetection: ChangeDetectionStrategy.OnPush, imports: [
3658
- NgClass,
3659
3800
  NgTemplateOutlet,
3660
3801
  MatTooltip,
3661
3802
  HeroIconComponent,
3662
3803
  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: [{
3804
+ ], template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:11px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));text-transform:uppercase;-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3805
+ }], 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
3806
  type: Input
3666
3807
  }], logo: [{
3667
3808
  type: Input
3809
+ }], logoText: [{
3810
+ type: Input
3811
+ }], logoSubtext: [{
3812
+ type: Input
3668
3813
  }], version: [{
3669
3814
  type: Input
3670
3815
  }], toggleExpanded: [{ type: i0.Output, args: ["toggleExpanded"] }], itemClicked: [{ type: i0.Output, args: ["itemClicked"] }] } });
@@ -3703,9 +3848,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3703
3848
  args: [{ selector: 'fx-ui-skeleton-detail', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl animate-pulse\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg bg-bg-hover shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded bg-bg-hover\"></div>\n <div class=\"h-3 w-3/4 rounded bg-bg-hover\"></div>\n </div>\n </div>\n </div>\n\n @for (c of range(cards()); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded bg-bg-hover\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of range(rows()); track r) {\n <div class=\"h-3 rounded bg-bg-hover\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n" }]
3704
3849
  }], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
3705
3850
 
3851
+ /**
3852
+ * V2 sparkbar — inline mini bar-trend (risk trend on project cards, deltas in
3853
+ * stat grids). Pure DOM, token-coloured; no chart engine.
3854
+ *
3855
+ * <fx-ui-sparkbar [values]="[3, 5, 2, 8, 6]" />
3856
+ */
3857
+ class SparkbarComponent {
3858
+ values = input([], ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
3859
+ /** Bar colour; null renders the accent. */
3860
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3861
+ height = input(28, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
3862
+ bars = computed(() => {
3863
+ const values = this.values();
3864
+ const max = Math.max(...values, 1);
3865
+ // 8% floor so zero values still show a base mark.
3866
+ return values.map((v) => Math.max((v / max) * 100, 8));
3867
+ }, ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
3868
+ barClass() {
3869
+ return this.tone() ? toneBg(this.tone()) : 'bg-accent';
3870
+ }
3871
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SparkbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3872
+ 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 });
3873
+ }
3874
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SparkbarComponent, decorators: [{
3875
+ type: Component,
3876
+ 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" }]
3877
+ }], 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 }] }] } });
3878
+
3706
3879
  /** KPI tile row: big number + tinted icon box + caption, in a responsive grid. */
3707
3880
  class StatCardsComponent {
3708
3881
  cards = input([], ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
3882
+ /**
3883
+ * 'tile' (default): tinted icon box + number + caption — the pre-V2 look.
3884
+ * 'figure': the V2 design's KPI column — caption on top, large figure-font
3885
+ * number, toned sub-caption underneath (no icon box).
3886
+ */
3887
+ variant = input('tile', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
3709
3888
  /** Grid columns; defaults to one column per card. */
3710
3889
  cols;
3711
3890
  get gridColumns() {
@@ -3721,8 +3900,14 @@ class StatCardsComponent {
3721
3900
  boxClass(card) {
3722
3901
  return card.tone ? toneBadge(card.tone) : 'bg-bg-hover text-text-secondary';
3723
3902
  }
3903
+ deltaClass(card) {
3904
+ return card.delta?.tone ? toneText(card.delta.tone) : 'text-muted';
3905
+ }
3906
+ subClass(card) {
3907
+ return card.subTone ? toneText(card.subTone) : 'text-text-secondary';
3908
+ }
3724
3909
  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 });
3910
+ 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
3911
  }
3727
3912
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, decorators: [{
3728
3913
  type: Component,
@@ -3730,8 +3915,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3730
3915
  NgStyle,
3731
3916
  NgClass,
3732
3917
  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: [{
3918
+ SparkbarComponent,
3919
+ ], 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" }]
3920
+ }], 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
3921
  type: Input
3736
3922
  }] } });
3737
3923
 
@@ -3798,6 +3984,123 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3798
3984
  type: Input
3799
3985
  }] } });
3800
3986
 
3987
+ /**
3988
+ * V2 filter pills — the quick-filter row above ranked lists (design refs 02,
3989
+ * 03, 11): outlined capsules with a muted count, the active one filled with
3990
+ * the primary text colour.
3991
+ *
3992
+ * Distinct from `fx-ui-filter-chips`, which tints each chip by its severity
3993
+ * tone and carries no count. Presentational: the caller owns the filter state.
3994
+ *
3995
+ * <fx-ui-filter-pills [options]="pills" [value]="filters.status"
3996
+ * (valueChange)="filters.status = $event; refresh()" />
3997
+ */
3998
+ class FilterPillsComponent {
3999
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4000
+ /** Accepts undefined so screens can bind an optional filter-bag key. */
4001
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4002
+ valueChange = output();
4003
+ isActive(option) {
4004
+ return (this.value() ?? '') === option.value;
4005
+ }
4006
+ pillClass(option) {
4007
+ return this.isActive(option)
4008
+ ? 'bg-text-primary text-text-inverse border-text-primary'
4009
+ : 'border-border-default text-text-secondary hover:border-border-strong';
4010
+ }
4011
+ countClass(option) {
4012
+ return this.isActive(option) ? 'opacity-70' : 'text-text-placeholder';
4013
+ }
4014
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4015
+ 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 });
4016
+ }
4017
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, decorators: [{
4018
+ type: Component,
4019
+ 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" }]
4020
+ }], 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"] }] } });
4021
+
4022
+ /**
4023
+ * V2 stage progress dots — the walkthrough position indicator on the attack
4024
+ * chain toolbar (design refs 04, 07): one dot per stage, the current one drawn
4025
+ * as a wider accent bar, visited ones solid, the rest faint.
4026
+ *
4027
+ * <fx-ui-stage-dots [count]="8" [current]="stageIndex()"
4028
+ * (dotClick)="goToStage($event)" />
4029
+ */
4030
+ class StageDotsComponent {
4031
+ count = input(0, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
4032
+ /** 0-based current stage; -1 (default) means none selected yet. */
4033
+ current = input(-1, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
4034
+ /** Emit the clicked stage index; wire it to make the dots navigable. */
4035
+ dotClick = output();
4036
+ dots = computed(() => Array.from({ length: Math.max(0, this.count()) }, (_, i) => i), ...(ngDevMode ? [{ debugName: "dots" }] : /* istanbul ignore next */ []));
4037
+ dotClass(index) {
4038
+ const current = this.current();
4039
+ if (index === current)
4040
+ return 'w-5 bg-accent';
4041
+ if (current >= 0 && index < current)
4042
+ return 'w-1.5 bg-text-placeholder';
4043
+ return 'w-1.5 bg-border-strong';
4044
+ }
4045
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StageDotsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4046
+ 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 });
4047
+ }
4048
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StageDotsComponent, decorators: [{
4049
+ type: Component,
4050
+ 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" }]
4051
+ }], 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"] }] } });
4052
+
4053
+ /**
4054
+ * V2 evidence code block — the mono panel that carries a process tree, a
4055
+ * command line or any captured text (design ref 05, "Process tree").
4056
+ *
4057
+ * Whitespace is preserved and the box scrolls on its own rather than pushing
4058
+ * the page wide, so ASCII trees survive narrow viewports.
4059
+ *
4060
+ * <fx-ui-code-block [label]="'analyze.processTree' | translate" [code]="tree" />
4061
+ */
4062
+ class CodeBlockComponent {
4063
+ code = input('', ...(ngDevMode ? [{ debugName: "code" }] : /* istanbul ignore next */ []));
4064
+ /** Caller-translated caption above the box; omit for a bare block. */
4065
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4066
+ /** Scroll cap; empty grows with the content. */
4067
+ maxHeight = input('', ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
4068
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CodeBlockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4069
+ 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 });
4070
+ }
4071
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CodeBlockComponent, decorators: [{
4072
+ type: Component,
4073
+ 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" }]
4074
+ }], 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 }] }] } });
4075
+
4076
+ /**
4077
+ * V2 captured-evidence frame — a faux window chrome (traffic lights + mono file
4078
+ * name + timestamp) around a screenshot, with a placeholder when the image is
4079
+ * missing (design ref 05, "Screenshot").
4080
+ *
4081
+ * <fx-ui-screenshot-frame [label]="'analyze.screenshot' | translate"
4082
+ * fileName="chain-05.png" timestamp="09:42:18"
4083
+ * [src]="url" [caption]="caption" />
4084
+ */
4085
+ class ScreenshotFrameComponent {
4086
+ /** Caller-translated caption above the frame; omit for a bare frame. */
4087
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4088
+ fileName = input('', ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
4089
+ timestamp = input('', ...(ngDevMode ? [{ debugName: "timestamp" }] : /* istanbul ignore next */ []));
4090
+ /** Image URL; empty renders the placeholder body. */
4091
+ src = input('', ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
4092
+ /** Caller-translated line under (or inside) the frame. */
4093
+ caption = input('', ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
4094
+ alt = input('', ...(ngDevMode ? [{ debugName: "alt" }] : /* istanbul ignore next */ []));
4095
+ height = input('180px', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4096
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScreenshotFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4097
+ 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 });
4098
+ }
4099
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScreenshotFrameComponent, decorators: [{
4100
+ type: Component,
4101
+ 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" }]
4102
+ }], 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 }] }] } });
4103
+
3801
4104
  /** Thin tone-coloured progress/score bar with optional label and caption. */
3802
4105
  class MeterComponent {
3803
4106
  value = 0;
@@ -3830,26 +4133,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3830
4133
  type: Input
3831
4134
  }] } });
3832
4135
 
3833
- /** Detail-panel section: card with an icon + heading header and projected body. */
4136
+ /**
4137
+ * Panel section: card with a header and projected body.
4138
+ * - titleStyle 'heading' (default): icon + heading, the pre-V2 look
4139
+ * - titleStyle 'eyebrow': the V2 design's panel header — small tracked
4140
+ * caption on the left, muted [aside] text on the right
4141
+ */
3834
4142
  class SectionCardComponent {
3835
4143
  /** Caller-translated heading. */
3836
4144
  title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3837
4145
  /** hero-icon name; omitted → text-only header. */
3838
4146
  icon;
3839
4147
  iconTone = input(null, ...(ngDevMode ? [{ debugName: "iconTone" }] : /* istanbul ignore next */ []));
4148
+ titleStyle = input('heading', ...(ngDevMode ? [{ debugName: "titleStyle" }] : /* istanbul ignore next */ []));
4149
+ /** Caller-translated right-aligned hint (eyebrow style). */
4150
+ aside = input('', ...(ngDevMode ? [{ debugName: "aside" }] : /* istanbul ignore next */ []));
3840
4151
  get iconClass() {
3841
4152
  const iconTone = this.iconTone();
3842
4153
  return iconTone ? toneText(iconTone) : 'text-text-secondary';
3843
4154
  }
3844
4155
  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 });
4156
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span\n class=\"txt-table-header uppercase\"\n style=\"letter-spacing: var(--og-tracking-label)\"\n >{{ title() }}</span\n >\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3846
4157
  }
3847
4158
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, decorators: [{
3848
4159
  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" }]
4160
+ args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span\n class=\"txt-table-header uppercase\"\n style=\"letter-spacing: var(--og-tracking-label)\"\n >{{ title() }}</span\n >\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
3850
4161
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{
3851
4162
  type: Input
3852
- }], iconTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTone", required: false }] }] } });
4163
+ }], 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
4164
 
3854
4165
  /**
3855
4166
  * Label-over-value field for detail panels. The label uses txt-field-label
@@ -3938,11 +4249,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3938
4249
 
3939
4250
  let nextUid = 0;
3940
4251
  /**
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:
4252
+ * Master-detail shell driven by a BaseMasterDetailComponent. Two-column by
4253
+ * default; projecting a [mdFacets] block (V2 lookup screens) adds a third
4254
+ * facets-rail column before the list. The host page projects its own
4255
+ * header/filters and supplies templates for the parts that vary per entity:
3944
4256
  *
3945
4257
  * <fx-ui-master-detail [table]="this" [emptyMessage]="'noData' | translate">
4258
+ * <div mdFacets>…<fx-ui-facets-rail …/>…</div> <!-- optional, V2 -->
3946
4259
  * <div mdHeader>…title / stat cards…</div>
3947
4260
  * <div mdFilters>…search bar / filter chips…</div>
3948
4261
  * <ng-template #row let-item let-selected="selected">…row body…</ng-template>
@@ -3953,6 +4266,9 @@ let nextUid = 0;
3953
4266
  * tab stop, ArrowUp/Down/Home/End move the selection.
3954
4267
  */
3955
4268
  class MasterDetailComponent {
4269
+ hostEl = inject((ElementRef));
4270
+ /** True when the host projected a [mdFacets] block — switches to 3 columns. */
4271
+ hasFacets = false;
3956
4272
  table;
3957
4273
  /** Fixed list-pane width on lg+ viewports (stacked full-width below). */
3958
4274
  listWidth = input('420px', ...(ngDevMode ? [{ debugName: "listWidth" }] : /* istanbul ignore next */ []));
@@ -3962,10 +4278,20 @@ class MasterDetailComponent {
3962
4278
  emptyMessage = input('No data', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
3963
4279
  listLabel = input('', ...(ngDevMode ? [{ debugName: "listLabel" }] : /* istanbul ignore next */ []));
3964
4280
  skeletonCards = input(3, ...(ngDevMode ? [{ debugName: "skeletonCards" }] : /* istanbul ignore next */ []));
4281
+ /** 'prevnext' renders the V2 "X of Y" + Prev/Next controls instead of the
4282
+ Material paginator (which stays in the DOM, hidden — it is the paging
4283
+ engine BaseTable subscribes to). */
4284
+ paginationStyle = input('paginator', ...(ngDevMode ? [{ debugName: "paginationStyle" }] : /* istanbul ignore next */ []));
4285
+ /** Caller-translated Prev/Next labels ("X of Y" is numeric). */
4286
+ prevLabel = input('Prev', ...(ngDevMode ? [{ debugName: "prevLabel" }] : /* istanbul ignore next */ []));
4287
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
3965
4288
  rowTemplate = contentChild.required('row');
3966
4289
  detailTemplate = contentChild.required('detail');
3967
4290
  paginator = viewChild.required(MatPaginator);
3968
4291
  uid = `fx-md-${nextUid++}`;
4292
+ ngAfterContentInit() {
4293
+ this.hasFacets = !!this.hostEl.nativeElement.querySelector('[mdFacets]');
4294
+ }
3969
4295
  ngAfterViewInit() {
3970
4296
  // The paginator lives in this view, not the host page's, so the host's
3971
4297
  // @ViewChild(MatPaginator) never finds it. Hand it to the table so
@@ -3974,6 +4300,17 @@ class MasterDetailComponent {
3974
4300
  this.table.paginator = this.paginator();
3975
4301
  }
3976
4302
  }
4303
+ /** "X of Y" range text for the prevnext pagination style. */
4304
+ rangeText() {
4305
+ const total = this.table.total ?? 0;
4306
+ if (!total)
4307
+ return '0 of 0';
4308
+ const page = this.paginator().pageIndex;
4309
+ const size = this.table.pageSize;
4310
+ const from = page * size + 1;
4311
+ const to = Math.min((page + 1) * size, total);
4312
+ return `${from}–${to} of ${total}`;
4313
+ }
3977
4314
  isSelected(row) {
3978
4315
  return this.table.idOf(row) === this.table.selectedId;
3979
4316
  }
@@ -4012,7 +4349,7 @@ class MasterDetailComponent {
4012
4349
  el?.scrollIntoView({ block: 'nearest' });
4013
4350
  }
4014
4351
  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"] }] });
4352
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, maxListHeight: { classPropertyName: "maxListHeight", publicName: "maxListHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"flex flex-col lg:flex-row items-start gap-xl\" [style.--fx-md-list-width]=\"listWidth()\">\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div class=\"fx-md-facets card-common bg-surface\" [class.hidden]=\"!hasFacets\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs\"\n [style.maxHeight]=\"maxListHeight()\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"w-full lg:flex-1 min-w-0\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n</div>\n", styles: [".fx-md-list{width:100%}@media(min-width:1080px){.fx-md-list{width:var(--fx-md-list-width, 420px);flex-shrink:0}}.fx-md-facets{width:100%}@media(min-width:1080px){.fx-md-facets{width:230px;flex-shrink:0}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }] });
4016
4353
  }
4017
4354
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
4018
4355
  type: Component,
@@ -4023,10 +4360,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4023
4360
  EmptyStateComponent,
4024
4361
  MatPaginator,
4025
4362
  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"] }]
4363
+ ], template: "<div class=\"flex flex-col lg:flex-row items-start gap-xl\" [style.--fx-md-list-width]=\"listWidth()\">\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div class=\"fx-md-facets card-common bg-surface\" [class.hidden]=\"!hasFacets\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs\"\n [style.maxHeight]=\"maxListHeight()\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"w-full lg:flex-1 min-w-0\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n</div>\n", styles: [".fx-md-list{width:100%}@media(min-width:1080px){.fx-md-list{width:var(--fx-md-list-width, 420px);flex-shrink:0}}.fx-md-facets{width:100%}@media(min-width:1080px){.fx-md-facets{width:230px;flex-shrink:0}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"] }]
4027
4364
  }], propDecorators: { table: [{
4028
4365
  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 }] }] } });
4366
+ }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], maxListHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxListHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4030
4367
 
4031
4368
  /**
4032
4369
  * Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook
@@ -4066,6 +4403,433 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4066
4403
  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
4404
  }], 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
4405
 
4406
+ /**
4407
+ * V2 theme picker — dropdown các theme OgeniumDashboards cho chọn
4408
+ * (OG_PICKER_THEME_META: porcelain/charcoal), mỗi dòng có swatch accent +
4409
+ * page. Drives ThemeService.setTheme(); replaces the pre-V2 sun/moon toggle
4410
+ * (behavior-layer rule: mat-menu stays inside the lib, apps never import
4411
+ * @angular/material directly).
4412
+ *
4413
+ * <fx-ui-theme-picker />
4414
+ */
4415
+ class ThemePickerComponent {
4416
+ themeService = inject(ThemeService);
4417
+ themes = OG_PICKER_THEME_META;
4418
+ current = toSignal(this.themeService.themeChanges(), {
4419
+ initialValue: this.themeService.getCurrentTheme(),
4420
+ });
4421
+ /** Tra trên bộ ĐẦY ĐỦ: theme đang active có thể là một trong 4 theme đã ẩn
4422
+ khỏi picker (deep-link /themes, localStorage cũ) — nhãn vẫn phải đúng. */
4423
+ currentMeta() {
4424
+ return OG_THEME_META.find((t) => t.name === this.current()) ?? this.themes[0];
4425
+ }
4426
+ select(theme) {
4427
+ this.themeService.setTheme(theme);
4428
+ }
4429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4430
+ 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 });
4431
+ }
4432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemePickerComponent, decorators: [{
4433
+ type: Component,
4434
+ 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" }]
4435
+ }] });
4436
+
4437
+ /**
4438
+ * "Waiting on the backend" marker (anh Tú 29/08).
4439
+ *
4440
+ * Put this on any UI the V2 design calls for but no endpoint feeds yet, so the
4441
+ * gap is visible in the product instead of living only in a backlog file.
4442
+ * `grep -r "fx-ui-pending-badge"` then lists every outstanding field.
4443
+ *
4444
+ * <fx-ui-pending-badge [field]="'confirmed-chains weekly series'" />
4445
+ *
4446
+ * Keep the copy factual: it states that data is missing, never fakes a value.
4447
+ */
4448
+ class PendingBadgeComponent {
4449
+ /** Caller-translated chip text. */
4450
+ label = input('Pending API', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4451
+ /**
4452
+ * What exactly is missing — shown as the title tooltip so a reviewer can read
4453
+ * the required field without opening the code.
4454
+ */
4455
+ field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4456
+ /** 'chip' (default) sits inline; 'dot' is a bare marker for dense rows. */
4457
+ variant = input('chip', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4458
+ get tooltip() {
4459
+ return this.field() ? `${this.label()} — ${this.field()}` : this.label();
4460
+ }
4461
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4462
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4463
+ }
4464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, decorators: [{
4465
+ type: Component,
4466
+ args: [{ selector: 'fx-ui-pending-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n" }]
4467
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4468
+
4469
+ /**
4470
+ * Wraps UI that is rendered from PLACEHOLDER data so the layout can be
4471
+ * reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
4472
+ * trước để review").
4473
+ *
4474
+ * The frame makes the status unmistakable — dashed outline, a corner label and
4475
+ * the pending marker naming the missing field — so sample shapes can never be
4476
+ * mistaken for measured values. Drop the wrapper (and the sample input) the
4477
+ * moment the API lands.
4478
+ *
4479
+ * The pending chip is inlined rather than composed from fx-ui-pending-badge:
4480
+ * one lib component importing another inside the same barrel tripped NG0919
4481
+ * (metadata read before definition) in consuming apps.
4482
+ *
4483
+ * <fx-ui-sample-frame [field]="'weekly activity'" [label]="'Sample layout'">
4484
+ * …chart rendered from sample series…
4485
+ * </fx-ui-sample-frame>
4486
+ */
4487
+ class SampleFrameComponent {
4488
+ /** Caller-translated corner label. */
4489
+ label = input('Sample layout', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4490
+ /** Field the backend still owes — shown on the pending badge tooltip. */
4491
+ field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4492
+ /** Caller-translated pending-chip text. */
4493
+ pendingLabel = input('Pending API', ...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
4494
+ get tooltip() {
4495
+ return this.field() ? `${this.pendingLabel()} — ${this.field()}` : this.pendingLabel();
4496
+ }
4497
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4498
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4499
+ }
4500
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, decorators: [{
4501
+ type: Component,
4502
+ args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
4503
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], pendingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingLabel", required: false }] }] } });
4504
+
4505
+ /**
4506
+ * V2 score badge — the risk/score chip used on attack-path rows, dashboard
4507
+ * panels and project tiles. Colour comes from the 0–100 risk band unless an
4508
+ * explicit tone is given (bands: ≥90 critical, ≥70 danger, ≥40 warning, else
4509
+ * success — same thresholds as value-tone's scoreTone on a 0–100 scale).
4510
+ */
4511
+ class ScoreBadgeComponent {
4512
+ score = input.required(...(ngDevMode ? [{ debugName: "score" }] : /* istanbul ignore next */ []));
4513
+ /** Denominator shown as "98 / 100"; omit for a bare score. */
4514
+ outOf = input(null, ...(ngDevMode ? [{ debugName: "outOf" }] : /* istanbul ignore next */ []));
4515
+ /** Overrides the band-derived tone. */
4516
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4517
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4518
+ resolvedTone = computed(() => {
4519
+ const explicit = this.tone();
4520
+ if (explicit)
4521
+ return explicit;
4522
+ const value = Number(this.score());
4523
+ if (!isFinite(value))
4524
+ return null;
4525
+ if (value >= 90)
4526
+ return 'critical';
4527
+ if (value >= 70)
4528
+ return 'danger';
4529
+ if (value >= 40)
4530
+ return 'warning';
4531
+ return 'success';
4532
+ }, ...(ngDevMode ? [{ debugName: "resolvedTone" }] : /* istanbul ignore next */ []));
4533
+ badgeClass() {
4534
+ return toneBadge(this.resolvedTone());
4535
+ }
4536
+ sizeClass() {
4537
+ switch (this.size()) {
4538
+ case 'sm':
4539
+ return 'text-xs px-normal py-[1px]';
4540
+ case 'lg':
4541
+ return 'text-2xl px-semi py-small font-semibold';
4542
+ default:
4543
+ return 'text-sm px-semi py-xs font-semibold';
4544
+ }
4545
+ }
4546
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScoreBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4547
+ 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 });
4548
+ }
4549
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScoreBadgeComponent, decorators: [{
4550
+ type: Component,
4551
+ 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" }]
4552
+ }], 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 }] }] } });
4553
+
4554
+ /**
4555
+ * V2 StatusDot — tiny tone-coloured indicator used in list rows, project
4556
+ * cards and stat grids. Optional pulse for "live" states (running scans).
4557
+ *
4558
+ * <fx-ui-status-dot tone="success" />
4559
+ * <fx-ui-status-dot tone="warning" [pulse]="true" />
4560
+ */
4561
+ class StatusDotComponent {
4562
+ tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4563
+ /** Diameter in px. */
4564
+ size = input(8, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4565
+ pulse = input(false, ...(ngDevMode ? [{ debugName: "pulse" }] : /* istanbul ignore next */ []));
4566
+ dotClass() {
4567
+ return toneBg(this.tone());
4568
+ }
4569
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatusDotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4570
+ 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 });
4571
+ }
4572
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatusDotComponent, decorators: [{
4573
+ type: Component,
4574
+ 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" }]
4575
+ }], 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 }] }] } });
4576
+
4577
+ /**
4578
+ * V2 segmented control — mode toggles (Essentials/Advanced), Catalog/Table
4579
+ * view switches, sub-navigation inside a tab. Generic n-option control; the
4580
+ * semantics live with the caller.
4581
+ *
4582
+ * Rendered as a compact pill group (anh Tú 29/08): equal-width options inside
4583
+ * a rounded-full track, with a single "thumb" that SLIDES to the active
4584
+ * option (transform transition — no per-button background repaint).
4585
+ *
4586
+ * <fx-ui-segmented [options]="modes" [(value)]="mode" />
4587
+ */
4588
+ class SegmentedComponent {
4589
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4590
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4591
+ /** Accessible name for the group. */
4592
+ ariaLabel = input('View mode', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
4593
+ /**
4594
+ * Both variants share the pill shape; they differ in the thumb fill:
4595
+ * 'default': raised surface thumb — view switches (Catalog/Table, ref 10).
4596
+ * 'pill': accent-filled thumb — the Essential/Advanced depth toggle
4597
+ * (design refs 04/05).
4598
+ */
4599
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4600
+ /** Index the sliding thumb sits on; -1 hides it (no/unknown selection). */
4601
+ activeIndex = computed(() => this.options().findIndex((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4602
+ /** Equal-width tracks → the thumb is 1/n of the inner width and slides by
4603
+ * multiples of its own width. Pure CSS transform, nothing to measure. */
4604
+ thumbWidth = computed(() => `calc((100% - 6px) / ${this.options().length || 1})`, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4605
+ thumbTransform = computed(() => `translateX(${this.activeIndex() * 100}%)`, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
4606
+ select(option) {
4607
+ if (option.disabled || option.value === this.value())
4608
+ return;
4609
+ this.value.set(option.value);
4610
+ }
4611
+ thumbClass() {
4612
+ return this.variant() === 'pill'
4613
+ ? 'bg-accent'
4614
+ : 'bg-surface-raised border border-border shadow-sm';
4615
+ }
4616
+ optionClass(option) {
4617
+ if (option.value !== this.value()) {
4618
+ return 'text-muted hover:text-strong';
4619
+ }
4620
+ return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
4621
+ }
4622
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4623
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4624
+ }
4625
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
4626
+ type: Component,
4627
+ args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
4628
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4629
+
4630
+ /**
4631
+ * V2 ListRow — presentational body for master-detail #row templates and other
4632
+ * selectable lists: status dot + title, trailing badges, meta line. Extra
4633
+ * content projects below the meta line.
4634
+ *
4635
+ * <ng-template #row let-item>
4636
+ * <fx-ui-list-row
4637
+ * [title]="item.name"
4638
+ * [meta]="item.version + ' · ' + item.date"
4639
+ * [dotTone]="statusTone(item)"
4640
+ * [badges]="[{ label: item.severity, tone: severityTone(item.severity) }]"
4641
+ * />
4642
+ * </ng-template>
4643
+ */
4644
+ class ListRowComponent {
4645
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
4646
+ meta = input('', ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
4647
+ badges = input([], ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
4648
+ dotTone = input(undefined, ...(ngDevMode ? [{ debugName: "dotTone" }] : /* istanbul ignore next */ []));
4649
+ dotPulse = input(false, ...(ngDevMode ? [{ debugName: "dotPulse" }] : /* istanbul ignore next */ []));
4650
+ badgeClass(badge) {
4651
+ return toneBadge(badge.tone ?? null);
4652
+ }
4653
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ListRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4654
+ 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 });
4655
+ }
4656
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ListRowComponent, decorators: [{
4657
+ type: Component,
4658
+ 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" }]
4659
+ }], 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 }] }] } });
4660
+
4661
+ /**
4662
+ * V2 BarList — ranked horizontal bars ("Top projects by risk", "Top critical
4663
+ * attack paths"). Pure DOM (accessible, token-native, clickable) — charts via
4664
+ * ECharts are for time series, not rankings.
4665
+ *
4666
+ * <fx-ui-bar-list [items]="topRisk" (select)="open($event)" />
4667
+ */
4668
+ class BarListComponent {
4669
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
4670
+ /** Rows render as buttons only when true (a (select) listener exists). */
4671
+ interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
4672
+ select = output();
4673
+ max = computed(() => Math.max(...this.items().map((i) => i.value), 1), ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
4674
+ widthOf(item) {
4675
+ return Math.max((item.value / this.max()) * 100, 2);
4676
+ }
4677
+ barClass(item) {
4678
+ return item.tone ? toneBg(item.tone) : 'bg-accent';
4679
+ }
4680
+ onSelect(item) {
4681
+ if (this.interactive())
4682
+ this.select.emit(item);
4683
+ }
4684
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BarListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4685
+ 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 });
4686
+ }
4687
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BarListComponent, decorators: [{
4688
+ type: Component,
4689
+ 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" }]
4690
+ }], 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"] }] } });
4691
+
4692
+ /**
4693
+ * V2 facets rail — the first column of 3-column lookup screens: filter groups
4694
+ * with counts + a Reset link. Presentational: filtering itself stays in the
4695
+ * page (feed the emitted selection into BaseMasterDetail filters).
4696
+ *
4697
+ * <div mdFacets>
4698
+ * <fx-ui-facets-rail [groups]="facetGroups" [selection]="facets"
4699
+ * (selectionChange)="onFacets($event)" />
4700
+ * </div>
4701
+ */
4702
+ class FacetsRailComponent {
4703
+ groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
4704
+ selection = input({}, ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
4705
+ /** Caller-translated label for the reset affordance. */
4706
+ resetLabel = input('Reset', ...(ngDevMode ? [{ debugName: "resetLabel" }] : /* istanbul ignore next */ []));
4707
+ selectionChange = output();
4708
+ hasSelection = computed(() => Object.values(this.selection()).some((values) => values?.length), ...(ngDevMode ? [{ debugName: "hasSelection" }] : /* istanbul ignore next */ []));
4709
+ isSelected(group, option) {
4710
+ return (this.selection()[group.id] ?? []).includes(option.value);
4711
+ }
4712
+ toggle(group, option) {
4713
+ const current = this.selection()[group.id] ?? [];
4714
+ let next;
4715
+ if (group.multi) {
4716
+ next = current.includes(option.value)
4717
+ ? current.filter((v) => v !== option.value)
4718
+ : [...current, option.value];
4719
+ }
4720
+ else {
4721
+ next = current.includes(option.value) ? [] : [option.value];
4722
+ }
4723
+ this.selectionChange.emit({ ...this.selection(), [group.id]: next });
4724
+ }
4725
+ reset() {
4726
+ this.selectionChange.emit({});
4727
+ }
4728
+ toneClass(option) {
4729
+ return option.tone ? toneText(option.tone) : '';
4730
+ }
4731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4732
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FacetsRailComponent, isStandalone: true, selector: "fx-ui-facets-rail", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, resetLabel: { classPropertyName: "resetLabel", publicName: "resetLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, ngImport: i0, template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div\n class=\"txt-table-header uppercase mb-small\"\n [style.letter-spacing]=\"'var(--og-tracking-label)'\"\n >\n {{ group.label }}\n </div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: StatusDotComponent, selector: "fx-ui-status-dot", inputs: ["tone", "size", "pulse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4733
+ }
4734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, decorators: [{
4735
+ type: Component,
4736
+ args: [{ selector: 'fx-ui-facets-rail', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, StatusDotComponent], template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div\n class=\"txt-table-header uppercase mb-small\"\n [style.letter-spacing]=\"'var(--og-tracking-label)'\"\n >\n {{ group.label }}\n </div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n" }]
4737
+ }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], resetLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "resetLabel", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
4738
+
4739
+ /** Trend blocks are softened solids — whole literals (Tailwind v4 has no safelist). */
4740
+ const TREND_BLOCK = {
4741
+ critical: 'bg-critical/60',
4742
+ danger: 'bg-danger/60',
4743
+ warning: 'bg-warning/60',
4744
+ success: 'bg-success/60',
4745
+ information: 'bg-information/60',
4746
+ };
4747
+ /**
4748
+ * V2 project card — the Project Management catalog tile (design 12): initials
4749
+ * avatar + name + schedule line, toned score chip, risk-trend blocks, metric
4750
+ * columns and a "Last analysis · … / Open ›" footer.
4751
+ *
4752
+ * <fx-ui-project-card [card]="project" (open)="goTo(project)" />
4753
+ */
4754
+ class ProjectCardComponent {
4755
+ card = input.required(...(ngDevMode ? [{ debugName: "card" }] : /* istanbul ignore next */ []));
4756
+ open = output();
4757
+ scoreChipClass() {
4758
+ return toneBadge(this.card().scoreTone ?? null);
4759
+ }
4760
+ chipClass(chip) {
4761
+ return toneBadge(chip.tone ?? null);
4762
+ }
4763
+ blockClass(tone) {
4764
+ return tone ? TREND_BLOCK[tone] : 'bg-bg-hover';
4765
+ }
4766
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4767
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4768
+ }
4769
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, decorators: [{
4770
+ type: Component,
4771
+ args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
4772
+ }], propDecorators: { card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: true }] }], open: [{ type: i0.Output, args: ["open"] }] } });
4773
+
4774
+ /**
4775
+ * V2 pipeline stepper — the New Analysis 7-stage pipeline header. Renders the
4776
+ * stage strip only; the page owns the stage bodies (project the current one
4777
+ * below). Stage states are data — the parent decides what is done/active.
4778
+ *
4779
+ * <fx-ui-pipeline-stepper [stages]="stages" (stageClick)="goTo($event)">
4780
+ * …current stage body…
4781
+ * </fx-ui-pipeline-stepper>
4782
+ */
4783
+ class PipelineStepperComponent {
4784
+ stages = input([], ...(ngDevMode ? [{ debugName: "stages" }] : /* istanbul ignore next */ []));
4785
+ /** Only done stages are click-navigable (review a completed step). */
4786
+ clickable = input(true, ...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
4787
+ stageClick = output();
4788
+ stateOf(stage) {
4789
+ return stage.state ?? 'pending';
4790
+ }
4791
+ circleClass(stage) {
4792
+ switch (this.stateOf(stage)) {
4793
+ case 'done':
4794
+ return 'bg-accent text-accent-contrast border-accent';
4795
+ case 'active':
4796
+ return 'bg-accent-soft text-accent-soft-fg border-accent';
4797
+ case 'error':
4798
+ return 'bg-critical/15 text-critical border-critical';
4799
+ default:
4800
+ return 'bg-surface text-faint border-border';
4801
+ }
4802
+ }
4803
+ labelClass(stage) {
4804
+ switch (this.stateOf(stage)) {
4805
+ case 'active':
4806
+ return 'text-strong font-semibold';
4807
+ case 'done':
4808
+ return 'text-body';
4809
+ case 'error':
4810
+ return 'text-critical';
4811
+ default:
4812
+ return 'text-faint';
4813
+ }
4814
+ }
4815
+ connectorClass(stage) {
4816
+ return this.stateOf(stage) === 'done' ? 'bg-accent' : 'bg-border';
4817
+ }
4818
+ canClick(stage) {
4819
+ return this.clickable() && this.stateOf(stage) === 'done';
4820
+ }
4821
+ onStageClick(stage, index) {
4822
+ if (this.canClick(stage))
4823
+ this.stageClick.emit(index);
4824
+ }
4825
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PipelineStepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4826
+ 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 });
4827
+ }
4828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PipelineStepperComponent, decorators: [{
4829
+ type: Component,
4830
+ args: [{ selector: 'fx-ui-pipeline-stepper', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<ol class=\"flex items-start overflow-x-auto pb-small\" role=\"list\">\n @for (stage of stages(); track stage.id; let i = $index; let last = $last) {\n <li class=\"flex items-start\" [class.flex-1]=\"!last\" [attr.aria-current]=\"stateOf(stage) === 'active' ? 'step' : null\">\n <button\n type=\"button\"\n class=\"flex flex-col items-center gap-xs min-w-20 px-xs\"\n [class.cursor-pointer]=\"canClick(stage)\"\n [class.cursor-default]=\"!canClick(stage)\"\n [tabindex]=\"canClick(stage) ? 0 : -1\"\n (click)=\"onStageClick(stage, i)\"\n >\n <span\n class=\"w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-semibold transition-colors\"\n [ngClass]=\"circleClass(stage)\"\n >\n @switch (stateOf(stage)) {\n @case ('done') {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex\" />\n }\n @case ('error') {\n <fx-ui-hero-icon icon=\"exclamation-triangle\" [size]=\"16\" class=\"flex\" />\n }\n @default {\n {{ i + 1 }}\n }\n }\n </span>\n <span class=\"text-xs text-center whitespace-nowrap\" [ngClass]=\"labelClass(stage)\">\n {{ stage.label }}\n </span>\n @if (stage.description) {\n <span class=\"txt-utility-description text-center whitespace-nowrap\">\n {{ stage.description }}\n </span>\n }\n </button>\n @if (!last) {\n <div class=\"flex-1 h-[2px] mt-4 mx-xs rounded-pill min-w-6\" [ngClass]=\"connectorClass(stage)\"></div>\n }\n </li>\n }\n</ol>\n<div class=\"mt-large\">\n <ng-content></ng-content>\n</div>\n" }]
4831
+ }], propDecorators: { stages: [{ type: i0.Input, args: [{ isSignal: true, alias: "stages", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], stageClick: [{ type: i0.Output, args: ["stageClick"] }] } });
4832
+
4069
4833
  const FX_COMPONENTS = [
4070
4834
  InputComponent,
4071
4835
  SelectComponent,
@@ -4096,12 +4860,28 @@ const FX_COMPONENTS = [
4096
4860
  StatCardsComponent,
4097
4861
  EmptyStateComponent,
4098
4862
  FilterChipsComponent,
4863
+ FilterPillsComponent,
4864
+ StageDotsComponent,
4865
+ CodeBlockComponent,
4866
+ ScreenshotFrameComponent,
4099
4867
  MeterComponent,
4100
4868
  SectionCardComponent,
4101
4869
  InfoFieldComponent,
4102
4870
  SeverityBadgesComponent,
4103
4871
  MasterDetailComponent,
4104
4872
  MenuComponent,
4873
+ ThemePickerComponent,
4874
+ PendingBadgeComponent,
4875
+ SampleFrameComponent,
4876
+ ScoreBadgeComponent,
4877
+ StatusDotComponent,
4878
+ SparkbarComponent,
4879
+ SegmentedComponent,
4880
+ ListRowComponent,
4881
+ BarListComponent,
4882
+ FacetsRailComponent,
4883
+ ProjectCardComponent,
4884
+ PipelineStepperComponent,
4105
4885
  ];
4106
4886
 
4107
4887
  const DIALOGS_COMPONENT = [
@@ -4125,14 +4905,14 @@ class UiModule {
4125
4905
  ReactiveFormsModule,
4126
4906
  RouterModule,
4127
4907
  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,
4908
+ TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, SkeletonTableLoadingComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, ConfirmationDialogComponent], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, SkeletonTableLoadingComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, ConfirmationDialogComponent, FormsModule,
4129
4909
  ReactiveFormsModule,
4130
4910
  RouterModule,
4131
4911
  HasPermissionDirective,
4132
4912
  TrimOnBlurDirective] });
4133
4913
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiModule, imports: [FormsModule,
4134
4914
  ReactiveFormsModule,
4135
- RouterModule, SelectComponent, DatetimePicker, MasterDetailComponent, MenuComponent, FormsModule,
4915
+ RouterModule, SelectComponent, DatetimePicker, MasterDetailComponent, MenuComponent, ThemePickerComponent, FormsModule,
4136
4916
  ReactiveFormsModule,
4137
4917
  RouterModule] });
4138
4918
  }
@@ -4168,5 +4948,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4168
4948
  * Generated bundle index. Do not edit.
4169
4949
  */
4170
4950
 
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 };
4951
+ export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseResolver, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxDialogService, FxLoadingService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, ListRowComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, OG_LIGHT_THEMES, OG_PICKER_THEMES, OG_PICKER_THEME_META, OG_THEMES, OG_THEME_META, PendingBadgeComponent, PermissionGuard, PermissionService, PipelineStepperComponent, ProjectCardComponent, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDetailComponent, SkeletonListComponent, SkeletonTableLoadingComponent, SliderComponent, SparkbarComponent, StageDotsComponent, StatCardsComponent, StatusDotComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemePickerComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, anyFeatureEnabled, countTone, createApi, featureEnabled, fxListResolver, scoreTone, severityTone, toneBadge, toneBg, toneColor, toneText };
4172
4952
  //# sourceMappingURL=foxeltech-angular-ui.mjs.map