@neural-ui/core 1.2.0 → 1.3.0

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 (239) hide show
  1. package/README.md +56 -88
  2. package/accordion/package.json +4 -0
  3. package/alert/package.json +4 -0
  4. package/autocomplete/package.json +4 -0
  5. package/avatar/package.json +4 -0
  6. package/badge/package.json +4 -0
  7. package/block-ui/package.json +4 -0
  8. package/breadcrumb/package.json +4 -0
  9. package/button/package.json +4 -0
  10. package/card/package.json +4 -0
  11. package/chart/package.json +4 -0
  12. package/checkbox/package.json +4 -0
  13. package/chip/package.json +4 -0
  14. package/code-block/package.json +4 -0
  15. package/color-picker/package.json +4 -0
  16. package/command-palette/package.json +4 -0
  17. package/confirm-dialog/package.json +4 -0
  18. package/context-menu/package.json +4 -0
  19. package/dashboard-grid/package.json +4 -0
  20. package/date-input/package.json +4 -0
  21. package/divider/package.json +4 -0
  22. package/empty-state/package.json +4 -0
  23. package/fesm2022/neural-ui-core-accordion.mjs +162 -0
  24. package/fesm2022/neural-ui-core-accordion.mjs.map +1 -0
  25. package/fesm2022/neural-ui-core-alert.mjs +116 -0
  26. package/fesm2022/neural-ui-core-alert.mjs.map +1 -0
  27. package/fesm2022/neural-ui-core-autocomplete.mjs +332 -0
  28. package/fesm2022/neural-ui-core-autocomplete.mjs.map +1 -0
  29. package/fesm2022/neural-ui-core-avatar.mjs +109 -0
  30. package/fesm2022/neural-ui-core-avatar.mjs.map +1 -0
  31. package/fesm2022/neural-ui-core-badge.mjs +54 -0
  32. package/fesm2022/neural-ui-core-badge.mjs.map +1 -0
  33. package/fesm2022/neural-ui-core-block-ui.mjs +95 -0
  34. package/fesm2022/neural-ui-core-block-ui.mjs.map +1 -0
  35. package/fesm2022/neural-ui-core-breadcrumb.mjs +84 -0
  36. package/fesm2022/neural-ui-core-breadcrumb.mjs.map +1 -0
  37. package/fesm2022/neural-ui-core-button.mjs +125 -0
  38. package/fesm2022/neural-ui-core-button.mjs.map +1 -0
  39. package/fesm2022/neural-ui-core-card.mjs +69 -0
  40. package/fesm2022/neural-ui-core-card.mjs.map +1 -0
  41. package/fesm2022/neural-ui-core-chart.mjs +287 -0
  42. package/fesm2022/neural-ui-core-chart.mjs.map +1 -0
  43. package/fesm2022/neural-ui-core-checkbox.mjs +138 -0
  44. package/fesm2022/neural-ui-core-checkbox.mjs.map +1 -0
  45. package/fesm2022/neural-ui-core-chip.mjs +130 -0
  46. package/fesm2022/neural-ui-core-chip.mjs.map +1 -0
  47. package/fesm2022/neural-ui-core-code-block.mjs +250 -0
  48. package/fesm2022/neural-ui-core-code-block.mjs.map +1 -0
  49. package/fesm2022/neural-ui-core-color-picker.mjs +435 -0
  50. package/fesm2022/neural-ui-core-color-picker.mjs.map +1 -0
  51. package/fesm2022/neural-ui-core-command-palette.mjs +235 -0
  52. package/fesm2022/neural-ui-core-command-palette.mjs.map +1 -0
  53. package/fesm2022/neural-ui-core-confirm-dialog.mjs +118 -0
  54. package/fesm2022/neural-ui-core-confirm-dialog.mjs.map +1 -0
  55. package/fesm2022/neural-ui-core-context-menu.mjs +158 -0
  56. package/fesm2022/neural-ui-core-context-menu.mjs.map +1 -0
  57. package/fesm2022/neural-ui-core-dashboard-grid.mjs +144 -0
  58. package/fesm2022/neural-ui-core-dashboard-grid.mjs.map +1 -0
  59. package/fesm2022/neural-ui-core-date-input.mjs +1332 -0
  60. package/fesm2022/neural-ui-core-date-input.mjs.map +1 -0
  61. package/fesm2022/neural-ui-core-divider.mjs +54 -0
  62. package/fesm2022/neural-ui-core-divider.mjs.map +1 -0
  63. package/fesm2022/neural-ui-core-empty-state.mjs +84 -0
  64. package/fesm2022/neural-ui-core-empty-state.mjs.map +1 -0
  65. package/fesm2022/neural-ui-core-filter-bar.mjs +118 -0
  66. package/fesm2022/neural-ui-core-filter-bar.mjs.map +1 -0
  67. package/fesm2022/neural-ui-core-icon.mjs +50 -0
  68. package/fesm2022/neural-ui-core-icon.mjs.map +1 -0
  69. package/fesm2022/neural-ui-core-image-viewer.mjs +309 -0
  70. package/fesm2022/neural-ui-core-image-viewer.mjs.map +1 -0
  71. package/fesm2022/neural-ui-core-input-otp.mjs +192 -0
  72. package/fesm2022/neural-ui-core-input-otp.mjs.map +1 -0
  73. package/fesm2022/neural-ui-core-input.mjs +320 -0
  74. package/fesm2022/neural-ui-core-input.mjs.map +1 -0
  75. package/fesm2022/neural-ui-core-knob.mjs +323 -0
  76. package/fesm2022/neural-ui-core-knob.mjs.map +1 -0
  77. package/fesm2022/neural-ui-core-meter-group.mjs +122 -0
  78. package/fesm2022/neural-ui-core-meter-group.mjs.map +1 -0
  79. package/fesm2022/neural-ui-core-modal.mjs +156 -0
  80. package/fesm2022/neural-ui-core-modal.mjs.map +1 -0
  81. package/fesm2022/neural-ui-core-multiselect.mjs +748 -0
  82. package/fesm2022/neural-ui-core-multiselect.mjs.map +1 -0
  83. package/fesm2022/neural-ui-core-nav.mjs +952 -0
  84. package/fesm2022/neural-ui-core-nav.mjs.map +1 -0
  85. package/fesm2022/neural-ui-core-notification-center.mjs +264 -0
  86. package/fesm2022/neural-ui-core-notification-center.mjs.map +1 -0
  87. package/fesm2022/neural-ui-core-number-input.mjs +331 -0
  88. package/fesm2022/neural-ui-core-number-input.mjs.map +1 -0
  89. package/fesm2022/neural-ui-core-pagination.mjs +198 -0
  90. package/fesm2022/neural-ui-core-pagination.mjs.map +1 -0
  91. package/fesm2022/neural-ui-core-popover.mjs +207 -0
  92. package/fesm2022/neural-ui-core-popover.mjs.map +1 -0
  93. package/fesm2022/neural-ui-core-progress-bar.mjs +105 -0
  94. package/fesm2022/neural-ui-core-progress-bar.mjs.map +1 -0
  95. package/fesm2022/neural-ui-core-radio.mjs +171 -0
  96. package/fesm2022/neural-ui-core-radio.mjs.map +1 -0
  97. package/fesm2022/neural-ui-core-rating.mjs +151 -0
  98. package/fesm2022/neural-ui-core-rating.mjs.map +1 -0
  99. package/fesm2022/neural-ui-core-select.mjs +638 -0
  100. package/fesm2022/neural-ui-core-select.mjs.map +1 -0
  101. package/fesm2022/neural-ui-core-sidebar.mjs +214 -0
  102. package/fesm2022/neural-ui-core-sidebar.mjs.map +1 -0
  103. package/fesm2022/neural-ui-core-skeleton.mjs +40 -0
  104. package/fesm2022/neural-ui-core-skeleton.mjs.map +1 -0
  105. package/fesm2022/neural-ui-core-slider.mjs +146 -0
  106. package/fesm2022/neural-ui-core-slider.mjs.map +1 -0
  107. package/fesm2022/neural-ui-core-spinner.mjs +113 -0
  108. package/fesm2022/neural-ui-core-spinner.mjs.map +1 -0
  109. package/fesm2022/neural-ui-core-split-button.mjs +252 -0
  110. package/fesm2022/neural-ui-core-split-button.mjs.map +1 -0
  111. package/fesm2022/neural-ui-core-splitter.mjs +174 -0
  112. package/fesm2022/neural-ui-core-splitter.mjs.map +1 -0
  113. package/fesm2022/neural-ui-core-stats-card.mjs +163 -0
  114. package/fesm2022/neural-ui-core-stats-card.mjs.map +1 -0
  115. package/fesm2022/neural-ui-core-stepper.mjs +204 -0
  116. package/fesm2022/neural-ui-core-stepper.mjs.map +1 -0
  117. package/fesm2022/neural-ui-core-switch.mjs +111 -0
  118. package/fesm2022/neural-ui-core-switch.mjs.map +1 -0
  119. package/fesm2022/neural-ui-core-table.mjs +1860 -0
  120. package/fesm2022/neural-ui-core-table.mjs.map +1 -0
  121. package/fesm2022/neural-ui-core-tabs.mjs +246 -0
  122. package/fesm2022/neural-ui-core-tabs.mjs.map +1 -0
  123. package/fesm2022/neural-ui-core-textarea.mjs +188 -0
  124. package/fesm2022/neural-ui-core-textarea.mjs.map +1 -0
  125. package/fesm2022/neural-ui-core-timeline.mjs +117 -0
  126. package/fesm2022/neural-ui-core-timeline.mjs.map +1 -0
  127. package/fesm2022/neural-ui-core-toast.mjs +171 -0
  128. package/fesm2022/neural-ui-core-toast.mjs.map +1 -0
  129. package/fesm2022/neural-ui-core-toggle-button-group.mjs +162 -0
  130. package/fesm2022/neural-ui-core-toggle-button-group.mjs.map +1 -0
  131. package/fesm2022/neural-ui-core-toolbar.mjs +67 -0
  132. package/fesm2022/neural-ui-core-toolbar.mjs.map +1 -0
  133. package/fesm2022/neural-ui-core-tooltip.mjs +151 -0
  134. package/fesm2022/neural-ui-core-tooltip.mjs.map +1 -0
  135. package/fesm2022/neural-ui-core-url-state.mjs +96 -0
  136. package/fesm2022/neural-ui-core-url-state.mjs.map +1 -0
  137. package/fesm2022/neural-ui-core-virtual-list.mjs +126 -0
  138. package/fesm2022/neural-ui-core-virtual-list.mjs.map +1 -0
  139. package/fesm2022/neural-ui-core.mjs +11 -8544
  140. package/fesm2022/neural-ui-core.mjs.map +1 -1
  141. package/filter-bar/package.json +4 -0
  142. package/icon/package.json +4 -0
  143. package/image-viewer/package.json +4 -0
  144. package/input/package.json +4 -0
  145. package/input-otp/package.json +4 -0
  146. package/knob/package.json +4 -0
  147. package/meter-group/package.json +4 -0
  148. package/modal/package.json +4 -0
  149. package/multiselect/package.json +4 -0
  150. package/nav/package.json +4 -0
  151. package/notification-center/package.json +4 -0
  152. package/number-input/package.json +4 -0
  153. package/package.json +252 -5
  154. package/pagination/package.json +4 -0
  155. package/popover/package.json +4 -0
  156. package/progress-bar/package.json +4 -0
  157. package/radio/package.json +4 -0
  158. package/rating/package.json +4 -0
  159. package/select/package.json +4 -0
  160. package/sidebar/package.json +4 -0
  161. package/skeleton/package.json +4 -0
  162. package/slider/package.json +4 -0
  163. package/spinner/package.json +4 -0
  164. package/split-button/package.json +4 -0
  165. package/splitter/package.json +4 -0
  166. package/stats-card/package.json +4 -0
  167. package/stepper/package.json +4 -0
  168. package/styles/_tokens.scss +209 -7
  169. package/styles.scss +1 -0
  170. package/switch/package.json +4 -0
  171. package/table/package.json +4 -0
  172. package/tabs/package.json +4 -0
  173. package/textarea/package.json +4 -0
  174. package/timeline/package.json +4 -0
  175. package/toast/package.json +4 -0
  176. package/toggle-button-group/package.json +4 -0
  177. package/toolbar/package.json +4 -0
  178. package/tooltip/package.json +4 -0
  179. package/types/neural-ui-core-accordion.d.ts +55 -0
  180. package/types/neural-ui-core-alert.d.ts +47 -0
  181. package/types/neural-ui-core-autocomplete.d.ts +69 -0
  182. package/types/neural-ui-core-avatar.d.ts +39 -0
  183. package/types/neural-ui-core-badge.d.ts +36 -0
  184. package/types/neural-ui-core-block-ui.d.ts +46 -0
  185. package/types/neural-ui-core-breadcrumb.d.ts +38 -0
  186. package/types/neural-ui-core-button.d.ts +55 -0
  187. package/types/neural-ui-core-card.d.ts +37 -0
  188. package/types/neural-ui-core-chart.d.ts +236 -0
  189. package/types/neural-ui-core-checkbox.d.ts +33 -0
  190. package/types/neural-ui-core-chip.d.ts +53 -0
  191. package/types/neural-ui-core-code-block.d.ts +55 -0
  192. package/types/neural-ui-core-color-picker.d.ts +55 -0
  193. package/types/neural-ui-core-command-palette.d.ts +56 -0
  194. package/types/neural-ui-core-confirm-dialog.d.ts +50 -0
  195. package/types/neural-ui-core-context-menu.d.ts +66 -0
  196. package/types/neural-ui-core-dashboard-grid.d.ts +41 -0
  197. package/types/neural-ui-core-date-input.d.ts +178 -0
  198. package/types/neural-ui-core-divider.d.ts +20 -0
  199. package/types/neural-ui-core-empty-state.d.ts +32 -0
  200. package/types/neural-ui-core-filter-bar.d.ts +49 -0
  201. package/types/neural-ui-core-icon.d.ts +33 -0
  202. package/types/neural-ui-core-image-viewer.d.ts +67 -0
  203. package/types/neural-ui-core-input-otp.d.ts +49 -0
  204. package/types/neural-ui-core-input.d.ts +86 -0
  205. package/types/neural-ui-core-knob.d.ts +68 -0
  206. package/types/neural-ui-core-meter-group.d.ts +52 -0
  207. package/types/neural-ui-core-modal.d.ts +54 -0
  208. package/types/neural-ui-core-multiselect.d.ts +129 -0
  209. package/types/neural-ui-core-nav.d.ts +69 -0
  210. package/types/neural-ui-core-notification-center.d.ts +60 -0
  211. package/types/neural-ui-core-number-input.d.ts +63 -0
  212. package/types/neural-ui-core-pagination.d.ts +30 -0
  213. package/types/neural-ui-core-popover.d.ts +73 -0
  214. package/types/neural-ui-core-progress-bar.d.ts +35 -0
  215. package/types/neural-ui-core-radio.d.ts +51 -0
  216. package/types/neural-ui-core-rating.d.ts +34 -0
  217. package/types/neural-ui-core-select.d.ts +161 -0
  218. package/types/neural-ui-core-sidebar.d.ts +57 -0
  219. package/types/neural-ui-core-skeleton.d.ts +22 -0
  220. package/types/neural-ui-core-slider.d.ts +42 -0
  221. package/types/neural-ui-core-spinner.d.ts +38 -0
  222. package/types/neural-ui-core-split-button.d.ts +65 -0
  223. package/types/neural-ui-core-splitter.d.ts +28 -0
  224. package/types/neural-ui-core-stats-card.d.ts +39 -0
  225. package/types/neural-ui-core-stepper.d.ts +51 -0
  226. package/types/neural-ui-core-switch.d.ts +34 -0
  227. package/types/neural-ui-core-table.d.ts +282 -0
  228. package/types/neural-ui-core-tabs.d.ts +76 -0
  229. package/types/neural-ui-core-textarea.d.ts +52 -0
  230. package/types/neural-ui-core-timeline.d.ts +33 -0
  231. package/types/neural-ui-core-toast.d.ts +70 -0
  232. package/types/neural-ui-core-toggle-button-group.d.ts +63 -0
  233. package/types/neural-ui-core-toolbar.d.ts +36 -0
  234. package/types/neural-ui-core-tooltip.d.ts +48 -0
  235. package/types/neural-ui-core-url-state.d.ts +58 -0
  236. package/types/neural-ui-core-virtual-list.d.ts +60 -0
  237. package/types/neural-ui-core.d.ts +5 -2107
  238. package/url-state/package.json +4 -0
  239. package/virtual-list/package.json +4 -0
@@ -0,0 +1,1860 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, TemplateRef, Directive, DestroyRef, PLATFORM_ID, contentChild, input, output, signal, computed, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
3
+ import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
4
+ import * as i1 from '@angular/forms';
5
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import { NeuUrlStateService } from '@neural-ui/core/url-state';
8
+ import { NeuInputComponent } from '@neural-ui/core/input';
9
+ import { NeuSelectComponent } from '@neural-ui/core/select';
10
+ import { NeuDateInputComponent } from '@neural-ui/core/date-input';
11
+ import { NeuIconComponent } from '@neural-ui/core/icon';
12
+
13
+ /**
14
+ * Directiva para definir el template de expansión de fila en NeuTable.
15
+ *
16
+ * Uso:
17
+ * ```html
18
+ * <neu-table [expandable]="true" [columns]="cols" [data]="rows">
19
+ * <ng-template neuTableExpand let-row>
20
+ * <div>{{ row.details }}</div>
21
+ * </ng-template>
22
+ * </neu-table>
23
+ * ```
24
+ */
25
+ class NeuTableExpandDirective {
26
+ templateRef = inject(TemplateRef);
27
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NeuTableExpandDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
28
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.8", type: NeuTableExpandDirective, isStandalone: true, selector: "ng-template[neuTableExpand]", ngImport: i0 });
29
+ }
30
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NeuTableExpandDirective, decorators: [{
31
+ type: Directive,
32
+ args: [{
33
+ selector: 'ng-template[neuTableExpand]',
34
+ standalone: true,
35
+ }]
36
+ }] });
37
+
38
+ // Conversor interno — el input acepta object[] para compatibilidad con
39
+ // interfaces de usuario concretas (User, Product…). Internamente casteamos a Row.
40
+ function asRows(data) {
41
+ return data;
42
+ }
43
+ /**
44
+ * NeuralUI Table — Componente Estrella
45
+ *
46
+ * Tabla de datos con:
47
+ * - Búsqueda/filtrado → ?q=...
48
+ * - Paginación → ?page=...
49
+ * - Ordenación → ?sort=col&sortDir=asc|desc
50
+ * - Selección de filas (múltiple) opcional
51
+ * - Skeleton de carga animado
52
+ * - Scroll horizontal elegante en mobile (<400px)
53
+ *
54
+ * Uso básico:
55
+ * <neu-table [columns]="cols" [data]="rows" />
56
+ *
57
+ * Múltiples tablas por página:
58
+ * <neu-table pageParam="userPage" searchParam="userQ" sortParam="userSort" ... />
59
+ */
60
+ class NeuTableComponent {
61
+ _destroyRef = inject(DestroyRef);
62
+ _urlState = inject(NeuUrlStateService);
63
+ _platformId = inject(PLATFORM_ID);
64
+ expandTemplate = contentChild(NeuTableExpandDirective, ...(ngDevMode ? [{ debugName: "expandTemplate" }] : /* istanbul ignore next */ []));
65
+ // ── Inputs de datos ─────────────────────────────────────────────────
66
+ columns = input([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
67
+ data = input([], ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
68
+ pageSize = input(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []));
69
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
70
+ title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
71
+ emptyMessage = input('No results found', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
72
+ skeletonRows = input([1, 2, 3, 4, 5], ...(ngDevMode ? [{ debugName: "skeletonRows" }] : /* istanbul ignore next */ []));
73
+ // ── Inputs de funcionalidad ──────────────────────────────────────────
74
+ searchable = input(true, ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
75
+ searchPlaceholder = input('Search...', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
76
+ exactMatchable = input(false, ...(ngDevMode ? [{ debugName: "exactMatchable" }] : /* istanbul ignore next */ []));
77
+ exactMatchLabel = input('Exact match', ...(ngDevMode ? [{ debugName: "exactMatchLabel" }] : /* istanbul ignore next */ []));
78
+ searchAriaLabel = input('Search table', ...(ngDevMode ? [{ debugName: "searchAriaLabel" }] : /* istanbul ignore next */ []));
79
+ clearSearchAriaLabel = input('Clear search', ...(ngDevMode ? [{ debugName: "clearSearchAriaLabel" }] : /* istanbul ignore next */ []));
80
+ clearFilterLabel = input('Clear filter', ...(ngDevMode ? [{ debugName: "clearFilterLabel" }] : /* istanbul ignore next */ []));
81
+ previousPageAriaLabel = input('Previous page', ...(ngDevMode ? [{ debugName: "previousPageAriaLabel" }] : /* istanbul ignore next */ []));
82
+ nextPageAriaLabel = input('Next page', ...(ngDevMode ? [{ debugName: "nextPageAriaLabel" }] : /* istanbul ignore next */ []));
83
+ pageSizeLabel = input('Rows:', ...(ngDevMode ? [{ debugName: "pageSizeLabel" }] : /* istanbul ignore next */ []));
84
+ pageSizeAriaLabel = input('Rows per page', ...(ngDevMode ? [{ debugName: "pageSizeAriaLabel" }] : /* istanbul ignore next */ []));
85
+ paginationAriaLabel = input('Pagination', ...(ngDevMode ? [{ debugName: "paginationAriaLabel" }] : /* istanbul ignore next */ []));
86
+ exportCsvTitle = input('Export CSV', ...(ngDevMode ? [{ debugName: "exportCsvTitle" }] : /* istanbul ignore next */ []));
87
+ exportJsonTitle = input('Export JSON', ...(ngDevMode ? [{ debugName: "exportJsonTitle" }] : /* istanbul ignore next */ []));
88
+ clearSelectionLabel = input('Clear selection', ...(ngDevMode ? [{ debugName: "clearSelectionLabel" }] : /* istanbul ignore next */ []));
89
+ selectionSummaryLabel = input('selected', ...(ngDevMode ? [{ debugName: "selectionSummaryLabel" }] : /* istanbul ignore next */ []));
90
+ tableAriaLabel = input('Data table', ...(ngDevMode ? [{ debugName: "tableAriaLabel" }] : /* istanbul ignore next */ []));
91
+ selectAllAriaLabel = input('Select all filtered rows', ...(ngDevMode ? [{ debugName: "selectAllAriaLabel" }] : /* istanbul ignore next */ []));
92
+ selectRowAriaLabel = input('Select row', ...(ngDevMode ? [{ debugName: "selectRowAriaLabel" }] : /* istanbul ignore next */ []));
93
+ expandRowAriaLabel = input('Expand row', ...(ngDevMode ? [{ debugName: "expandRowAriaLabel" }] : /* istanbul ignore next */ []));
94
+ filterPlaceholder = input('Filter...', ...(ngDevMode ? [{ debugName: "filterPlaceholder" }] : /* istanbul ignore next */ []));
95
+ filterAriaPrefix = input('Filter by', ...(ngDevMode ? [{ debugName: "filterAriaPrefix" }] : /* istanbul ignore next */ []));
96
+ allFilterOptionLabel = input('All', ...(ngDevMode ? [{ debugName: "allFilterOptionLabel" }] : /* istanbul ignore next */ []));
97
+ ofLabel = input('of', ...(ngDevMode ? [{ debugName: "ofLabel" }] : /* istanbul ignore next */ []));
98
+ resultLabelSingular = input('result', ...(ngDevMode ? [{ debugName: "resultLabelSingular" }] : /* istanbul ignore next */ []));
99
+ resultLabelPlural = input('results', ...(ngDevMode ? [{ debugName: "resultLabelPlural" }] : /* istanbul ignore next */ []));
100
+ sortable = input(false, ...(ngDevMode ? [{ debugName: "sortable" }] : /* istanbul ignore next */ []));
101
+ selectable = input(false, ...(ngDevMode ? [{ debugName: "selectable" }] : /* istanbul ignore next */ []));
102
+ expandable = input(false, ...(ngDevMode ? [{ debugName: "expandable" }] : /* istanbul ignore next */ []));
103
+ exportable = input(false, ...(ngDevMode ? [{ debugName: "exportable" }] : /* istanbul ignore next */ []));
104
+ exportFileName = input('export', ...(ngDevMode ? [{ debugName: "exportFileName" }] : /* istanbul ignore next */ []));
105
+ pageSizeOptions = input([], ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : /* istanbul ignore next */ []));
106
+ stickyHeader = input(false, ...(ngDevMode ? [{ debugName: "stickyHeader" }] : /* istanbul ignore next */ []));
107
+ rowKey = input('id', ...(ngDevMode ? [{ debugName: "rowKey" }] : /* istanbul ignore next */ []));
108
+ // ── Inputs nuevos v1.3.0 ──────────────────────────────────────────────
109
+ density = input('normal', ...(ngDevMode ? [{ debugName: "density" }] : /* istanbul ignore next */ []));
110
+ showRowNumbers = input(false, ...(ngDevMode ? [{ debugName: "showRowNumbers" }] : /* istanbul ignore next */ []));
111
+ rowClass = input(undefined, ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
112
+ footerRow = input(undefined, ...(ngDevMode ? [{ debugName: "footerRow" }] : /* istanbul ignore next */ []));
113
+ emptyStateTemplate = input(undefined, ...(ngDevMode ? [{ debugName: "emptyStateTemplate" }] : /* istanbul ignore next */ []));
114
+ serverSide = input(false, ...(ngDevMode ? [{ debugName: "serverSide" }] : /* istanbul ignore next */ []));
115
+ totalItems = input(undefined, ...(ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []));
116
+ multiSort = input(false, ...(ngDevMode ? [{ debugName: "multiSort" }] : /* istanbul ignore next */ []));
117
+ exportFormats = input(['csv'], ...(ngDevMode ? [{ debugName: "exportFormats" }] : /* istanbul ignore next */ []));
118
+ exportColumns = input([], ...(ngDevMode ? [{ debugName: "exportColumns" }] : /* istanbul ignore next */ []));
119
+ // ── URL params ────────────────────────────────────────────────────────
120
+ pageParam = input('page', ...(ngDevMode ? [{ debugName: "pageParam" }] : /* istanbul ignore next */ []));
121
+ searchParam = input('q', ...(ngDevMode ? [{ debugName: "searchParam" }] : /* istanbul ignore next */ []));
122
+ sortParam = input('sort', ...(ngDevMode ? [{ debugName: "sortParam" }] : /* istanbul ignore next */ []));
123
+ sortDirParam = input('sortDir', ...(ngDevMode ? [{ debugName: "sortDirParam" }] : /* istanbul ignore next */ []));
124
+ /** Param used to persist multi-sort state in the URL / Param para persistir multisort en la URL */
125
+ multiSortParam = input('msort', ...(ngDevMode ? [{ debugName: "multiSortParam" }] : /* istanbul ignore next */ []));
126
+ /**
127
+ * Activa o desactiva la sincronización de estado con los queryParams de la URL.
128
+ * Cuando es false, la paginación, búsqueda y ordenación se gestionan con
129
+ * señales internas sin tocar la URL del navegador.
130
+ *
131
+ * Enables or disables URL query-param synchronization.
132
+ * When false, pagination, search and sort are managed with internal signals
133
+ * and the browser URL is never modified.
134
+ */
135
+ useUrlState = input(true, ...(ngDevMode ? [{ debugName: "useUrlState" }] : /* istanbul ignore next */ []));
136
+ // ── Outputs ───────────────────────────────────────────────────────────
137
+ selectionChange = output();
138
+ rowClick = output();
139
+ rowDblClick = output();
140
+ actionClick = output();
141
+ serverStateChange = output();
142
+ searchChange = output();
143
+ // ── Estado interno (usado cuando useUrlState = false) ─────────────────
144
+ // Internal state signals (used when useUrlState = false)
145
+ _internalPage = signal(1, ...(ngDevMode ? [{ debugName: "_internalPage" }] : /* istanbul ignore next */ []));
146
+ _internalSearch = signal('', ...(ngDevMode ? [{ debugName: "_internalSearch" }] : /* istanbul ignore next */ []));
147
+ _internalSortKey = signal('', ...(ngDevMode ? [{ debugName: "_internalSortKey" }] : /* istanbul ignore next */ []));
148
+ _internalSortDir = signal('asc', ...(ngDevMode ? [{ debugName: "_internalSortDir" }] : /* istanbul ignore next */ []));
149
+ _internalMultiSort = signal('', ...(ngDevMode ? [{ debugName: "_internalMultiSort" }] : /* istanbul ignore next */ []));
150
+ // ── URL State ─────────────────────────────────────────────────────────
151
+ currentPage = computed(() => {
152
+ if (!this.useUrlState())
153
+ return this._internalPage();
154
+ const raw = this._urlState.getParam(this.pageParam())();
155
+ const n = Number(raw);
156
+ return !raw || isNaN(n) || n < 1 ? 1 : n;
157
+ }, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
158
+ searchQuery = computed(() => {
159
+ if (!this.useUrlState())
160
+ return this._internalSearch();
161
+ return this._urlState.getParam(this.searchParam())() ?? '';
162
+ }, ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
163
+ sortKey = computed(() => {
164
+ if (!this.useUrlState())
165
+ return this._internalSortKey();
166
+ return this._urlState.getParam(this.sortParam())() ?? '';
167
+ }, ...(ngDevMode ? [{ debugName: "sortKey" }] : /* istanbul ignore next */ []));
168
+ sortDir = computed(() => {
169
+ if (!this.useUrlState())
170
+ return this._internalSortDir();
171
+ const d = this._urlState.getParam(this.sortDirParam())();
172
+ return d === 'desc' ? 'desc' : 'asc';
173
+ }, ...(ngDevMode ? [{ debugName: "sortDir" }] : /* istanbul ignore next */ []));
174
+ // ---- Pipeline de datos / Data pipeline ----
175
+ rows = computed(() => asRows(this.data()), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
176
+ // ── Estado interno ────────────────────────────────────────────────────
177
+ _exactMatch = signal(false, ...(ngDevMode ? [{ debugName: "_exactMatch" }] : /* istanbul ignore next */ []));
178
+ exactMatch = this._exactMatch.asReadonly();
179
+ /**
180
+ * Multi-sort entries derived from URL param or internal state.
181
+ * Entradas de multisort derivadas del param de URL o del estado interno.
182
+ */
183
+ _sortEntries = computed(() => {
184
+ if (!this.multiSort())
185
+ return [];
186
+ const param = this.useUrlState()
187
+ ? this._urlState.getParam(this.multiSortParam())()
188
+ : this._internalMultiSort();
189
+ if (!param)
190
+ return [];
191
+ return param.split(',').flatMap((chunk) => {
192
+ const [key, dir] = chunk.split(':');
193
+ if (!key)
194
+ return [];
195
+ return [{ key, dir: (dir === 'desc' ? 'desc' : 'asc') }];
196
+ });
197
+ }, ...(ngDevMode ? [{ debugName: "_sortEntries" }] : /* istanbul ignore next */ []));
198
+ _columnFilters = signal({}, ...(ngDevMode ? [{ debugName: "_columnFilters" }] : /* istanbul ignore next */ []));
199
+ _pageSizeControl = new FormControl('', { nonNullable: true });
200
+ _columnFilterControls = new Map();
201
+ /** True when at least one column has filterable:true / True si alguna columna tiene filterable:true */
202
+ _hasFilterableCol = computed(() => this.columns().some((c) => c.filterable), ...(ngDevMode ? [{ debugName: "_hasFilterableCol" }] : /* istanbul ignore next */ []));
203
+ /** Convierte filterOptions de string[] a NeuSelectOption[] con opción "Todos" al inicio.
204
+ * Converts filterOptions from string[] to NeuSelectOption[] with a leading "All" option. */
205
+ _filterOpts(col) {
206
+ return [
207
+ { label: this.allFilterOptionLabel(), value: '' },
208
+ ...(col.filterOptions ?? []).map((o) => ({ label: o, value: o })),
209
+ ];
210
+ }
211
+ _pageSizeOptions = computed(() => this.pageSizeOptions().map((size) => ({ label: String(size), value: String(size) })), ...(ngDevMode ? [{ debugName: "_pageSizeOptions" }] : /* istanbul ignore next */ []));
212
+ constructor() {
213
+ this._pageSizeControl.valueChanges
214
+ .pipe(takeUntilDestroyed(this._destroyRef))
215
+ .subscribe((value) => this.onPageSizeChange(value));
216
+ effect(() => {
217
+ const nextSize = String(this.effectivePageSize());
218
+ if (this._pageSizeControl.value !== nextSize) {
219
+ this._pageSizeControl.setValue(nextSize, { emitEvent: false });
220
+ }
221
+ });
222
+ effect(() => {
223
+ const filters = this._columnFilters();
224
+ for (const col of this.columns()) {
225
+ if (!col.filterable)
226
+ continue;
227
+ const control = this.columnFilterControl(col.key);
228
+ const nextValue = String(filters[col.key] ?? '');
229
+ if (control.value !== nextValue) {
230
+ control.setValue(nextValue, { emitEvent: false });
231
+ }
232
+ }
233
+ });
234
+ }
235
+ _confirmPending = signal(null, ...(ngDevMode ? [{ debugName: "_confirmPending" }] : /* istanbul ignore next */ []));
236
+ // ── Pipeline de datos ─────────────────────────────────────────────────
237
+ _rows = computed(() => asRows(this.data()), ...(ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []));
238
+ filteredData = computed(() => {
239
+ if (this.serverSide())
240
+ return this._rows();
241
+ const rows = this._rows();
242
+ const colFilters = this._columnFilters();
243
+ const q = this.searchQuery().trim().toLowerCase();
244
+ const exact = this._exactMatch();
245
+ const matches = (text) => exact ? text.toLowerCase() === q : text.toLowerCase().includes(q);
246
+ return rows.filter((row) => {
247
+ const passesGlobal = !q ||
248
+ this.columns().some((col) => {
249
+ const rawVal = String(row[col.key] ?? '');
250
+ const displayVal = col.cell
251
+ ? col.cell(row)
252
+ : col.type === 'badge' && col.badgeMap?.[rawVal]?.label
253
+ ? col.badgeMap[rawVal].label
254
+ : rawVal;
255
+ return matches(displayVal) || matches(rawVal);
256
+ });
257
+ if (!passesGlobal)
258
+ return false;
259
+ for (const [colKey, filterVal] of Object.entries(colFilters)) {
260
+ if (filterVal === null || filterVal === '' || filterVal === undefined)
261
+ continue;
262
+ if (!String(row[colKey] ?? '')
263
+ .toLowerCase()
264
+ .includes(String(filterVal).toLowerCase()))
265
+ return false;
266
+ }
267
+ return true;
268
+ });
269
+ }, ...(ngDevMode ? [{ debugName: "filteredData" }] : /* istanbul ignore next */ []));
270
+ sortedData = computed(() => {
271
+ if (this.serverSide())
272
+ return this.filteredData();
273
+ const data = [...this.filteredData()];
274
+ if (!this.sortable())
275
+ return data;
276
+ if (this.multiSort() && this._sortEntries().length > 0) {
277
+ const entries = this._sortEntries();
278
+ return data.sort((a, b) => {
279
+ for (const entry of entries) {
280
+ const cmp = String(a[entry.key] ?? '').localeCompare(String(b[entry.key] ?? ''), undefined, { numeric: true });
281
+ if (cmp !== 0)
282
+ return entry.dir === 'asc' ? cmp : -cmp;
283
+ }
284
+ return 0;
285
+ });
286
+ }
287
+ const key = this.sortKey();
288
+ if (!key)
289
+ return data;
290
+ const dir = this.sortDir();
291
+ return data.sort((a, b) => {
292
+ const cmp = String(a[key] ?? '').localeCompare(String(b[key] ?? ''), undefined, {
293
+ numeric: true,
294
+ });
295
+ return dir === 'asc' ? cmp : -cmp;
296
+ });
297
+ }, ...(ngDevMode ? [{ debugName: "sortedData" }] : /* istanbul ignore next */ []));
298
+ _dynamicPageSize = signal(null, ...(ngDevMode ? [{ debugName: "_dynamicPageSize" }] : /* istanbul ignore next */ []));
299
+ effectivePageSize = computed(() => this._dynamicPageSize() ?? this.pageSize(), ...(ngDevMode ? [{ debugName: "effectivePageSize" }] : /* istanbul ignore next */ []));
300
+ totalPages = computed(() => {
301
+ const total = this.serverSide() && this.totalItems() != null
302
+ ? this.totalItems()
303
+ : this.filteredData().length;
304
+ return Math.max(1, Math.ceil(total / this.effectivePageSize()));
305
+ }, ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
306
+ paginatedData = computed(() => {
307
+ if (this.serverSide())
308
+ return this._rows();
309
+ const page = Math.min(this.currentPage(), this.totalPages());
310
+ const size = this.effectivePageSize();
311
+ return this.sortedData().slice((page - 1) * size, page * size);
312
+ }, ...(ngDevMode ? [{ debugName: "paginatedData" }] : /* istanbul ignore next */ []));
313
+ pageNumbers = computed(() => {
314
+ const total = this.totalPages();
315
+ const current = this.currentPage();
316
+ let start = Math.max(1, current - 2);
317
+ const end = Math.min(total, start + 4);
318
+ start = Math.max(1, end - 4);
319
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
320
+ }, ...(ngDevMode ? [{ debugName: "pageNumbers" }] : /* istanbul ignore next */ []));
321
+ paginationInfo = computed(() => {
322
+ const total = this.serverSide() && this.totalItems() != null
323
+ ? this.totalItems()
324
+ : this.filteredData().length;
325
+ const page = Math.min(this.currentPage(), this.totalPages());
326
+ const size = this.effectivePageSize();
327
+ const from = Math.min((page - 1) * size + 1, total);
328
+ const to = Math.min(page * size, total);
329
+ return `${from}\u2013${to} ${this.ofLabel()} ${total} ${total === 1 ? this.resultLabelSingular() : this.resultLabelPlural()}`;
330
+ }, ...(ngDevMode ? [{ debugName: "paginationInfo" }] : /* istanbul ignore next */ []));
331
+ totalColspan = computed(() => {
332
+ let cols = this.columns().length;
333
+ if (this.selectable())
334
+ cols++;
335
+ if (this.expandable())
336
+ cols++;
337
+ if (this.showRowNumbers())
338
+ cols++;
339
+ return cols;
340
+ }, ...(ngDevMode ? [{ debugName: "totalColspan" }] : /* istanbul ignore next */ []));
341
+ /** Calcula el offset izquierdo acumulado para columnas frozen-left múltiples.
342
+ * Calculates cumulative left offset for multiple frozen-left columns. */
343
+ _frozenLeftOffsets = computed(() => {
344
+ const offsets = new Map();
345
+ let leftPx = 0;
346
+ for (const col of this.columns()) {
347
+ if (col.frozen === 'left') {
348
+ offsets.set(col.key, leftPx);
349
+ if (col.width && col.width !== 'auto') {
350
+ const px = parseFloat(col.width);
351
+ if (!isNaN(px))
352
+ leftPx += px;
353
+ }
354
+ }
355
+ }
356
+ return offsets;
357
+ }, ...(ngDevMode ? [{ debugName: "_frozenLeftOffsets" }] : /* istanbul ignore next */ []));
358
+ _lastFrozenLeftKey = computed(() => {
359
+ const frozenLeft = this.columns().filter((col) => col.frozen === 'left');
360
+ return frozenLeft.at(-1)?.key ?? null;
361
+ }, ...(ngDevMode ? [{ debugName: "_lastFrozenLeftKey" }] : /* istanbul ignore next */ []));
362
+ _firstFrozenRightKey = computed(() => {
363
+ const frozenRight = this.columns().filter((col) => col.frozen === 'right');
364
+ return frozenRight[0]?.key ?? null;
365
+ }, ...(ngDevMode ? [{ debugName: "_firstFrozenRightKey" }] : /* istanbul ignore next */ []));
366
+ isLastFrozenLeftColumn(key) {
367
+ return this._lastFrozenLeftKey() === key;
368
+ }
369
+ isFirstFrozenRightColumn(key) {
370
+ return this._firstFrozenRightKey() === key;
371
+ }
372
+ // ── Expansión de filas ────────────────────────────────────────────────
373
+ _expandedKeys = signal(new Set(), ...(ngDevMode ? [{ debugName: "_expandedKeys" }] : /* istanbul ignore next */ []));
374
+ isRowExpanded(row) {
375
+ return this._expandedKeys().has(this.getRowKey(row));
376
+ }
377
+ toggleExpand(row) {
378
+ const key = this.getRowKey(row);
379
+ const set = new Set(this._expandedKeys());
380
+ if (set.has(key))
381
+ set.delete(key);
382
+ else
383
+ set.add(key);
384
+ this._expandedKeys.set(set);
385
+ }
386
+ // ── Selección ─────────────────────────────────────────────────────────
387
+ _selectedKeys = signal(new Set(), ...(ngDevMode ? [{ debugName: "_selectedKeys" }] : /* istanbul ignore next */ []));
388
+ selectedCount = computed(() => this._selectedKeys().size, ...(ngDevMode ? [{ debugName: "selectedCount" }] : /* istanbul ignore next */ []));
389
+ selectedRowsInfo = computed(() => `${this.selectedCount()} ${this.selectionSummaryLabel()}`, ...(ngDevMode ? [{ debugName: "selectedRowsInfo" }] : /* istanbul ignore next */ []));
390
+ /**
391
+ * TRUE cuando TODOS los registros que pasan el filtro activo están seleccionados.
392
+ * A diferencia de una selección global, actúa solo sobre el subconjunto filtrado.
393
+ */
394
+ isAllFilteredSelected = computed(() => {
395
+ const filtered = this.filteredData();
396
+ return (filtered.length > 0 && filtered.every((r) => this._selectedKeys().has(this.getRowKey(r))));
397
+ }, ...(ngDevMode ? [{ debugName: "isAllFilteredSelected" }] : /* istanbul ignore next */ []));
398
+ isSomeFilteredSelected = computed(() => this.filteredData().some((r) => this._selectedKeys().has(this.getRowKey(r))) &&
399
+ !this.isAllFilteredSelected(), ...(ngDevMode ? [{ debugName: "isSomeFilteredSelected" }] : /* istanbul ignore next */ []));
400
+ isRowSelected(row) {
401
+ return this._selectedKeys().has(this.getRowKey(row));
402
+ }
403
+ toggleRow(row) {
404
+ const key = this.getRowKey(row);
405
+ const set = new Set(this._selectedKeys());
406
+ if (set.has(key))
407
+ set.delete(key);
408
+ else
409
+ set.add(key);
410
+ this._selectedKeys.set(set);
411
+ this._emitSelection(set);
412
+ }
413
+ /** Selecciona/deselecciona SOLO los datos filtrados activos. */
414
+ toggleAll() {
415
+ const set = new Set(this._selectedKeys());
416
+ if (this.isAllFilteredSelected()) {
417
+ this.filteredData().forEach((r) => set.delete(this.getRowKey(r)));
418
+ }
419
+ else {
420
+ this.filteredData().forEach((r) => set.add(this.getRowKey(r)));
421
+ }
422
+ this._selectedKeys.set(set);
423
+ this._emitSelection(set);
424
+ }
425
+ clearSelection() {
426
+ this._selectedKeys.set(new Set());
427
+ this.selectionChange.emit([]);
428
+ }
429
+ _emitSelection(keys) {
430
+ const selected = this._rows().filter((r) => keys.has(this.getRowKey(r)));
431
+ this.selectionChange.emit(selected);
432
+ }
433
+ // ── Navegación ────────────────────────────────────────────────────────
434
+ goToPage(page) {
435
+ const clamped = Math.max(1, Math.min(page, this.totalPages()));
436
+ if (this.serverSide()) {
437
+ this._emitServerState({ page: clamped });
438
+ }
439
+ else if (this.useUrlState()) {
440
+ this._urlState.setParam(this.pageParam(), String(clamped));
441
+ }
442
+ else {
443
+ this._internalPage.set(clamped);
444
+ }
445
+ }
446
+ sortBy(key, event) {
447
+ if (this.multiSort() && event?.shiftKey) {
448
+ // Shift+click: toggle or add column / Shift+click: alterna o añade columna
449
+ const entries = [...this._sortEntries()];
450
+ const idx = entries.findIndex((e) => e.key === key);
451
+ if (idx >= 0) {
452
+ entries[idx] = { key, dir: entries[idx].dir === 'asc' ? 'desc' : 'asc' };
453
+ }
454
+ else {
455
+ entries.push({ key, dir: 'asc' });
456
+ }
457
+ const val = entries.map((e) => `${e.key}:${e.dir}`).join(',') || null;
458
+ if (this.useUrlState()) {
459
+ this._urlState.setParam(this.multiSortParam(), val);
460
+ }
461
+ else {
462
+ this._internalMultiSort.set(val ?? '');
463
+ }
464
+ }
465
+ else if (this.multiSort()) {
466
+ // Single click: replace sort / Click simple: reemplaza el orden
467
+ const current = this._sortEntries().find((e) => e.key === key);
468
+ const dir = current?.dir === 'asc' ? 'desc' : 'asc';
469
+ const val = `${key}:${dir}`;
470
+ if (this.useUrlState()) {
471
+ this._urlState.setParam(this.multiSortParam(), val);
472
+ }
473
+ else {
474
+ this._internalMultiSort.set(val);
475
+ }
476
+ }
477
+ else {
478
+ const dir = this.sortKey() === key && this.sortDir() === 'asc' ? 'desc' : 'asc';
479
+ if (this.serverSide()) {
480
+ this._emitServerState({ sortKey: key, sortDir: dir, page: 1 });
481
+ }
482
+ else if (this.useUrlState()) {
483
+ this._urlState.patchParams({
484
+ [this.sortParam()]: key,
485
+ [this.sortDirParam()]: dir,
486
+ [this.pageParam()]: '1',
487
+ });
488
+ }
489
+ else {
490
+ this._internalSortKey.set(key);
491
+ this._internalSortDir.set(dir);
492
+ this._internalPage.set(1);
493
+ }
494
+ }
495
+ }
496
+ onRowClick(row, _event) {
497
+ if (this.selectable())
498
+ this.toggleRow(row);
499
+ this.rowClick.emit(row);
500
+ }
501
+ onSearch(event) {
502
+ const q = event.target.value;
503
+ this.searchChange.emit(q);
504
+ if (this.serverSide()) {
505
+ this._emitServerState({ search: q, page: 1 });
506
+ }
507
+ else if (this.useUrlState()) {
508
+ this._urlState.patchParams({ [this.searchParam()]: q || null, [this.pageParam()]: '1' });
509
+ }
510
+ else {
511
+ this._internalSearch.set(q);
512
+ this._internalPage.set(1);
513
+ }
514
+ }
515
+ clearSearch() {
516
+ if (this.serverSide()) {
517
+ this._emitServerState({ search: '', page: 1 });
518
+ }
519
+ else if (this.useUrlState()) {
520
+ this._urlState.patchParams({ [this.searchParam()]: null, [this.pageParam()]: '1' });
521
+ }
522
+ else {
523
+ this._internalSearch.set('');
524
+ this._internalPage.set(1);
525
+ }
526
+ }
527
+ onPageSizeChange(value) {
528
+ const normalizedValue = value && typeof value === 'object' ? (value.target?.value ?? null) : value;
529
+ const size = Number(normalizedValue);
530
+ if (!Number.isFinite(size) || size <= 0)
531
+ return;
532
+ this._dynamicPageSize.set(size);
533
+ if (this.serverSide()) {
534
+ this._emitServerState({ pageSize: size, page: 1 });
535
+ }
536
+ else if (this.useUrlState()) {
537
+ this._urlState.patchParams({ [this.pageParam()]: '1' });
538
+ }
539
+ else {
540
+ this._internalPage.set(1);
541
+ }
542
+ }
543
+ setExactMatch(value) {
544
+ this._exactMatch.set(value);
545
+ }
546
+ setColumnFilter(colKey, value) {
547
+ // Column filters are local signal — no URL update avoids scroll jumps.
548
+ // paginatedData already clamps currentPage to totalPages automatically.
549
+ // Los filtros de columna son locales — sin URL para evitar saltos de scroll.
550
+ // paginatedData ya adapta currentPage a totalPages automáticamente.
551
+ this._columnFilters.update((f) => ({ ...f, [colKey]: value }));
552
+ if (this.serverSide()) {
553
+ this._emitServerState({
554
+ columnFilters: { ...this._columnFilters(), [colKey]: value },
555
+ page: 1,
556
+ });
557
+ }
558
+ }
559
+ clearColumnFilters() {
560
+ this._columnFilters.set({});
561
+ }
562
+ // ── Actions ───────────────────────────────────────────────────────────
563
+ isConfirmPending(row, action) {
564
+ const p = this._confirmPending();
565
+ return p !== null && p.rowKey === this.getRowKey(row) && p.actionKey === action.key;
566
+ }
567
+ handleAction(row, action) {
568
+ if (action.confirm) {
569
+ const p = this._confirmPending();
570
+ const rowKey = this.getRowKey(row);
571
+ if (p && p.rowKey === rowKey && p.actionKey === action.key) {
572
+ this._confirmPending.set(null);
573
+ this.actionClick.emit({ action, row });
574
+ }
575
+ else {
576
+ this._confirmPending.set({ rowKey, actionKey: action.key });
577
+ }
578
+ }
579
+ else {
580
+ this.actionClick.emit({ action, row });
581
+ }
582
+ }
583
+ cancelConfirm() {
584
+ this._confirmPending.set(null);
585
+ }
586
+ // ── Export ────────────────────────────────────────────────────────────
587
+ exportCsv() {
588
+ if (!isPlatformBrowser(this._platformId))
589
+ return;
590
+ const cols = this._getExportColumns();
591
+ const headers = cols.map((c) => `"${c.header.replace(/"/g, '""')}"`);
592
+ const rows = this.filteredData().map((row) => cols.map((col) => `"${this.getCellValue(row, col).replace(/"/g, '""')}"`));
593
+ const csv = [headers, ...rows].map((r) => r.join(',')).join('\r\n');
594
+ this._downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8;' }), `${this.exportFileName()}.csv`);
595
+ }
596
+ exportJson() {
597
+ if (!isPlatformBrowser(this._platformId))
598
+ return;
599
+ const cols = this._getExportColumns();
600
+ const data = this.filteredData().map((row) => {
601
+ const obj = {};
602
+ cols.forEach((col) => (obj[col.key] = this.getCellValue(row, col)));
603
+ return obj;
604
+ });
605
+ this._downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `${this.exportFileName()}.json`);
606
+ }
607
+ _getExportColumns() {
608
+ const keys = this.exportColumns();
609
+ const all = this.columns().filter((c) => c.type !== 'actions');
610
+ return keys.length ? all.filter((c) => keys.includes(c.key)) : all;
611
+ }
612
+ _downloadBlob(blob, filename) {
613
+ const url = URL.createObjectURL(blob);
614
+ const a = document.createElement('a');
615
+ a.href = url;
616
+ a.download = filename;
617
+ document.body.appendChild(a);
618
+ a.click();
619
+ document.body.removeChild(a);
620
+ URL.revokeObjectURL(url);
621
+ }
622
+ // ── Utilidades ────────────────────────────────────────────────────────
623
+ getRowKey(row) {
624
+ return row[this.rowKey()] ?? JSON.stringify(row);
625
+ }
626
+ getRowClass(row) {
627
+ return this.rowClass()?.(row) ?? '';
628
+ }
629
+ getCellValue(row, col) {
630
+ if (col.cell)
631
+ return col.cell(row);
632
+ const val = row[col.key];
633
+ if (val == null)
634
+ return '—';
635
+ const locale = col.locale ?? 'es-ES';
636
+ switch (col.type) {
637
+ case 'date': {
638
+ const d = val instanceof Date ? val : new Date(String(val));
639
+ return isNaN(d.getTime())
640
+ ? String(val)
641
+ : d.toLocaleDateString(locale, col.dateFormat ?? { day: '2-digit', month: '2-digit', year: 'numeric' });
642
+ }
643
+ case 'number':
644
+ return new Intl.NumberFormat(locale, col.numberFormat).format(Number(val));
645
+ case 'currency':
646
+ return new Intl.NumberFormat(locale, {
647
+ style: 'currency',
648
+ currency: col.currencyCode ?? 'EUR',
649
+ ...col.numberFormat,
650
+ }).format(Number(val));
651
+ default:
652
+ return String(val);
653
+ }
654
+ }
655
+ getSortPriority(key) {
656
+ if (!this.multiSort())
657
+ return -1;
658
+ return this._sortEntries().findIndex((e) => e.key === key);
659
+ }
660
+ columnFilterControl(key) {
661
+ let control = this._columnFilterControls.get(key);
662
+ if (!control) {
663
+ control = new FormControl(String(this.getColumnFilterValue(key)), {
664
+ nonNullable: true,
665
+ });
666
+ control.valueChanges
667
+ .pipe(takeUntilDestroyed(this._destroyRef))
668
+ .subscribe((value) => this.setColumnFilter(key, value));
669
+ this._columnFilterControls.set(key, control);
670
+ }
671
+ return control;
672
+ }
673
+ getColumnFilterValue(key) {
674
+ return this._columnFilters()[key] ?? '';
675
+ }
676
+ _emitServerState(patch) {
677
+ const current = {
678
+ page: this.currentPage(),
679
+ pageSize: this.effectivePageSize(),
680
+ search: this.searchQuery(),
681
+ sortKey: this.sortKey(),
682
+ sortDir: this.sortDir(),
683
+ sortEntries: this._sortEntries(),
684
+ columnFilters: this._columnFilters(),
685
+ };
686
+ this.serverStateChange.emit({ ...current, ...patch });
687
+ }
688
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NeuTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
689
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: NeuTableComponent, isStandalone: true, selector: "neu-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, skeletonRows: { classPropertyName: "skeletonRows", publicName: "skeletonRows", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, exactMatchable: { classPropertyName: "exactMatchable", publicName: "exactMatchable", isSignal: true, isRequired: false, transformFunction: null }, exactMatchLabel: { classPropertyName: "exactMatchLabel", publicName: "exactMatchLabel", isSignal: true, isRequired: false, transformFunction: null }, searchAriaLabel: { classPropertyName: "searchAriaLabel", publicName: "searchAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, clearSearchAriaLabel: { classPropertyName: "clearSearchAriaLabel", publicName: "clearSearchAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, clearFilterLabel: { classPropertyName: "clearFilterLabel", publicName: "clearFilterLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageAriaLabel: { classPropertyName: "previousPageAriaLabel", publicName: "previousPageAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageAriaLabel: { classPropertyName: "nextPageAriaLabel", publicName: "nextPageAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, pageSizeLabel: { classPropertyName: "pageSizeLabel", publicName: "pageSizeLabel", isSignal: true, isRequired: false, transformFunction: null }, pageSizeAriaLabel: { classPropertyName: "pageSizeAriaLabel", publicName: "pageSizeAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, paginationAriaLabel: { classPropertyName: "paginationAriaLabel", publicName: "paginationAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, exportCsvTitle: { classPropertyName: "exportCsvTitle", publicName: "exportCsvTitle", isSignal: true, isRequired: false, transformFunction: null }, exportJsonTitle: { classPropertyName: "exportJsonTitle", publicName: "exportJsonTitle", isSignal: true, isRequired: false, transformFunction: null }, clearSelectionLabel: { classPropertyName: "clearSelectionLabel", publicName: "clearSelectionLabel", isSignal: true, isRequired: false, transformFunction: null }, selectionSummaryLabel: { classPropertyName: "selectionSummaryLabel", publicName: "selectionSummaryLabel", isSignal: true, isRequired: false, transformFunction: null }, tableAriaLabel: { classPropertyName: "tableAriaLabel", publicName: "tableAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, selectAllAriaLabel: { classPropertyName: "selectAllAriaLabel", publicName: "selectAllAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, selectRowAriaLabel: { classPropertyName: "selectRowAriaLabel", publicName: "selectRowAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, expandRowAriaLabel: { classPropertyName: "expandRowAriaLabel", publicName: "expandRowAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, filterPlaceholder: { classPropertyName: "filterPlaceholder", publicName: "filterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, filterAriaPrefix: { classPropertyName: "filterAriaPrefix", publicName: "filterAriaPrefix", isSignal: true, isRequired: false, transformFunction: null }, allFilterOptionLabel: { classPropertyName: "allFilterOptionLabel", publicName: "allFilterOptionLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, resultLabelSingular: { classPropertyName: "resultLabelSingular", publicName: "resultLabelSingular", isSignal: true, isRequired: false, transformFunction: null }, resultLabelPlural: { classPropertyName: "resultLabelPlural", publicName: "resultLabelPlural", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, exportable: { classPropertyName: "exportable", publicName: "exportable", isSignal: true, isRequired: false, transformFunction: null }, exportFileName: { classPropertyName: "exportFileName", publicName: "exportFileName", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, rowKey: { classPropertyName: "rowKey", publicName: "rowKey", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, showRowNumbers: { classPropertyName: "showRowNumbers", publicName: "showRowNumbers", isSignal: true, isRequired: false, transformFunction: null }, rowClass: { classPropertyName: "rowClass", publicName: "rowClass", isSignal: true, isRequired: false, transformFunction: null }, footerRow: { classPropertyName: "footerRow", publicName: "footerRow", isSignal: true, isRequired: false, transformFunction: null }, emptyStateTemplate: { classPropertyName: "emptyStateTemplate", publicName: "emptyStateTemplate", isSignal: true, isRequired: false, transformFunction: null }, serverSide: { classPropertyName: "serverSide", publicName: "serverSide", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, multiSort: { classPropertyName: "multiSort", publicName: "multiSort", isSignal: true, isRequired: false, transformFunction: null }, exportFormats: { classPropertyName: "exportFormats", publicName: "exportFormats", isSignal: true, isRequired: false, transformFunction: null }, exportColumns: { classPropertyName: "exportColumns", publicName: "exportColumns", isSignal: true, isRequired: false, transformFunction: null }, pageParam: { classPropertyName: "pageParam", publicName: "pageParam", isSignal: true, isRequired: false, transformFunction: null }, searchParam: { classPropertyName: "searchParam", publicName: "searchParam", isSignal: true, isRequired: false, transformFunction: null }, sortParam: { classPropertyName: "sortParam", publicName: "sortParam", isSignal: true, isRequired: false, transformFunction: null }, sortDirParam: { classPropertyName: "sortDirParam", publicName: "sortDirParam", isSignal: true, isRequired: false, transformFunction: null }, multiSortParam: { classPropertyName: "multiSortParam", publicName: "multiSortParam", isSignal: true, isRequired: false, transformFunction: null }, useUrlState: { classPropertyName: "useUrlState", publicName: "useUrlState", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", rowClick: "rowClick", rowDblClick: "rowDblClick", actionClick: "actionClick", serverStateChange: "serverStateChange", searchChange: "searchChange" }, host: { properties: { "class.neu-table__host": "true", "class.neu-table__host--compact": "density() === \"compact\"", "class.neu-table__host--relaxed": "density() === \"relaxed\"" } }, queries: [{ propertyName: "expandTemplate", first: true, predicate: NeuTableExpandDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
690
+ <div class="neu-table-container" [class.neu-table--sticky-header]="stickyHeader()">
691
+ <!-- ---- Toolbar ---- -->
692
+ @if (searchable() || title() || exportable()) {
693
+ <div class="neu-table__toolbar">
694
+ @if (title()) {
695
+ <h3 class="neu-table__title">{{ title() }}</h3>
696
+ }
697
+ @if (searchable()) {
698
+ <div class="neu-table__search-group">
699
+ <div class="neu-table__search-wrapper">
700
+ <svg
701
+ class="neu-table__search-icon"
702
+ viewBox="0 0 24 24"
703
+ fill="none"
704
+ stroke="currentColor"
705
+ stroke-width="2"
706
+ stroke-linecap="round"
707
+ stroke-linejoin="round"
708
+ aria-hidden="true"
709
+ >
710
+ <circle cx="11" cy="11" r="8" />
711
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
712
+ </svg>
713
+ <input
714
+ class="neu-table__search"
715
+ type="search"
716
+ [placeholder]="searchPlaceholder()"
717
+ [value]="searchQuery()"
718
+ (input)="onSearch($event)"
719
+ [attr.aria-label]="searchAriaLabel()"
720
+ />
721
+ @if (searchQuery()) {
722
+ <button
723
+ class="neu-table__search-clear"
724
+ type="button"
725
+ [attr.aria-label]="clearSearchAriaLabel()"
726
+ (click)="clearSearch()"
727
+ >
728
+ <svg
729
+ viewBox="0 0 24 24"
730
+ fill="none"
731
+ stroke="currentColor"
732
+ stroke-width="2.5"
733
+ stroke-linecap="round"
734
+ stroke-linejoin="round"
735
+ >
736
+ <line x1="18" y1="6" x2="6" y2="18" />
737
+ <line x1="6" y1="6" x2="18" y2="18" />
738
+ </svg>
739
+ </button>
740
+ }
741
+ </div>
742
+ @if (exactMatchable()) {
743
+ <label class="neu-table__exact-label">
744
+ <input
745
+ class="neu-table__exact-checkbox"
746
+ type="checkbox"
747
+ [checked]="exactMatch()"
748
+ (change)="setExactMatch($any($event.target).checked)"
749
+ />
750
+ {{ exactMatchLabel() }}
751
+ </label>
752
+ }
753
+ </div>
754
+ }
755
+ @if (exportable() && !loading()) {
756
+ <div class="neu-table__export-group">
757
+ @if (exportFormats().includes('csv')) {
758
+ <button
759
+ class="neu-table__export-btn"
760
+ type="button"
761
+ (click)="exportCsv()"
762
+ [title]="exportCsvTitle()"
763
+ >
764
+ <svg
765
+ viewBox="0 0 24 24"
766
+ fill="none"
767
+ stroke="currentColor"
768
+ stroke-width="2"
769
+ stroke-linecap="round"
770
+ stroke-linejoin="round"
771
+ aria-hidden="true"
772
+ width="15"
773
+ height="15"
774
+ >
775
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
776
+ <polyline points="7 10 12 15 17 10" />
777
+ <line x1="12" y1="15" x2="12" y2="3" />
778
+ </svg>
779
+ CSV
780
+ </button>
781
+ }
782
+ @if (exportFormats().includes('json')) {
783
+ <button
784
+ class="neu-table__export-btn"
785
+ type="button"
786
+ (click)="exportJson()"
787
+ [title]="exportJsonTitle()"
788
+ >
789
+ <svg
790
+ viewBox="0 0 24 24"
791
+ fill="none"
792
+ stroke="currentColor"
793
+ stroke-width="2"
794
+ stroke-linecap="round"
795
+ stroke-linejoin="round"
796
+ aria-hidden="true"
797
+ width="15"
798
+ height="15"
799
+ >
800
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
801
+ <polyline points="7 10 12 15 17 10" />
802
+ <line x1="12" y1="15" x2="12" y2="3" />
803
+ </svg>
804
+ JSON
805
+ </button>
806
+ }
807
+ </div>
808
+ }
809
+ </div>
810
+ }
811
+
812
+ <!-- Barra de selección (aparece al seleccionar) -->
813
+ @if (selectable() && selectedCount() > 0) {
814
+ <div class="neu-table__selection-bar">
815
+ <span>{{ selectedRowsInfo() }}</span>
816
+ <button class="neu-table__selection-clear" type="button" (click)="clearSelection()">
817
+ {{ clearSelectionLabel() }}
818
+ </button>
819
+ </div>
820
+ }
821
+
822
+ <!-- ---- Scroll container ---- -->
823
+ <div
824
+ class="neu-table__scroll-container"
825
+ role="region"
826
+ [attr.aria-label]="title() || tableAriaLabel()"
827
+ >
828
+ <table class="neu-table" [attr.aria-rowcount]="filteredData().length">
829
+ <thead class="neu-table__head">
830
+ <tr>
831
+ <!-- Checkbox de selección global -->
832
+ @if (expandable()) {
833
+ <th class="neu-table__th neu-table__th--expand-col" scope="col"></th>
834
+ }
835
+ @if (selectable()) {
836
+ <th class="neu-table__th neu-table__th--check" scope="col">
837
+ <input
838
+ type="checkbox"
839
+ class="neu-table__checkbox"
840
+ [checked]="isAllFilteredSelected()"
841
+ [indeterminate]="isSomeFilteredSelected()"
842
+ (change)="toggleAll()"
843
+ [attr.aria-label]="selectAllAriaLabel()"
844
+ />
845
+ </th>
846
+ }
847
+ @if (showRowNumbers()) {
848
+ <th class="neu-table__th neu-table__th--rn" scope="col">#</th>
849
+ }
850
+ @for (col of columns(); track col.key) {
851
+ <th
852
+ class="neu-table__th"
853
+ [class.neu-table__th--sortable]="sortable() && col.sortable !== false"
854
+ [class.neu-table__th--sorted-asc]="
855
+ !multiSort() && sortKey() === col.key && sortDir() === 'asc'
856
+ "
857
+ [class.neu-table__th--sorted-desc]="
858
+ !multiSort() && sortKey() === col.key && sortDir() === 'desc'
859
+ "
860
+ [class]="col.cellClass ?? ''"
861
+ [class.neu-table__th--frozen-left]="col.frozen === 'left'"
862
+ [class.neu-table__th--frozen-right]="col.frozen === 'right'"
863
+ [class.neu-table__th--frozen-left-boundary]="isLastFrozenLeftColumn(col.key)"
864
+ [class.neu-table__th--frozen-right-boundary]="isFirstFrozenRightColumn(col.key)"
865
+ [style.left]="
866
+ col.frozen === 'left' ? (_frozenLeftOffsets().get(col.key) ?? 0) + 'px' : null
867
+ "
868
+ [style.width]="col.width ?? 'auto'"
869
+ [style.text-align]="col.align ?? 'left'"
870
+ scope="col"
871
+ (click)="sortable() && col.sortable !== false ? sortBy(col.key, $event) : null"
872
+ >
873
+ @if (col.headerTemplate) {
874
+ <ng-container
875
+ [ngTemplateOutlet]="col.headerTemplate"
876
+ [ngTemplateOutletContext]="{ $implicit: col }"
877
+ />
878
+ } @else {
879
+ {{ col.header }}
880
+ }
881
+ @if (sortable() && col.sortable !== false) {
882
+ <span class="neu-table__sort-icon" aria-hidden="true">
883
+ @if (multiSort()) {
884
+ @let pri = getSortPriority(col.key);
885
+ @if (pri >= 0) {
886
+ <span class="neu-table__sort-priority">{{ pri + 1 }}</span>
887
+ {{ _sortEntries()[pri]?.dir === 'asc' ? '↑' : '↓' }}
888
+ } @else {
889
+
890
+ }
891
+ } @else {
892
+ @if (sortKey() === col.key) {
893
+ {{ sortDir() === 'asc' ? '↑' : '↓' }}
894
+ } @else {
895
+
896
+ }
897
+ }
898
+ </span>
899
+ }
900
+ </th>
901
+ }
902
+ </tr>
903
+ <!-- ── Fila de filtros de columna / Column filter row ── -->
904
+ @if (_hasFilterableCol()) {
905
+ <tr class="neu-table__filter-row">
906
+ @if (expandable()) {
907
+ <th class="neu-table__th neu-table__th--filter-placeholder" scope="col"></th>
908
+ }
909
+ @if (selectable()) {
910
+ <th class="neu-table__th neu-table__th--filter-placeholder" scope="col"></th>
911
+ }
912
+ @if (showRowNumbers()) {
913
+ <th class="neu-table__th neu-table__th--filter-placeholder" scope="col"></th>
914
+ }
915
+ @for (col of columns(); track col.key) {
916
+ <th
917
+ class="neu-table__th neu-table__th--filter"
918
+ scope="col"
919
+ [style.width]="col.width ?? 'auto'"
920
+ >
921
+ @if (col.filterable) {
922
+ @if (col.filterType === 'select') {
923
+ <neu-select
924
+ [floatingLabel]="false"
925
+ [options]="_filterOpts(col)"
926
+ [formControl]="columnFilterControl(col.key)"
927
+ [attr.aria-label]="filterAriaPrefix() + ' ' + col.header"
928
+ />
929
+ } @else if (col.filterType === 'date') {
930
+ <neu-date-input
931
+ [formControl]="columnFilterControl(col.key)"
932
+ [attr.aria-label]="filterAriaPrefix() + ' ' + col.header"
933
+ />
934
+ } @else {
935
+ <neu-input
936
+ [floatingLabel]="false"
937
+ [placeholder]="filterPlaceholder()"
938
+ [formControl]="columnFilterControl(col.key)"
939
+ [attr.aria-label]="filterAriaPrefix() + ' ' + col.header"
940
+ />
941
+ }
942
+ }
943
+ </th>
944
+ }
945
+ </tr>
946
+ }
947
+ </thead>
948
+
949
+ <tbody class="neu-table__body">
950
+ @if (loading()) {
951
+ <tr class="neu-table__row--loading">
952
+ <td [attr.colspan]="totalColspan()" class="neu-table__td neu-table__td--center">
953
+ <div class="neu-table__skeleton-rows">
954
+ @for (_ of skeletonRows(); track $index) {
955
+ <div class="neu-table__skeleton-row">
956
+ @for (__ of columns(); track $index) {
957
+ <div class="neu-table__skeleton-cell"></div>
958
+ }
959
+ </div>
960
+ }
961
+ </div>
962
+ </td>
963
+ </tr>
964
+ } @else if (paginatedData().length === 0) {
965
+ <tr>
966
+ <td [attr.colspan]="totalColspan()" class="neu-table__td neu-table__td--empty">
967
+ @if (emptyStateTemplate()) {
968
+ <ng-container [ngTemplateOutlet]="emptyStateTemplate()!" />
969
+ } @else {
970
+ <div class="neu-table__empty">
971
+ <svg
972
+ viewBox="0 0 24 24"
973
+ fill="none"
974
+ stroke="currentColor"
975
+ stroke-width="1.5"
976
+ stroke-linecap="round"
977
+ stroke-linejoin="round"
978
+ >
979
+ <circle cx="11" cy="11" r="8" />
980
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
981
+ </svg>
982
+ <p>{{ emptyMessage() }}</p>
983
+ @if (searchQuery()) {
984
+ <button
985
+ class="neu-table__clear-filter"
986
+ type="button"
987
+ (click)="clearSearch()"
988
+ >
989
+ {{ clearFilterLabel() }}
990
+ </button>
991
+ }
992
+ </div>
993
+ }
994
+ </td>
995
+ </tr>
996
+ } @else {
997
+ @for (row of paginatedData(); track getRowKey(row); let rowIdx = $index) {
998
+ <tr
999
+ class="neu-table__row"
1000
+ [class]="getRowClass(row)"
1001
+ [class.neu-table__row--selected]="isRowSelected(row)"
1002
+ [class.neu-table__row--clickable]="selectable()"
1003
+ (click)="onRowClick(row, $event)"
1004
+ (dblclick)="rowDblClick.emit(row)"
1005
+ >
1006
+ @if (expandable()) {
1007
+ <td
1008
+ class="neu-table__td neu-table__td--expand-col"
1009
+ (click)="$event.stopPropagation()"
1010
+ >
1011
+ <button
1012
+ class="neu-table__expand-btn"
1013
+ type="button"
1014
+ (click)="toggleExpand(row)"
1015
+ [class.neu-table__expand-btn--open]="isRowExpanded(row)"
1016
+ [attr.aria-expanded]="isRowExpanded(row)"
1017
+ [attr.aria-label]="expandRowAriaLabel()"
1018
+ >
1019
+ <svg
1020
+ viewBox="0 0 24 24"
1021
+ fill="none"
1022
+ stroke="currentColor"
1023
+ stroke-width="2.5"
1024
+ stroke-linecap="round"
1025
+ stroke-linejoin="round"
1026
+ >
1027
+ <polyline points="9 18 15 12 9 6" />
1028
+ </svg>
1029
+ </button>
1030
+ </td>
1031
+ }
1032
+ @if (selectable()) {
1033
+ <td
1034
+ class="neu-table__td neu-table__th--check"
1035
+ (click)="$event.stopPropagation()"
1036
+ >
1037
+ <input
1038
+ type="checkbox"
1039
+ class="neu-table__checkbox"
1040
+ [checked]="isRowSelected(row)"
1041
+ (change)="toggleRow(row)"
1042
+ [attr.aria-label]="selectRowAriaLabel()"
1043
+ />
1044
+ </td>
1045
+ }
1046
+ @if (showRowNumbers()) {
1047
+ <td class="neu-table__td neu-table__td--rn">
1048
+ {{ (currentPage() - 1) * effectivePageSize() + rowIdx + 1 }}
1049
+ </td>
1050
+ }
1051
+ @for (col of columns(); track col.key) {
1052
+ <td
1053
+ class="neu-table__td"
1054
+ [class]="col.cellClass ?? ''"
1055
+ [class.neu-table__td--frozen-left]="col.frozen === 'left'"
1056
+ [class.neu-table__td--frozen-right]="col.frozen === 'right'"
1057
+ [class.neu-table__td--frozen-left-boundary]="isLastFrozenLeftColumn(col.key)"
1058
+ [class.neu-table__td--frozen-right-boundary]="
1059
+ isFirstFrozenRightColumn(col.key)
1060
+ "
1061
+ [style.left]="
1062
+ col.frozen === 'left'
1063
+ ? (_frozenLeftOffsets().get(col.key) ?? 0) + 'px'
1064
+ : null
1065
+ "
1066
+ [style.text-align]="col.align ?? 'left'"
1067
+ >
1068
+ @if (col.cellTemplate) {
1069
+ <ng-container
1070
+ [ngTemplateOutlet]="col.cellTemplate"
1071
+ [ngTemplateOutletContext]="{ $implicit: row, row: row, column: col }"
1072
+ />
1073
+ } @else if (col.type === 'badge') {
1074
+ @let badgeVal = getCellValue(row, col);
1075
+ @let badgeCfg = col.badgeMap?.[badgeVal];
1076
+ <span
1077
+ class="neu-table__cell-badge neu-table__cell-badge--{{
1078
+ badgeCfg?.variant ?? 'default'
1079
+ }}"
1080
+ >
1081
+ {{ badgeCfg?.label ?? badgeVal }}
1082
+ </span>
1083
+ } @else if (col.type === 'link') {
1084
+ <a
1085
+ class="neu-table__cell-link"
1086
+ [href]="col.linkHref ? col.linkHref(row) : '#'"
1087
+ [target]="col.linkTarget ?? '_self'"
1088
+ (click)="$event.stopPropagation()"
1089
+ >{{ getCellValue(row, col) }}</a
1090
+ >
1091
+ } @else if (col.type === 'actions') {
1092
+ <div class="neu-table__actions">
1093
+ @for (action of col.actions ?? []; track action.key) {
1094
+ @if (action.show === undefined || action.show(row)) {
1095
+ @if (isConfirmPending(row, action)) {
1096
+ <span class="neu-table__action-confirm">
1097
+ <span>{{ action.confirm }}</span>
1098
+ <button
1099
+ class="neu-table__action-btn neu-table__action-btn--danger"
1100
+ type="button"
1101
+ (click)="handleAction(row, action); $event.stopPropagation()"
1102
+ >
1103
+
1104
+ </button>
1105
+ <button
1106
+ class="neu-table__action-btn"
1107
+ type="button"
1108
+ (click)="cancelConfirm(); $event.stopPropagation()"
1109
+ >
1110
+ No
1111
+ </button>
1112
+ </span>
1113
+ } @else {
1114
+ <button
1115
+ class="neu-table__action-btn neu-table__action-btn--{{
1116
+ action.variant ?? 'ghost'
1117
+ }}"
1118
+ type="button"
1119
+ [disabled]="action.disabled ? action.disabled(row) : false"
1120
+ [title]="action.label"
1121
+ (click)="handleAction(row, action); $event.stopPropagation()"
1122
+ >
1123
+ @if (action.icon.startsWith('lucide')) {
1124
+ <neu-icon
1125
+ class="neu-table__action-icon"
1126
+ [name]="action.icon"
1127
+ size="1rem"
1128
+ aria-hidden="true"
1129
+ />
1130
+ } @else {
1131
+ <span class="neu-table__action-icon" aria-hidden="true">{{
1132
+ action.icon
1133
+ }}</span>
1134
+ }
1135
+ <span class="sr-only">{{ action.label }}</span>
1136
+ </button>
1137
+ }
1138
+ }
1139
+ }
1140
+ </div>
1141
+ } @else {
1142
+ {{ getCellValue(row, col) }}
1143
+ }
1144
+ </td>
1145
+ }
1146
+ </tr>
1147
+ @if (expandable() && isRowExpanded(row)) {
1148
+ <tr class="neu-table__row-expand-detail">
1149
+ <td [attr.colspan]="totalColspan()" class="neu-table__td--expand-panel">
1150
+ <div class="neu-table__expand-content">
1151
+ @if (expandTemplate(); as tpl) {
1152
+ <ng-container
1153
+ [ngTemplateOutlet]="tpl.templateRef"
1154
+ [ngTemplateOutletContext]="{ $implicit: row }"
1155
+ />
1156
+ }
1157
+ </div>
1158
+ </td>
1159
+ </tr>
1160
+ }
1161
+ }
1162
+ }
1163
+ </tbody>
1164
+ @if (footerRow()) {
1165
+ <tfoot class="neu-table__foot">
1166
+ <tr class="neu-table__footer-row">
1167
+ @if (expandable()) {
1168
+ <td class="neu-table__td"></td>
1169
+ }
1170
+ @if (selectable()) {
1171
+ <td class="neu-table__td"></td>
1172
+ }
1173
+ @if (showRowNumbers()) {
1174
+ <td class="neu-table__td"></td>
1175
+ }
1176
+ @for (col of columns(); track col.key) {
1177
+ <td
1178
+ class="neu-table__td neu-table__td--footer"
1179
+ [style.text-align]="col.align ?? 'left'"
1180
+ >
1181
+ {{ footerRow()![col.key] !== undefined ? footerRow()![col.key] : '' }}
1182
+ </td>
1183
+ }
1184
+ </tr>
1185
+ </tfoot>
1186
+ }
1187
+ </table>
1188
+ </div>
1189
+
1190
+ <!-- ---- Footer ---- -->
1191
+ @if (!loading() && filteredData().length > 0) {
1192
+ <div class="neu-table__footer">
1193
+ @if (pageSizeOptions().length > 0) {
1194
+ <div class="neu-table__page-size">
1195
+ <label class="neu-table__page-size-label">{{ pageSizeLabel() }}</label>
1196
+ <neu-select
1197
+ class="neu-table__page-size-select"
1198
+ [floatingLabel]="false"
1199
+ [options]="_pageSizeOptions()"
1200
+ [formControl]="_pageSizeControl"
1201
+ [attr.aria-label]="pageSizeAriaLabel()"
1202
+ size="sm"
1203
+ />
1204
+ </div>
1205
+ }
1206
+ <span class="neu-table__info">
1207
+ {{ paginationInfo() }}
1208
+ </span>
1209
+ @if (totalPages() > 1) {
1210
+ <nav class="neu-table__pagination" [attr.aria-label]="paginationAriaLabel()">
1211
+ <button
1212
+ class="neu-table__page-btn"
1213
+ [disabled]="currentPage() === 1"
1214
+ (click)="goToPage(currentPage() - 1)"
1215
+ [attr.aria-label]="previousPageAriaLabel()"
1216
+ type="button"
1217
+ >
1218
+ <svg
1219
+ viewBox="0 0 24 24"
1220
+ fill="none"
1221
+ stroke="currentColor"
1222
+ stroke-width="2.5"
1223
+ stroke-linecap="round"
1224
+ stroke-linejoin="round"
1225
+ >
1226
+ <polyline points="15 18 9 12 15 6" />
1227
+ </svg>
1228
+ </button>
1229
+ @for (page of pageNumbers(); track page) {
1230
+ <button
1231
+ class="neu-table__page-btn"
1232
+ [class.neu-table__page-btn--active]="page === currentPage()"
1233
+ (click)="goToPage(page)"
1234
+ [attr.aria-current]="page === currentPage() ? 'page' : null"
1235
+ type="button"
1236
+ >
1237
+ {{ page }}
1238
+ </button>
1239
+ }
1240
+ <button
1241
+ class="neu-table__page-btn"
1242
+ [disabled]="currentPage() === totalPages()"
1243
+ (click)="goToPage(currentPage() + 1)"
1244
+ [attr.aria-label]="nextPageAriaLabel()"
1245
+ type="button"
1246
+ >
1247
+ <svg
1248
+ viewBox="0 0 24 24"
1249
+ fill="none"
1250
+ stroke="currentColor"
1251
+ stroke-width="2.5"
1252
+ stroke-linecap="round"
1253
+ stroke-linejoin="round"
1254
+ >
1255
+ <polyline points="9 18 15 12 9 6" />
1256
+ </svg>
1257
+ </button>
1258
+ </nav>
1259
+ }
1260
+ </div>
1261
+ }
1262
+ </div>
1263
+ `, isInline: true, styles: [".neu-table-container{background:var(--neu-surface);border:1px solid var(--neu-border);border-radius:var(--neu-radius-lg);overflow:hidden;box-shadow:var(--neu-shadow-sm)}.neu-table__toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--neu-space-3);padding:var(--neu-space-4) var(--neu-space-5);border-bottom:1px solid var(--neu-border);background:var(--neu-surface)}.neu-table__title{font-size:var(--neu-text-base);font-weight:600;color:var(--neu-text);margin:0;letter-spacing:-.01em}.neu-table__search-group{display:flex;align-items:center;gap:var(--neu-space-3);flex:1;max-width:420px;min-width:0}.neu-table__search-wrapper{position:relative;display:flex;align-items:center;flex:1;min-width:0}.neu-table__search-icon{position:absolute;left:var(--neu-space-3);width:15px;height:15px;color:var(--neu-text-muted);pointer-events:none;flex-shrink:0}.neu-table__search{width:100%;height:36px;padding:0 var(--neu-space-3) 0 36px;border:1.5px solid var(--neu-border);border-radius:var(--neu-radius);background:var(--neu-surface-2);font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);color:var(--neu-text);outline:none;transition:border-color var(--neu-transition),box-shadow var(--neu-transition)}.neu-table__search::placeholder{color:var(--neu-text-muted)}.neu-table__search:focus{border-color:var(--neu-primary);box-shadow:var(--neu-focus-ring);background:var(--neu-surface)}.neu-table__search::-webkit-search-cancel-button{display:none}.neu-table__search-clear{position:absolute;right:var(--neu-space-2);display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:transparent;color:var(--neu-text-muted);cursor:pointer;border-radius:var(--neu-radius-sm);transition:color var(--neu-transition),background-color var(--neu-transition)}.neu-table__search-clear svg{width:13px;height:13px}.neu-table__search-clear:hover{color:var(--neu-error);background:var(--neu-error-bg)}.neu-table__exact-label{display:inline-flex;align-items:center;gap:6px;font-size:var(--neu-text-sm);color:var(--neu-text-muted);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.neu-table__exact-label:hover{color:var(--neu-text)}.neu-table__exact-checkbox{width:14px;height:14px;accent-color:var(--neu-primary);cursor:pointer}.neu-table__scroll-container{position:relative;isolation:isolate;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--neu-surface-3) transparent}.neu-table__scroll-container::-webkit-scrollbar{height:4px}.neu-table__scroll-container::-webkit-scrollbar-track{background:transparent}.neu-table__scroll-container::-webkit-scrollbar-thumb{background:var(--neu-surface-3);border-radius:99px}@media(max-width:399px){.neu-table__scroll-container{background-image:linear-gradient(to right,var(--neu-surface),var(--neu-surface)),linear-gradient(to right,var(--neu-surface),var(--neu-surface)),linear-gradient(to right,rgba(15,23,42,.08),transparent),linear-gradient(to left,rgba(15,23,42,.08),transparent);background-position:left center,right center,left center,right center;background-repeat:no-repeat;background-color:var(--neu-surface);background-size:20px 100%,20px 100%,10px 100%,10px 100%;background-attachment:local,local,scroll,scroll}}.neu-table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--neu-text-sm);min-width:400px}.neu-table--sticky-header .neu-table__scroll-container{overflow-y:auto;max-height:var(--neu-table-max-height, 480px)}.neu-table--sticky-header .neu-table__head tr:first-child .neu-table__th{position:sticky;top:0;z-index:2;background:var(--neu-surface-2);box-shadow:inset 0 -1px 0 var(--neu-border)}.neu-table--sticky-header .neu-table__th--filter,.neu-table--sticky-header .neu-table__th--filter-placeholder{position:sticky;top:var(--neu-table-header-height, 44px);z-index:1;background:var(--neu-surface-2)}.neu-table__head{background:var(--neu-surface-2)}.neu-table__th{padding:var(--neu-space-3) var(--neu-space-4);font-size:var(--neu-text-xs);font-weight:600;color:var(--neu-text-muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--neu-border);white-space:nowrap;-webkit-user-select:none;user-select:none;background-clip:padding-box}.neu-table__th--check{width:40px;text-align:center!important;padding:var(--neu-space-3) var(--neu-space-3)}.neu-table__th--sortable{cursor:pointer;transition:color var(--neu-transition)}.neu-table__th--sortable:hover{color:var(--neu-text)}.neu-table__th--sorted-asc,.neu-table__th--sorted-desc{color:var(--neu-primary)}.neu-table__sort-icon{margin-left:4px;font-style:normal;font-size:10px;opacity:.5}.neu-table__th--sorted-asc .neu-table__sort-icon,.neu-table__th--sorted-desc .neu-table__sort-icon{opacity:1}.neu-table__checkbox{width:16px;height:16px;cursor:pointer;accent-color:var(--neu-primary)}.neu-table__selection-bar{display:flex;align-items:center;justify-content:space-between;padding:var(--neu-space-2) var(--neu-space-5);background:var(--neu-primary-50);border-bottom:1px solid var(--neu-primary-100);font-size:var(--neu-text-sm);color:var(--neu-primary);font-weight:500;animation:neu-tab-fade .15s ease}.neu-table__selection-clear{background:none;border:none;color:var(--neu-primary);font-size:var(--neu-text-xs);font-weight:500;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0;font-family:var(--neu-font-sans)}.neu-table__row{transition:background-color var(--neu-transition)}.neu-table__row:hover{background:var(--neu-surface-2)}.neu-table__row:not(:last-child) .neu-table__td{border-bottom:1px solid var(--neu-border)}.neu-table__row--selected{background:var(--neu-primary-50)!important}.neu-table__row--selected .neu-table__td{color:var(--neu-primary)}.neu-table__row--clickable{cursor:pointer}.neu-table__td{padding:var(--neu-space-3) var(--neu-space-4);color:var(--neu-text);line-height:1.5;vertical-align:middle;background-clip:padding-box}.neu-table__td--center{text-align:center}.neu-table__td--empty{padding:var(--neu-space-8) 0}.neu-table__skeleton-rows{display:flex;flex-direction:column;gap:1px}.neu-table__skeleton-row{display:flex;align-items:center;padding:var(--neu-space-3) var(--neu-space-4);gap:var(--neu-space-4);border-bottom:1px solid var(--neu-border)}.neu-table__skeleton-cell{height:14px;flex:1;border-radius:var(--neu-radius-sm);background:linear-gradient(90deg,var(--neu-surface-2) 0%,var(--neu-surface-3) 50%,var(--neu-surface-2) 100%);background-size:200% 100%;animation:neu-skeleton 1.4s ease infinite}.neu-table__skeleton-cell:first-child{max-width:60px}.neu-table__skeleton-cell:nth-child(2){max-width:160px}.neu-table__skeleton-cell:last-child{max-width:80px}@keyframes neu-skeleton{0%{background-position:200% 0}to{background-position:-200% 0}}.neu-table__empty{display:flex;flex-direction:column;align-items:center;gap:var(--neu-space-3);padding:var(--neu-space-8) var(--neu-space-4);color:var(--neu-text-muted)}.neu-table__empty svg{width:40px;height:40px;opacity:.4}.neu-table__empty p{font-size:var(--neu-text-sm);margin:0}.neu-table__clear-filter{background:none;border:none;color:var(--neu-primary);font-size:var(--neu-text-xs);font-weight:500;cursor:pointer;padding:0;text-decoration:underline;text-underline-offset:2px}.neu-table__clear-filter:hover{color:var(--neu-primary-dark)}.neu-table__footer{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--neu-space-3);padding:var(--neu-space-3) var(--neu-space-5);border-top:1px solid var(--neu-border);background:var(--neu-surface-2)}.neu-table__info{font-size:var(--neu-text-xs);color:var(--neu-text-muted)}.neu-table__pagination{display:flex;align-items:center;gap:2px}.neu-table__page-btn{display:inline-flex;align-items:center;justify-content:center;min-width:32px;height:32px;padding:0 var(--neu-space-2);border:1px solid transparent;border-radius:var(--neu-radius);background:transparent;font-family:var(--neu-font-sans);font-size:var(--neu-text-xs);font-weight:500;color:var(--neu-text-muted);cursor:pointer;transition:all var(--neu-transition)}.neu-table__page-btn svg{width:14px;height:14px}.neu-table__page-btn:hover:not(:disabled){background:var(--neu-surface-3);color:var(--neu-text);border-color:var(--neu-border)}.neu-table__page-btn:disabled{opacity:.35;cursor:not-allowed}.neu-table__page-btn--active{background:var(--neu-primary);color:var(--neu-primary-fg);border-color:var(--neu-primary)}.neu-table__page-btn--active:hover{background:var(--neu-primary-dark);border-color:var(--neu-primary-dark)}.neu-table__export-btn{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 var(--neu-space-3);border:1.5px solid var(--neu-border);border-radius:var(--neu-radius);background:var(--neu-surface);font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);font-weight:500;color:var(--neu-text-muted);cursor:pointer;white-space:nowrap;transition:all var(--neu-transition);flex-shrink:0}.neu-table__export-btn svg{flex-shrink:0}.neu-table__export-btn:hover{color:var(--neu-primary);border-color:var(--neu-primary);background:var(--neu-primary-50)}.neu-table__page-size{display:flex;align-items:center;gap:var(--neu-space-2);min-width:0}.neu-table__page-size-label{font-size:var(--neu-text-xs);color:var(--neu-text-muted)}.neu-table__page-size-select{display:block;min-width:78px}.neu-table__page-size-select .neu-select__trigger{min-width:78px}.neu-table__th--expand-col,.neu-table__td--expand-col{width:36px;padding:var(--neu-space-2) var(--neu-space-2);text-align:center}.neu-table__expand-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;color:var(--neu-text-muted);cursor:pointer;border-radius:var(--neu-radius-sm);transition:all var(--neu-transition);padding:0}.neu-table__expand-btn svg{width:14px;height:14px;transition:transform .2s ease}.neu-table__expand-btn:hover{color:var(--neu-primary);background:var(--neu-primary-50)}.neu-table__expand-btn--open svg{transform:rotate(90deg)}.neu-table__row-expand-detail{background:var(--neu-surface-2)}.neu-table__td--expand-panel{padding:0}.neu-table__expand-content{padding:var(--neu-space-4) var(--neu-space-5);border-bottom:1px solid var(--neu-border);animation:neu-tab-fade .18s ease}.neu-table__cell-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--neu-radius-full);font-size:var(--neu-text-xs);font-weight:600;letter-spacing:.02em;white-space:nowrap}.neu-table__cell-badge--primary{background:var(--neu-primary-100);color:var(--neu-primary-dark)}.neu-table__cell-badge--success{background:var(--neu-table-badge-success-bg);color:var(--neu-table-badge-success-text)}.neu-table__cell-badge--warning{background:var(--neu-table-badge-warning-bg);color:var(--neu-table-badge-warning-text)}.neu-table__cell-badge--danger{background:var(--neu-table-badge-danger-bg);color:var(--neu-table-badge-danger-text)}.neu-table__cell-badge--info{background:var(--neu-primary-50);color:var(--neu-primary)}.neu-table__cell-badge--default{background:var(--neu-surface-3);color:var(--neu-text-muted)}@keyframes neu-tab-fade{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.neu-table__host--compact .neu-table__th,.neu-table__host--compact .neu-table__td{padding:var(--neu-table-density-compact-padding, 4px 8px)}.neu-table__host--relaxed .neu-table__th,.neu-table__host--relaxed .neu-table__td{padding:var(--neu-table-density-relaxed-padding, 16px 20px)}.neu-table__th--rn,.neu-table__td--rn{width:44px;text-align:right;padding-right:var(--neu-space-4);color:var(--neu-text-muted);font-variant-numeric:tabular-nums;font-size:var(--neu-text-xs)}.neu-table__th--frozen-left,.neu-table__th--frozen-right{position:sticky;z-index:3;background:var(--neu-surface-2)}.neu-table__th--frozen-left{left:0}.neu-table__th--frozen-right{right:0}.neu-table__th--frozen-left-boundary{box-shadow:inset -1px 0 0 var(--neu-border),6px 0 8px -8px #0f172a52}.neu-table__th--frozen-right-boundary{box-shadow:inset 1px 0 0 var(--neu-border),-6px 0 8px -8px #0f172a52}.neu-table__td--frozen-left,.neu-table__td--frozen-right{position:sticky;z-index:2;background:var(--neu-surface)}.neu-table__td--frozen-left{left:0}.neu-table__td--frozen-right{right:0}.neu-table__td--frozen-left-boundary{box-shadow:inset -1px 0 0 var(--neu-border),6px 0 8px -8px #0f172a52}.neu-table__td--frozen-right-boundary{box-shadow:inset 1px 0 0 var(--neu-border),-6px 0 8px -8px #0f172a52}.neu-table__foot{background:var(--neu-surface-2)}.neu-table__footer-row .neu-table__td--footer{padding:var(--neu-space-3) var(--neu-space-4);font-weight:600;font-size:var(--neu-text-sm);border-top:2px solid var(--neu-border);color:var(--neu-text)}.neu-table__sort-priority{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;background:var(--neu-primary);color:var(--neu-primary-fg);font-size:9px;font-weight:700;margin-right:2px}.neu-table__cell-link{color:var(--neu-primary);text-decoration:none;text-underline-offset:2px}.neu-table__cell-link:hover{text-decoration:underline}.neu-table__actions{display:flex;align-items:center;gap:var(--neu-space-1);flex-wrap:nowrap}.neu-table__action-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:var(--neu-radius-sm);background:transparent;font-size:14px;cursor:pointer;transition:all var(--neu-transition);color:var(--neu-text-muted)}.neu-table__action-btn:disabled{opacity:.35;cursor:not-allowed}.neu-table__action-btn--ghost:hover:not(:disabled){background:var(--neu-surface-3);color:var(--neu-text)}.neu-table__action-btn--primary{background:var(--neu-primary-50);color:var(--neu-primary)}.neu-table__action-btn--primary:hover:not(:disabled){background:var(--neu-primary);color:var(--neu-primary-fg)}.neu-table__action-btn--danger{color:var(--neu-error)}.neu-table__action-btn--danger:hover:not(:disabled){background:var(--neu-error-bg)}.neu-table__action-icon{line-height:1}.neu-table__action-confirm{display:inline-flex;align-items:center;gap:var(--neu-space-1);font-size:var(--neu-text-xs);color:var(--neu-text-muted);white-space:nowrap}.neu-table__action-confirm .neu-table__action-btn{width:auto;padding:0 var(--neu-space-2);font-size:var(--neu-text-xs);font-family:var(--neu-font-sans);font-weight:500;border:1px solid var(--neu-border)}.neu-table__export-group{display:flex;align-items:center;gap:var(--neu-space-2);flex-shrink:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.neu-table__filter-row .neu-table__th--filter,.neu-table__filter-row .neu-table__th--filter-placeholder{padding:var(--neu-space-2) var(--neu-space-3);background:var(--neu-surface-2);border-bottom:2px solid var(--neu-border);font-weight:400;text-transform:none;letter-spacing:0;vertical-align:middle}.neu-table__col-filter-input,.neu-table__col-filter-select{display:block;width:100%;height:28px;padding:0 var(--neu-space-2);border:1px solid var(--neu-border);border-radius:var(--neu-radius-sm);background:var(--neu-surface);font-family:var(--neu-font-sans);font-size:var(--neu-text-xs);color:var(--neu-text);font-weight:400;outline:none;transition:border-color var(--neu-transition),box-shadow var(--neu-transition)}.neu-table__col-filter-input::placeholder,.neu-table__col-filter-select::placeholder{color:var(--neu-text-muted)}.neu-table__col-filter-input:focus,.neu-table__col-filter-select:focus{border-color:var(--neu-primary);box-shadow:0 0 0 2px #007aff1a;background:var(--neu-surface)}.neu-table__col-filter-select{cursor:pointer}.neu-table--scrollable .neu-table__scroll-container{max-height:480px;overflow-y:auto}.neu-table--scrollable .neu-table__head .neu-table__th{position:sticky;top:0;z-index:2;background:var(--neu-surface-2);box-shadow:0 1px 0 var(--neu-border)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: NeuInputComponent, selector: "neu-input", inputs: ["type", "label", "placeholder", "floatingLabel", "size", "hint", "errorMessage", "disabled", "autocomplete", "startIcon", "endIcon", "icon", "iconPosition", "inputId", "name", "required", "readonly", "maxlength", "minlength", "min", "max", "pattern"] }, { kind: "component", type: NeuSelectComponent, selector: "neu-select", inputs: ["options", "label", "placeholder", "errorMessage", "disabled", "floatingLabel", "size", "searchable", "searchPlaceholder", "clearable", "noResultsMessage", "clearAriaLabel", "urlParam"], outputs: ["selectionChange"] }, { kind: "component", type: NeuDateInputComponent, selector: "neu-date-input", inputs: ["type", "label", "hint", "errorMessage", "disabled", "size", "readonly", "name", "inputId", "required", "min", "max", "step", "locale", "placeholder", "dateFormat"], outputs: ["rangeChange"] }, { kind: "component", type: NeuIconComponent, selector: "neu-icon", inputs: ["name", "strokeWidth", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1264
+ }
1265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NeuTableComponent, decorators: [{
1266
+ type: Component,
1267
+ args: [{ selector: 'neu-table', imports: [
1268
+ NgTemplateOutlet,
1269
+ ReactiveFormsModule,
1270
+ NeuInputComponent,
1271
+ NeuSelectComponent,
1272
+ NeuDateInputComponent,
1273
+ NeuIconComponent,
1274
+ ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1275
+ '[class.neu-table__host]': 'true',
1276
+ '[class.neu-table__host--compact]': 'density() === "compact"',
1277
+ '[class.neu-table__host--relaxed]': 'density() === "relaxed"',
1278
+ }, template: `
1279
+ <div class="neu-table-container" [class.neu-table--sticky-header]="stickyHeader()">
1280
+ <!-- ---- Toolbar ---- -->
1281
+ @if (searchable() || title() || exportable()) {
1282
+ <div class="neu-table__toolbar">
1283
+ @if (title()) {
1284
+ <h3 class="neu-table__title">{{ title() }}</h3>
1285
+ }
1286
+ @if (searchable()) {
1287
+ <div class="neu-table__search-group">
1288
+ <div class="neu-table__search-wrapper">
1289
+ <svg
1290
+ class="neu-table__search-icon"
1291
+ viewBox="0 0 24 24"
1292
+ fill="none"
1293
+ stroke="currentColor"
1294
+ stroke-width="2"
1295
+ stroke-linecap="round"
1296
+ stroke-linejoin="round"
1297
+ aria-hidden="true"
1298
+ >
1299
+ <circle cx="11" cy="11" r="8" />
1300
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
1301
+ </svg>
1302
+ <input
1303
+ class="neu-table__search"
1304
+ type="search"
1305
+ [placeholder]="searchPlaceholder()"
1306
+ [value]="searchQuery()"
1307
+ (input)="onSearch($event)"
1308
+ [attr.aria-label]="searchAriaLabel()"
1309
+ />
1310
+ @if (searchQuery()) {
1311
+ <button
1312
+ class="neu-table__search-clear"
1313
+ type="button"
1314
+ [attr.aria-label]="clearSearchAriaLabel()"
1315
+ (click)="clearSearch()"
1316
+ >
1317
+ <svg
1318
+ viewBox="0 0 24 24"
1319
+ fill="none"
1320
+ stroke="currentColor"
1321
+ stroke-width="2.5"
1322
+ stroke-linecap="round"
1323
+ stroke-linejoin="round"
1324
+ >
1325
+ <line x1="18" y1="6" x2="6" y2="18" />
1326
+ <line x1="6" y1="6" x2="18" y2="18" />
1327
+ </svg>
1328
+ </button>
1329
+ }
1330
+ </div>
1331
+ @if (exactMatchable()) {
1332
+ <label class="neu-table__exact-label">
1333
+ <input
1334
+ class="neu-table__exact-checkbox"
1335
+ type="checkbox"
1336
+ [checked]="exactMatch()"
1337
+ (change)="setExactMatch($any($event.target).checked)"
1338
+ />
1339
+ {{ exactMatchLabel() }}
1340
+ </label>
1341
+ }
1342
+ </div>
1343
+ }
1344
+ @if (exportable() && !loading()) {
1345
+ <div class="neu-table__export-group">
1346
+ @if (exportFormats().includes('csv')) {
1347
+ <button
1348
+ class="neu-table__export-btn"
1349
+ type="button"
1350
+ (click)="exportCsv()"
1351
+ [title]="exportCsvTitle()"
1352
+ >
1353
+ <svg
1354
+ viewBox="0 0 24 24"
1355
+ fill="none"
1356
+ stroke="currentColor"
1357
+ stroke-width="2"
1358
+ stroke-linecap="round"
1359
+ stroke-linejoin="round"
1360
+ aria-hidden="true"
1361
+ width="15"
1362
+ height="15"
1363
+ >
1364
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
1365
+ <polyline points="7 10 12 15 17 10" />
1366
+ <line x1="12" y1="15" x2="12" y2="3" />
1367
+ </svg>
1368
+ CSV
1369
+ </button>
1370
+ }
1371
+ @if (exportFormats().includes('json')) {
1372
+ <button
1373
+ class="neu-table__export-btn"
1374
+ type="button"
1375
+ (click)="exportJson()"
1376
+ [title]="exportJsonTitle()"
1377
+ >
1378
+ <svg
1379
+ viewBox="0 0 24 24"
1380
+ fill="none"
1381
+ stroke="currentColor"
1382
+ stroke-width="2"
1383
+ stroke-linecap="round"
1384
+ stroke-linejoin="round"
1385
+ aria-hidden="true"
1386
+ width="15"
1387
+ height="15"
1388
+ >
1389
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
1390
+ <polyline points="7 10 12 15 17 10" />
1391
+ <line x1="12" y1="15" x2="12" y2="3" />
1392
+ </svg>
1393
+ JSON
1394
+ </button>
1395
+ }
1396
+ </div>
1397
+ }
1398
+ </div>
1399
+ }
1400
+
1401
+ <!-- Barra de selección (aparece al seleccionar) -->
1402
+ @if (selectable() && selectedCount() > 0) {
1403
+ <div class="neu-table__selection-bar">
1404
+ <span>{{ selectedRowsInfo() }}</span>
1405
+ <button class="neu-table__selection-clear" type="button" (click)="clearSelection()">
1406
+ {{ clearSelectionLabel() }}
1407
+ </button>
1408
+ </div>
1409
+ }
1410
+
1411
+ <!-- ---- Scroll container ---- -->
1412
+ <div
1413
+ class="neu-table__scroll-container"
1414
+ role="region"
1415
+ [attr.aria-label]="title() || tableAriaLabel()"
1416
+ >
1417
+ <table class="neu-table" [attr.aria-rowcount]="filteredData().length">
1418
+ <thead class="neu-table__head">
1419
+ <tr>
1420
+ <!-- Checkbox de selección global -->
1421
+ @if (expandable()) {
1422
+ <th class="neu-table__th neu-table__th--expand-col" scope="col"></th>
1423
+ }
1424
+ @if (selectable()) {
1425
+ <th class="neu-table__th neu-table__th--check" scope="col">
1426
+ <input
1427
+ type="checkbox"
1428
+ class="neu-table__checkbox"
1429
+ [checked]="isAllFilteredSelected()"
1430
+ [indeterminate]="isSomeFilteredSelected()"
1431
+ (change)="toggleAll()"
1432
+ [attr.aria-label]="selectAllAriaLabel()"
1433
+ />
1434
+ </th>
1435
+ }
1436
+ @if (showRowNumbers()) {
1437
+ <th class="neu-table__th neu-table__th--rn" scope="col">#</th>
1438
+ }
1439
+ @for (col of columns(); track col.key) {
1440
+ <th
1441
+ class="neu-table__th"
1442
+ [class.neu-table__th--sortable]="sortable() && col.sortable !== false"
1443
+ [class.neu-table__th--sorted-asc]="
1444
+ !multiSort() && sortKey() === col.key && sortDir() === 'asc'
1445
+ "
1446
+ [class.neu-table__th--sorted-desc]="
1447
+ !multiSort() && sortKey() === col.key && sortDir() === 'desc'
1448
+ "
1449
+ [class]="col.cellClass ?? ''"
1450
+ [class.neu-table__th--frozen-left]="col.frozen === 'left'"
1451
+ [class.neu-table__th--frozen-right]="col.frozen === 'right'"
1452
+ [class.neu-table__th--frozen-left-boundary]="isLastFrozenLeftColumn(col.key)"
1453
+ [class.neu-table__th--frozen-right-boundary]="isFirstFrozenRightColumn(col.key)"
1454
+ [style.left]="
1455
+ col.frozen === 'left' ? (_frozenLeftOffsets().get(col.key) ?? 0) + 'px' : null
1456
+ "
1457
+ [style.width]="col.width ?? 'auto'"
1458
+ [style.text-align]="col.align ?? 'left'"
1459
+ scope="col"
1460
+ (click)="sortable() && col.sortable !== false ? sortBy(col.key, $event) : null"
1461
+ >
1462
+ @if (col.headerTemplate) {
1463
+ <ng-container
1464
+ [ngTemplateOutlet]="col.headerTemplate"
1465
+ [ngTemplateOutletContext]="{ $implicit: col }"
1466
+ />
1467
+ } @else {
1468
+ {{ col.header }}
1469
+ }
1470
+ @if (sortable() && col.sortable !== false) {
1471
+ <span class="neu-table__sort-icon" aria-hidden="true">
1472
+ @if (multiSort()) {
1473
+ @let pri = getSortPriority(col.key);
1474
+ @if (pri >= 0) {
1475
+ <span class="neu-table__sort-priority">{{ pri + 1 }}</span>
1476
+ {{ _sortEntries()[pri]?.dir === 'asc' ? '↑' : '↓' }}
1477
+ } @else {
1478
+
1479
+ }
1480
+ } @else {
1481
+ @if (sortKey() === col.key) {
1482
+ {{ sortDir() === 'asc' ? '↑' : '↓' }}
1483
+ } @else {
1484
+
1485
+ }
1486
+ }
1487
+ </span>
1488
+ }
1489
+ </th>
1490
+ }
1491
+ </tr>
1492
+ <!-- ── Fila de filtros de columna / Column filter row ── -->
1493
+ @if (_hasFilterableCol()) {
1494
+ <tr class="neu-table__filter-row">
1495
+ @if (expandable()) {
1496
+ <th class="neu-table__th neu-table__th--filter-placeholder" scope="col"></th>
1497
+ }
1498
+ @if (selectable()) {
1499
+ <th class="neu-table__th neu-table__th--filter-placeholder" scope="col"></th>
1500
+ }
1501
+ @if (showRowNumbers()) {
1502
+ <th class="neu-table__th neu-table__th--filter-placeholder" scope="col"></th>
1503
+ }
1504
+ @for (col of columns(); track col.key) {
1505
+ <th
1506
+ class="neu-table__th neu-table__th--filter"
1507
+ scope="col"
1508
+ [style.width]="col.width ?? 'auto'"
1509
+ >
1510
+ @if (col.filterable) {
1511
+ @if (col.filterType === 'select') {
1512
+ <neu-select
1513
+ [floatingLabel]="false"
1514
+ [options]="_filterOpts(col)"
1515
+ [formControl]="columnFilterControl(col.key)"
1516
+ [attr.aria-label]="filterAriaPrefix() + ' ' + col.header"
1517
+ />
1518
+ } @else if (col.filterType === 'date') {
1519
+ <neu-date-input
1520
+ [formControl]="columnFilterControl(col.key)"
1521
+ [attr.aria-label]="filterAriaPrefix() + ' ' + col.header"
1522
+ />
1523
+ } @else {
1524
+ <neu-input
1525
+ [floatingLabel]="false"
1526
+ [placeholder]="filterPlaceholder()"
1527
+ [formControl]="columnFilterControl(col.key)"
1528
+ [attr.aria-label]="filterAriaPrefix() + ' ' + col.header"
1529
+ />
1530
+ }
1531
+ }
1532
+ </th>
1533
+ }
1534
+ </tr>
1535
+ }
1536
+ </thead>
1537
+
1538
+ <tbody class="neu-table__body">
1539
+ @if (loading()) {
1540
+ <tr class="neu-table__row--loading">
1541
+ <td [attr.colspan]="totalColspan()" class="neu-table__td neu-table__td--center">
1542
+ <div class="neu-table__skeleton-rows">
1543
+ @for (_ of skeletonRows(); track $index) {
1544
+ <div class="neu-table__skeleton-row">
1545
+ @for (__ of columns(); track $index) {
1546
+ <div class="neu-table__skeleton-cell"></div>
1547
+ }
1548
+ </div>
1549
+ }
1550
+ </div>
1551
+ </td>
1552
+ </tr>
1553
+ } @else if (paginatedData().length === 0) {
1554
+ <tr>
1555
+ <td [attr.colspan]="totalColspan()" class="neu-table__td neu-table__td--empty">
1556
+ @if (emptyStateTemplate()) {
1557
+ <ng-container [ngTemplateOutlet]="emptyStateTemplate()!" />
1558
+ } @else {
1559
+ <div class="neu-table__empty">
1560
+ <svg
1561
+ viewBox="0 0 24 24"
1562
+ fill="none"
1563
+ stroke="currentColor"
1564
+ stroke-width="1.5"
1565
+ stroke-linecap="round"
1566
+ stroke-linejoin="round"
1567
+ >
1568
+ <circle cx="11" cy="11" r="8" />
1569
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
1570
+ </svg>
1571
+ <p>{{ emptyMessage() }}</p>
1572
+ @if (searchQuery()) {
1573
+ <button
1574
+ class="neu-table__clear-filter"
1575
+ type="button"
1576
+ (click)="clearSearch()"
1577
+ >
1578
+ {{ clearFilterLabel() }}
1579
+ </button>
1580
+ }
1581
+ </div>
1582
+ }
1583
+ </td>
1584
+ </tr>
1585
+ } @else {
1586
+ @for (row of paginatedData(); track getRowKey(row); let rowIdx = $index) {
1587
+ <tr
1588
+ class="neu-table__row"
1589
+ [class]="getRowClass(row)"
1590
+ [class.neu-table__row--selected]="isRowSelected(row)"
1591
+ [class.neu-table__row--clickable]="selectable()"
1592
+ (click)="onRowClick(row, $event)"
1593
+ (dblclick)="rowDblClick.emit(row)"
1594
+ >
1595
+ @if (expandable()) {
1596
+ <td
1597
+ class="neu-table__td neu-table__td--expand-col"
1598
+ (click)="$event.stopPropagation()"
1599
+ >
1600
+ <button
1601
+ class="neu-table__expand-btn"
1602
+ type="button"
1603
+ (click)="toggleExpand(row)"
1604
+ [class.neu-table__expand-btn--open]="isRowExpanded(row)"
1605
+ [attr.aria-expanded]="isRowExpanded(row)"
1606
+ [attr.aria-label]="expandRowAriaLabel()"
1607
+ >
1608
+ <svg
1609
+ viewBox="0 0 24 24"
1610
+ fill="none"
1611
+ stroke="currentColor"
1612
+ stroke-width="2.5"
1613
+ stroke-linecap="round"
1614
+ stroke-linejoin="round"
1615
+ >
1616
+ <polyline points="9 18 15 12 9 6" />
1617
+ </svg>
1618
+ </button>
1619
+ </td>
1620
+ }
1621
+ @if (selectable()) {
1622
+ <td
1623
+ class="neu-table__td neu-table__th--check"
1624
+ (click)="$event.stopPropagation()"
1625
+ >
1626
+ <input
1627
+ type="checkbox"
1628
+ class="neu-table__checkbox"
1629
+ [checked]="isRowSelected(row)"
1630
+ (change)="toggleRow(row)"
1631
+ [attr.aria-label]="selectRowAriaLabel()"
1632
+ />
1633
+ </td>
1634
+ }
1635
+ @if (showRowNumbers()) {
1636
+ <td class="neu-table__td neu-table__td--rn">
1637
+ {{ (currentPage() - 1) * effectivePageSize() + rowIdx + 1 }}
1638
+ </td>
1639
+ }
1640
+ @for (col of columns(); track col.key) {
1641
+ <td
1642
+ class="neu-table__td"
1643
+ [class]="col.cellClass ?? ''"
1644
+ [class.neu-table__td--frozen-left]="col.frozen === 'left'"
1645
+ [class.neu-table__td--frozen-right]="col.frozen === 'right'"
1646
+ [class.neu-table__td--frozen-left-boundary]="isLastFrozenLeftColumn(col.key)"
1647
+ [class.neu-table__td--frozen-right-boundary]="
1648
+ isFirstFrozenRightColumn(col.key)
1649
+ "
1650
+ [style.left]="
1651
+ col.frozen === 'left'
1652
+ ? (_frozenLeftOffsets().get(col.key) ?? 0) + 'px'
1653
+ : null
1654
+ "
1655
+ [style.text-align]="col.align ?? 'left'"
1656
+ >
1657
+ @if (col.cellTemplate) {
1658
+ <ng-container
1659
+ [ngTemplateOutlet]="col.cellTemplate"
1660
+ [ngTemplateOutletContext]="{ $implicit: row, row: row, column: col }"
1661
+ />
1662
+ } @else if (col.type === 'badge') {
1663
+ @let badgeVal = getCellValue(row, col);
1664
+ @let badgeCfg = col.badgeMap?.[badgeVal];
1665
+ <span
1666
+ class="neu-table__cell-badge neu-table__cell-badge--{{
1667
+ badgeCfg?.variant ?? 'default'
1668
+ }}"
1669
+ >
1670
+ {{ badgeCfg?.label ?? badgeVal }}
1671
+ </span>
1672
+ } @else if (col.type === 'link') {
1673
+ <a
1674
+ class="neu-table__cell-link"
1675
+ [href]="col.linkHref ? col.linkHref(row) : '#'"
1676
+ [target]="col.linkTarget ?? '_self'"
1677
+ (click)="$event.stopPropagation()"
1678
+ >{{ getCellValue(row, col) }}</a
1679
+ >
1680
+ } @else if (col.type === 'actions') {
1681
+ <div class="neu-table__actions">
1682
+ @for (action of col.actions ?? []; track action.key) {
1683
+ @if (action.show === undefined || action.show(row)) {
1684
+ @if (isConfirmPending(row, action)) {
1685
+ <span class="neu-table__action-confirm">
1686
+ <span>{{ action.confirm }}</span>
1687
+ <button
1688
+ class="neu-table__action-btn neu-table__action-btn--danger"
1689
+ type="button"
1690
+ (click)="handleAction(row, action); $event.stopPropagation()"
1691
+ >
1692
+
1693
+ </button>
1694
+ <button
1695
+ class="neu-table__action-btn"
1696
+ type="button"
1697
+ (click)="cancelConfirm(); $event.stopPropagation()"
1698
+ >
1699
+ No
1700
+ </button>
1701
+ </span>
1702
+ } @else {
1703
+ <button
1704
+ class="neu-table__action-btn neu-table__action-btn--{{
1705
+ action.variant ?? 'ghost'
1706
+ }}"
1707
+ type="button"
1708
+ [disabled]="action.disabled ? action.disabled(row) : false"
1709
+ [title]="action.label"
1710
+ (click)="handleAction(row, action); $event.stopPropagation()"
1711
+ >
1712
+ @if (action.icon.startsWith('lucide')) {
1713
+ <neu-icon
1714
+ class="neu-table__action-icon"
1715
+ [name]="action.icon"
1716
+ size="1rem"
1717
+ aria-hidden="true"
1718
+ />
1719
+ } @else {
1720
+ <span class="neu-table__action-icon" aria-hidden="true">{{
1721
+ action.icon
1722
+ }}</span>
1723
+ }
1724
+ <span class="sr-only">{{ action.label }}</span>
1725
+ </button>
1726
+ }
1727
+ }
1728
+ }
1729
+ </div>
1730
+ } @else {
1731
+ {{ getCellValue(row, col) }}
1732
+ }
1733
+ </td>
1734
+ }
1735
+ </tr>
1736
+ @if (expandable() && isRowExpanded(row)) {
1737
+ <tr class="neu-table__row-expand-detail">
1738
+ <td [attr.colspan]="totalColspan()" class="neu-table__td--expand-panel">
1739
+ <div class="neu-table__expand-content">
1740
+ @if (expandTemplate(); as tpl) {
1741
+ <ng-container
1742
+ [ngTemplateOutlet]="tpl.templateRef"
1743
+ [ngTemplateOutletContext]="{ $implicit: row }"
1744
+ />
1745
+ }
1746
+ </div>
1747
+ </td>
1748
+ </tr>
1749
+ }
1750
+ }
1751
+ }
1752
+ </tbody>
1753
+ @if (footerRow()) {
1754
+ <tfoot class="neu-table__foot">
1755
+ <tr class="neu-table__footer-row">
1756
+ @if (expandable()) {
1757
+ <td class="neu-table__td"></td>
1758
+ }
1759
+ @if (selectable()) {
1760
+ <td class="neu-table__td"></td>
1761
+ }
1762
+ @if (showRowNumbers()) {
1763
+ <td class="neu-table__td"></td>
1764
+ }
1765
+ @for (col of columns(); track col.key) {
1766
+ <td
1767
+ class="neu-table__td neu-table__td--footer"
1768
+ [style.text-align]="col.align ?? 'left'"
1769
+ >
1770
+ {{ footerRow()![col.key] !== undefined ? footerRow()![col.key] : '' }}
1771
+ </td>
1772
+ }
1773
+ </tr>
1774
+ </tfoot>
1775
+ }
1776
+ </table>
1777
+ </div>
1778
+
1779
+ <!-- ---- Footer ---- -->
1780
+ @if (!loading() && filteredData().length > 0) {
1781
+ <div class="neu-table__footer">
1782
+ @if (pageSizeOptions().length > 0) {
1783
+ <div class="neu-table__page-size">
1784
+ <label class="neu-table__page-size-label">{{ pageSizeLabel() }}</label>
1785
+ <neu-select
1786
+ class="neu-table__page-size-select"
1787
+ [floatingLabel]="false"
1788
+ [options]="_pageSizeOptions()"
1789
+ [formControl]="_pageSizeControl"
1790
+ [attr.aria-label]="pageSizeAriaLabel()"
1791
+ size="sm"
1792
+ />
1793
+ </div>
1794
+ }
1795
+ <span class="neu-table__info">
1796
+ {{ paginationInfo() }}
1797
+ </span>
1798
+ @if (totalPages() > 1) {
1799
+ <nav class="neu-table__pagination" [attr.aria-label]="paginationAriaLabel()">
1800
+ <button
1801
+ class="neu-table__page-btn"
1802
+ [disabled]="currentPage() === 1"
1803
+ (click)="goToPage(currentPage() - 1)"
1804
+ [attr.aria-label]="previousPageAriaLabel()"
1805
+ type="button"
1806
+ >
1807
+ <svg
1808
+ viewBox="0 0 24 24"
1809
+ fill="none"
1810
+ stroke="currentColor"
1811
+ stroke-width="2.5"
1812
+ stroke-linecap="round"
1813
+ stroke-linejoin="round"
1814
+ >
1815
+ <polyline points="15 18 9 12 15 6" />
1816
+ </svg>
1817
+ </button>
1818
+ @for (page of pageNumbers(); track page) {
1819
+ <button
1820
+ class="neu-table__page-btn"
1821
+ [class.neu-table__page-btn--active]="page === currentPage()"
1822
+ (click)="goToPage(page)"
1823
+ [attr.aria-current]="page === currentPage() ? 'page' : null"
1824
+ type="button"
1825
+ >
1826
+ {{ page }}
1827
+ </button>
1828
+ }
1829
+ <button
1830
+ class="neu-table__page-btn"
1831
+ [disabled]="currentPage() === totalPages()"
1832
+ (click)="goToPage(currentPage() + 1)"
1833
+ [attr.aria-label]="nextPageAriaLabel()"
1834
+ type="button"
1835
+ >
1836
+ <svg
1837
+ viewBox="0 0 24 24"
1838
+ fill="none"
1839
+ stroke="currentColor"
1840
+ stroke-width="2.5"
1841
+ stroke-linecap="round"
1842
+ stroke-linejoin="round"
1843
+ >
1844
+ <polyline points="9 18 15 12 9 6" />
1845
+ </svg>
1846
+ </button>
1847
+ </nav>
1848
+ }
1849
+ </div>
1850
+ }
1851
+ </div>
1852
+ `, styles: [".neu-table-container{background:var(--neu-surface);border:1px solid var(--neu-border);border-radius:var(--neu-radius-lg);overflow:hidden;box-shadow:var(--neu-shadow-sm)}.neu-table__toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--neu-space-3);padding:var(--neu-space-4) var(--neu-space-5);border-bottom:1px solid var(--neu-border);background:var(--neu-surface)}.neu-table__title{font-size:var(--neu-text-base);font-weight:600;color:var(--neu-text);margin:0;letter-spacing:-.01em}.neu-table__search-group{display:flex;align-items:center;gap:var(--neu-space-3);flex:1;max-width:420px;min-width:0}.neu-table__search-wrapper{position:relative;display:flex;align-items:center;flex:1;min-width:0}.neu-table__search-icon{position:absolute;left:var(--neu-space-3);width:15px;height:15px;color:var(--neu-text-muted);pointer-events:none;flex-shrink:0}.neu-table__search{width:100%;height:36px;padding:0 var(--neu-space-3) 0 36px;border:1.5px solid var(--neu-border);border-radius:var(--neu-radius);background:var(--neu-surface-2);font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);color:var(--neu-text);outline:none;transition:border-color var(--neu-transition),box-shadow var(--neu-transition)}.neu-table__search::placeholder{color:var(--neu-text-muted)}.neu-table__search:focus{border-color:var(--neu-primary);box-shadow:var(--neu-focus-ring);background:var(--neu-surface)}.neu-table__search::-webkit-search-cancel-button{display:none}.neu-table__search-clear{position:absolute;right:var(--neu-space-2);display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:transparent;color:var(--neu-text-muted);cursor:pointer;border-radius:var(--neu-radius-sm);transition:color var(--neu-transition),background-color var(--neu-transition)}.neu-table__search-clear svg{width:13px;height:13px}.neu-table__search-clear:hover{color:var(--neu-error);background:var(--neu-error-bg)}.neu-table__exact-label{display:inline-flex;align-items:center;gap:6px;font-size:var(--neu-text-sm);color:var(--neu-text-muted);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.neu-table__exact-label:hover{color:var(--neu-text)}.neu-table__exact-checkbox{width:14px;height:14px;accent-color:var(--neu-primary);cursor:pointer}.neu-table__scroll-container{position:relative;isolation:isolate;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--neu-surface-3) transparent}.neu-table__scroll-container::-webkit-scrollbar{height:4px}.neu-table__scroll-container::-webkit-scrollbar-track{background:transparent}.neu-table__scroll-container::-webkit-scrollbar-thumb{background:var(--neu-surface-3);border-radius:99px}@media(max-width:399px){.neu-table__scroll-container{background-image:linear-gradient(to right,var(--neu-surface),var(--neu-surface)),linear-gradient(to right,var(--neu-surface),var(--neu-surface)),linear-gradient(to right,rgba(15,23,42,.08),transparent),linear-gradient(to left,rgba(15,23,42,.08),transparent);background-position:left center,right center,left center,right center;background-repeat:no-repeat;background-color:var(--neu-surface);background-size:20px 100%,20px 100%,10px 100%,10px 100%;background-attachment:local,local,scroll,scroll}}.neu-table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--neu-text-sm);min-width:400px}.neu-table--sticky-header .neu-table__scroll-container{overflow-y:auto;max-height:var(--neu-table-max-height, 480px)}.neu-table--sticky-header .neu-table__head tr:first-child .neu-table__th{position:sticky;top:0;z-index:2;background:var(--neu-surface-2);box-shadow:inset 0 -1px 0 var(--neu-border)}.neu-table--sticky-header .neu-table__th--filter,.neu-table--sticky-header .neu-table__th--filter-placeholder{position:sticky;top:var(--neu-table-header-height, 44px);z-index:1;background:var(--neu-surface-2)}.neu-table__head{background:var(--neu-surface-2)}.neu-table__th{padding:var(--neu-space-3) var(--neu-space-4);font-size:var(--neu-text-xs);font-weight:600;color:var(--neu-text-muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--neu-border);white-space:nowrap;-webkit-user-select:none;user-select:none;background-clip:padding-box}.neu-table__th--check{width:40px;text-align:center!important;padding:var(--neu-space-3) var(--neu-space-3)}.neu-table__th--sortable{cursor:pointer;transition:color var(--neu-transition)}.neu-table__th--sortable:hover{color:var(--neu-text)}.neu-table__th--sorted-asc,.neu-table__th--sorted-desc{color:var(--neu-primary)}.neu-table__sort-icon{margin-left:4px;font-style:normal;font-size:10px;opacity:.5}.neu-table__th--sorted-asc .neu-table__sort-icon,.neu-table__th--sorted-desc .neu-table__sort-icon{opacity:1}.neu-table__checkbox{width:16px;height:16px;cursor:pointer;accent-color:var(--neu-primary)}.neu-table__selection-bar{display:flex;align-items:center;justify-content:space-between;padding:var(--neu-space-2) var(--neu-space-5);background:var(--neu-primary-50);border-bottom:1px solid var(--neu-primary-100);font-size:var(--neu-text-sm);color:var(--neu-primary);font-weight:500;animation:neu-tab-fade .15s ease}.neu-table__selection-clear{background:none;border:none;color:var(--neu-primary);font-size:var(--neu-text-xs);font-weight:500;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0;font-family:var(--neu-font-sans)}.neu-table__row{transition:background-color var(--neu-transition)}.neu-table__row:hover{background:var(--neu-surface-2)}.neu-table__row:not(:last-child) .neu-table__td{border-bottom:1px solid var(--neu-border)}.neu-table__row--selected{background:var(--neu-primary-50)!important}.neu-table__row--selected .neu-table__td{color:var(--neu-primary)}.neu-table__row--clickable{cursor:pointer}.neu-table__td{padding:var(--neu-space-3) var(--neu-space-4);color:var(--neu-text);line-height:1.5;vertical-align:middle;background-clip:padding-box}.neu-table__td--center{text-align:center}.neu-table__td--empty{padding:var(--neu-space-8) 0}.neu-table__skeleton-rows{display:flex;flex-direction:column;gap:1px}.neu-table__skeleton-row{display:flex;align-items:center;padding:var(--neu-space-3) var(--neu-space-4);gap:var(--neu-space-4);border-bottom:1px solid var(--neu-border)}.neu-table__skeleton-cell{height:14px;flex:1;border-radius:var(--neu-radius-sm);background:linear-gradient(90deg,var(--neu-surface-2) 0%,var(--neu-surface-3) 50%,var(--neu-surface-2) 100%);background-size:200% 100%;animation:neu-skeleton 1.4s ease infinite}.neu-table__skeleton-cell:first-child{max-width:60px}.neu-table__skeleton-cell:nth-child(2){max-width:160px}.neu-table__skeleton-cell:last-child{max-width:80px}@keyframes neu-skeleton{0%{background-position:200% 0}to{background-position:-200% 0}}.neu-table__empty{display:flex;flex-direction:column;align-items:center;gap:var(--neu-space-3);padding:var(--neu-space-8) var(--neu-space-4);color:var(--neu-text-muted)}.neu-table__empty svg{width:40px;height:40px;opacity:.4}.neu-table__empty p{font-size:var(--neu-text-sm);margin:0}.neu-table__clear-filter{background:none;border:none;color:var(--neu-primary);font-size:var(--neu-text-xs);font-weight:500;cursor:pointer;padding:0;text-decoration:underline;text-underline-offset:2px}.neu-table__clear-filter:hover{color:var(--neu-primary-dark)}.neu-table__footer{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--neu-space-3);padding:var(--neu-space-3) var(--neu-space-5);border-top:1px solid var(--neu-border);background:var(--neu-surface-2)}.neu-table__info{font-size:var(--neu-text-xs);color:var(--neu-text-muted)}.neu-table__pagination{display:flex;align-items:center;gap:2px}.neu-table__page-btn{display:inline-flex;align-items:center;justify-content:center;min-width:32px;height:32px;padding:0 var(--neu-space-2);border:1px solid transparent;border-radius:var(--neu-radius);background:transparent;font-family:var(--neu-font-sans);font-size:var(--neu-text-xs);font-weight:500;color:var(--neu-text-muted);cursor:pointer;transition:all var(--neu-transition)}.neu-table__page-btn svg{width:14px;height:14px}.neu-table__page-btn:hover:not(:disabled){background:var(--neu-surface-3);color:var(--neu-text);border-color:var(--neu-border)}.neu-table__page-btn:disabled{opacity:.35;cursor:not-allowed}.neu-table__page-btn--active{background:var(--neu-primary);color:var(--neu-primary-fg);border-color:var(--neu-primary)}.neu-table__page-btn--active:hover{background:var(--neu-primary-dark);border-color:var(--neu-primary-dark)}.neu-table__export-btn{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 var(--neu-space-3);border:1.5px solid var(--neu-border);border-radius:var(--neu-radius);background:var(--neu-surface);font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);font-weight:500;color:var(--neu-text-muted);cursor:pointer;white-space:nowrap;transition:all var(--neu-transition);flex-shrink:0}.neu-table__export-btn svg{flex-shrink:0}.neu-table__export-btn:hover{color:var(--neu-primary);border-color:var(--neu-primary);background:var(--neu-primary-50)}.neu-table__page-size{display:flex;align-items:center;gap:var(--neu-space-2);min-width:0}.neu-table__page-size-label{font-size:var(--neu-text-xs);color:var(--neu-text-muted)}.neu-table__page-size-select{display:block;min-width:78px}.neu-table__page-size-select .neu-select__trigger{min-width:78px}.neu-table__th--expand-col,.neu-table__td--expand-col{width:36px;padding:var(--neu-space-2) var(--neu-space-2);text-align:center}.neu-table__expand-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;color:var(--neu-text-muted);cursor:pointer;border-radius:var(--neu-radius-sm);transition:all var(--neu-transition);padding:0}.neu-table__expand-btn svg{width:14px;height:14px;transition:transform .2s ease}.neu-table__expand-btn:hover{color:var(--neu-primary);background:var(--neu-primary-50)}.neu-table__expand-btn--open svg{transform:rotate(90deg)}.neu-table__row-expand-detail{background:var(--neu-surface-2)}.neu-table__td--expand-panel{padding:0}.neu-table__expand-content{padding:var(--neu-space-4) var(--neu-space-5);border-bottom:1px solid var(--neu-border);animation:neu-tab-fade .18s ease}.neu-table__cell-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--neu-radius-full);font-size:var(--neu-text-xs);font-weight:600;letter-spacing:.02em;white-space:nowrap}.neu-table__cell-badge--primary{background:var(--neu-primary-100);color:var(--neu-primary-dark)}.neu-table__cell-badge--success{background:var(--neu-table-badge-success-bg);color:var(--neu-table-badge-success-text)}.neu-table__cell-badge--warning{background:var(--neu-table-badge-warning-bg);color:var(--neu-table-badge-warning-text)}.neu-table__cell-badge--danger{background:var(--neu-table-badge-danger-bg);color:var(--neu-table-badge-danger-text)}.neu-table__cell-badge--info{background:var(--neu-primary-50);color:var(--neu-primary)}.neu-table__cell-badge--default{background:var(--neu-surface-3);color:var(--neu-text-muted)}@keyframes neu-tab-fade{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.neu-table__host--compact .neu-table__th,.neu-table__host--compact .neu-table__td{padding:var(--neu-table-density-compact-padding, 4px 8px)}.neu-table__host--relaxed .neu-table__th,.neu-table__host--relaxed .neu-table__td{padding:var(--neu-table-density-relaxed-padding, 16px 20px)}.neu-table__th--rn,.neu-table__td--rn{width:44px;text-align:right;padding-right:var(--neu-space-4);color:var(--neu-text-muted);font-variant-numeric:tabular-nums;font-size:var(--neu-text-xs)}.neu-table__th--frozen-left,.neu-table__th--frozen-right{position:sticky;z-index:3;background:var(--neu-surface-2)}.neu-table__th--frozen-left{left:0}.neu-table__th--frozen-right{right:0}.neu-table__th--frozen-left-boundary{box-shadow:inset -1px 0 0 var(--neu-border),6px 0 8px -8px #0f172a52}.neu-table__th--frozen-right-boundary{box-shadow:inset 1px 0 0 var(--neu-border),-6px 0 8px -8px #0f172a52}.neu-table__td--frozen-left,.neu-table__td--frozen-right{position:sticky;z-index:2;background:var(--neu-surface)}.neu-table__td--frozen-left{left:0}.neu-table__td--frozen-right{right:0}.neu-table__td--frozen-left-boundary{box-shadow:inset -1px 0 0 var(--neu-border),6px 0 8px -8px #0f172a52}.neu-table__td--frozen-right-boundary{box-shadow:inset 1px 0 0 var(--neu-border),-6px 0 8px -8px #0f172a52}.neu-table__foot{background:var(--neu-surface-2)}.neu-table__footer-row .neu-table__td--footer{padding:var(--neu-space-3) var(--neu-space-4);font-weight:600;font-size:var(--neu-text-sm);border-top:2px solid var(--neu-border);color:var(--neu-text)}.neu-table__sort-priority{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;background:var(--neu-primary);color:var(--neu-primary-fg);font-size:9px;font-weight:700;margin-right:2px}.neu-table__cell-link{color:var(--neu-primary);text-decoration:none;text-underline-offset:2px}.neu-table__cell-link:hover{text-decoration:underline}.neu-table__actions{display:flex;align-items:center;gap:var(--neu-space-1);flex-wrap:nowrap}.neu-table__action-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:var(--neu-radius-sm);background:transparent;font-size:14px;cursor:pointer;transition:all var(--neu-transition);color:var(--neu-text-muted)}.neu-table__action-btn:disabled{opacity:.35;cursor:not-allowed}.neu-table__action-btn--ghost:hover:not(:disabled){background:var(--neu-surface-3);color:var(--neu-text)}.neu-table__action-btn--primary{background:var(--neu-primary-50);color:var(--neu-primary)}.neu-table__action-btn--primary:hover:not(:disabled){background:var(--neu-primary);color:var(--neu-primary-fg)}.neu-table__action-btn--danger{color:var(--neu-error)}.neu-table__action-btn--danger:hover:not(:disabled){background:var(--neu-error-bg)}.neu-table__action-icon{line-height:1}.neu-table__action-confirm{display:inline-flex;align-items:center;gap:var(--neu-space-1);font-size:var(--neu-text-xs);color:var(--neu-text-muted);white-space:nowrap}.neu-table__action-confirm .neu-table__action-btn{width:auto;padding:0 var(--neu-space-2);font-size:var(--neu-text-xs);font-family:var(--neu-font-sans);font-weight:500;border:1px solid var(--neu-border)}.neu-table__export-group{display:flex;align-items:center;gap:var(--neu-space-2);flex-shrink:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.neu-table__filter-row .neu-table__th--filter,.neu-table__filter-row .neu-table__th--filter-placeholder{padding:var(--neu-space-2) var(--neu-space-3);background:var(--neu-surface-2);border-bottom:2px solid var(--neu-border);font-weight:400;text-transform:none;letter-spacing:0;vertical-align:middle}.neu-table__col-filter-input,.neu-table__col-filter-select{display:block;width:100%;height:28px;padding:0 var(--neu-space-2);border:1px solid var(--neu-border);border-radius:var(--neu-radius-sm);background:var(--neu-surface);font-family:var(--neu-font-sans);font-size:var(--neu-text-xs);color:var(--neu-text);font-weight:400;outline:none;transition:border-color var(--neu-transition),box-shadow var(--neu-transition)}.neu-table__col-filter-input::placeholder,.neu-table__col-filter-select::placeholder{color:var(--neu-text-muted)}.neu-table__col-filter-input:focus,.neu-table__col-filter-select:focus{border-color:var(--neu-primary);box-shadow:0 0 0 2px #007aff1a;background:var(--neu-surface)}.neu-table__col-filter-select{cursor:pointer}.neu-table--scrollable .neu-table__scroll-container{max-height:480px;overflow-y:auto}.neu-table--scrollable .neu-table__head .neu-table__th{position:sticky;top:0;z-index:2;background:var(--neu-surface-2);box-shadow:0 1px 0 var(--neu-border)}\n"] }]
1853
+ }], ctorParameters: () => [], propDecorators: { expandTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NeuTableExpandDirective), { isSignal: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], skeletonRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonRows", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], exactMatchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "exactMatchable", required: false }] }], exactMatchLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "exactMatchLabel", required: false }] }], searchAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchAriaLabel", required: false }] }], clearSearchAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearSearchAriaLabel", required: false }] }], clearFilterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearFilterLabel", required: false }] }], previousPageAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageAriaLabel", required: false }] }], nextPageAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageAriaLabel", required: false }] }], pageSizeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeLabel", required: false }] }], pageSizeAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeAriaLabel", required: false }] }], paginationAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationAriaLabel", required: false }] }], exportCsvTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportCsvTitle", required: false }] }], exportJsonTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportJsonTitle", required: false }] }], clearSelectionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearSelectionLabel", required: false }] }], selectionSummaryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionSummaryLabel", required: false }] }], tableAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableAriaLabel", required: false }] }], selectAllAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllAriaLabel", required: false }] }], selectRowAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectRowAriaLabel", required: false }] }], expandRowAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandRowAriaLabel", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], filterAriaPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterAriaPrefix", required: false }] }], allFilterOptionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "allFilterOptionLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], resultLabelSingular: [{ type: i0.Input, args: [{ isSignal: true, alias: "resultLabelSingular", required: false }] }], resultLabelPlural: [{ type: i0.Input, args: [{ isSignal: true, alias: "resultLabelPlural", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], exportable: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportable", required: false }] }], exportFileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportFileName", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], rowKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowKey", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], showRowNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRowNumbers", required: false }] }], rowClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowClass", required: false }] }], footerRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerRow", required: false }] }], emptyStateTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyStateTemplate", required: false }] }], serverSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverSide", required: false }] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: false }] }], multiSort: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSort", required: false }] }], exportFormats: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportFormats", required: false }] }], exportColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportColumns", required: false }] }], pageParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageParam", required: false }] }], searchParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchParam", required: false }] }], sortParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortParam", required: false }] }], sortDirParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDirParam", required: false }] }], multiSortParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSortParam", required: false }] }], useUrlState: [{ type: i0.Input, args: [{ isSignal: true, alias: "useUrlState", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDblClick: [{ type: i0.Output, args: ["rowDblClick"] }], actionClick: [{ type: i0.Output, args: ["actionClick"] }], serverStateChange: [{ type: i0.Output, args: ["serverStateChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }] } });
1854
+
1855
+ /**
1856
+ * Generated bundle index. Do not edit.
1857
+ */
1858
+
1859
+ export { NeuTableComponent, NeuTableExpandDirective };
1860
+ //# sourceMappingURL=neural-ui-core-table.mjs.map