shirkasoft-ui-components 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 (79) hide show
  1. package/fesm2022/shirkasoft-ui-components-breadcrumb.mjs +199 -0
  2. package/fesm2022/shirkasoft-ui-components-breadcrumb.mjs.map +1 -0
  3. package/fesm2022/shirkasoft-ui-components-button.mjs +148 -0
  4. package/fesm2022/shirkasoft-ui-components-button.mjs.map +1 -0
  5. package/fesm2022/shirkasoft-ui-components-chart.mjs +153 -0
  6. package/fesm2022/shirkasoft-ui-components-chart.mjs.map +1 -0
  7. package/fesm2022/shirkasoft-ui-components-confirm-dialog.mjs +143 -0
  8. package/fesm2022/shirkasoft-ui-components-confirm-dialog.mjs.map +1 -0
  9. package/fesm2022/shirkasoft-ui-components-date-picker.mjs +638 -0
  10. package/fesm2022/shirkasoft-ui-components-date-picker.mjs.map +1 -0
  11. package/fesm2022/shirkasoft-ui-components-fieldset.mjs +94 -0
  12. package/fesm2022/shirkasoft-ui-components-fieldset.mjs.map +1 -0
  13. package/fesm2022/shirkasoft-ui-components-file-upload.mjs +242 -0
  14. package/fesm2022/shirkasoft-ui-components-file-upload.mjs.map +1 -0
  15. package/fesm2022/shirkasoft-ui-components-knob.mjs +269 -0
  16. package/fesm2022/shirkasoft-ui-components-knob.mjs.map +1 -0
  17. package/fesm2022/shirkasoft-ui-components-loading.mjs +255 -0
  18. package/fesm2022/shirkasoft-ui-components-loading.mjs.map +1 -0
  19. package/fesm2022/shirkasoft-ui-components-modal.mjs +290 -0
  20. package/fesm2022/shirkasoft-ui-components-modal.mjs.map +1 -0
  21. package/fesm2022/shirkasoft-ui-components-notification.mjs +143 -0
  22. package/fesm2022/shirkasoft-ui-components-notification.mjs.map +1 -0
  23. package/fesm2022/shirkasoft-ui-components-price-input.mjs +64 -0
  24. package/fesm2022/shirkasoft-ui-components-price-input.mjs.map +1 -0
  25. package/fesm2022/shirkasoft-ui-components-rail-sidebar.mjs +205 -0
  26. package/fesm2022/shirkasoft-ui-components-rail-sidebar.mjs.map +1 -0
  27. package/fesm2022/shirkasoft-ui-components-select.mjs +737 -0
  28. package/fesm2022/shirkasoft-ui-components-select.mjs.map +1 -0
  29. package/fesm2022/shirkasoft-ui-components-sidebar.mjs +185 -0
  30. package/fesm2022/shirkasoft-ui-components-sidebar.mjs.map +1 -0
  31. package/fesm2022/shirkasoft-ui-components-skeleton.mjs +74 -0
  32. package/fesm2022/shirkasoft-ui-components-skeleton.mjs.map +1 -0
  33. package/fesm2022/shirkasoft-ui-components-table.mjs +689 -0
  34. package/fesm2022/shirkasoft-ui-components-table.mjs.map +1 -0
  35. package/fesm2022/shirkasoft-ui-components-tabs.mjs +249 -0
  36. package/fesm2022/shirkasoft-ui-components-tabs.mjs.map +1 -0
  37. package/fesm2022/shirkasoft-ui-components-tag.mjs +70 -0
  38. package/fesm2022/shirkasoft-ui-components-tag.mjs.map +1 -0
  39. package/fesm2022/shirkasoft-ui-components-text-field.mjs +370 -0
  40. package/fesm2022/shirkasoft-ui-components-text-field.mjs.map +1 -0
  41. package/fesm2022/shirkasoft-ui-components-timeline.mjs +222 -0
  42. package/fesm2022/shirkasoft-ui-components-timeline.mjs.map +1 -0
  43. package/fesm2022/shirkasoft-ui-components-toggle.mjs +186 -0
  44. package/fesm2022/shirkasoft-ui-components-toggle.mjs.map +1 -0
  45. package/fesm2022/shirkasoft-ui-components-tooltip.mjs +169 -0
  46. package/fesm2022/shirkasoft-ui-components-tooltip.mjs.map +1 -0
  47. package/fesm2022/shirkasoft-ui-components-utils.mjs +64 -0
  48. package/fesm2022/shirkasoft-ui-components-utils.mjs.map +1 -0
  49. package/fesm2022/shirkasoft-ui-components-wizard.mjs +167 -0
  50. package/fesm2022/shirkasoft-ui-components-wizard.mjs.map +1 -0
  51. package/fesm2022/shirkasoft-ui-components.mjs +0 -5729
  52. package/fesm2022/shirkasoft-ui-components.mjs.map +1 -1
  53. package/package.json +101 -1
  54. package/types/shirkasoft-ui-components-breadcrumb.d.ts +68 -0
  55. package/types/shirkasoft-ui-components-button.d.ts +59 -0
  56. package/types/shirkasoft-ui-components-chart.d.ts +65 -0
  57. package/types/shirkasoft-ui-components-confirm-dialog.d.ts +88 -0
  58. package/types/shirkasoft-ui-components-date-picker.d.ts +200 -0
  59. package/types/shirkasoft-ui-components-fieldset.d.ts +72 -0
  60. package/types/shirkasoft-ui-components-file-upload.d.ts +87 -0
  61. package/types/shirkasoft-ui-components-knob.d.ts +69 -0
  62. package/types/shirkasoft-ui-components-loading.d.ts +94 -0
  63. package/types/shirkasoft-ui-components-modal.d.ts +139 -0
  64. package/types/shirkasoft-ui-components-notification.d.ts +82 -0
  65. package/types/shirkasoft-ui-components-price-input.d.ts +41 -0
  66. package/types/shirkasoft-ui-components-rail-sidebar.d.ts +121 -0
  67. package/types/shirkasoft-ui-components-select.d.ts +182 -0
  68. package/types/shirkasoft-ui-components-sidebar.d.ts +106 -0
  69. package/types/shirkasoft-ui-components-skeleton.d.ts +36 -0
  70. package/types/shirkasoft-ui-components-table.d.ts +293 -0
  71. package/types/shirkasoft-ui-components-tabs.d.ts +102 -0
  72. package/types/shirkasoft-ui-components-tag.d.ts +31 -0
  73. package/types/shirkasoft-ui-components-text-field.d.ts +120 -0
  74. package/types/shirkasoft-ui-components-timeline.d.ts +178 -0
  75. package/types/shirkasoft-ui-components-toggle.d.ts +48 -0
  76. package/types/shirkasoft-ui-components-tooltip.d.ts +47 -0
  77. package/types/shirkasoft-ui-components-utils.d.ts +9 -0
  78. package/types/shirkasoft-ui-components-wizard.d.ts +101 -0
  79. package/types/shirkasoft-ui-components.d.ts +1 -2333
@@ -0,0 +1,689 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ChangeDetectorRef, DestroyRef, input, output, signal, linkedSignal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import * as i1 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import * as i2 from '@angular/forms';
7
+ import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
8
+ import * as i3 from '@jsverse/transloco';
9
+ import { TranslocoService, TranslocoModule } from '@jsverse/transloco';
10
+ import { Subject, debounceTime } from 'rxjs';
11
+ import { LucidePlus, LucideDynamicIcon } from '@lucide/angular';
12
+ import { TagComponent } from 'shirkasoft-ui-components/tag';
13
+ import { TooltipComponent } from 'shirkasoft-ui-components/tooltip';
14
+ import { getIcon } from 'shirkasoft-ui-components/utils';
15
+
16
+ /**
17
+ * Shirkasoft UI Components.
18
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
19
+ * Belongs to Shirkasoft.
20
+ */
21
+ /**
22
+ * Data table with sorting, column filters, global search, pagination,
23
+ * row/header actions, tag cells and optional server-side mode.
24
+ */
25
+ class TableComponent {
26
+ /** Returns the effective CSS width for a column ('actions' included). */
27
+ getColumnWidth(col) {
28
+ return col.width || this.DEFAULT_COLUMN_WIDTH;
29
+ }
30
+ constructor() {
31
+ this.fb = inject(FormBuilder);
32
+ this.cdr = inject(ChangeDetectorRef);
33
+ this.transloco = inject(TranslocoService);
34
+ this.destroyRef = inject(DestroyRef);
35
+ /** Maps icon names to lucide components, falling back to a plus icon. */
36
+ this.getIcon = (name) => getIcon(name, LucidePlus);
37
+ /**
38
+ * Minimum width applied to each column (except `actions`, which keeps its
39
+ * own computed width) only on narrow screens. On wide screens columns
40
+ * stretch normally to fill the container width. Can be overridden per
41
+ * column via `col.width`.
42
+ */
43
+ this.DEFAULT_COLUMN_WIDTH = '140px';
44
+ /** Signal with the translated "Actions" header label. */
45
+ this.actionsHeaderLabel = toSignal(this.transloco.selectTranslate('common.actions'), {
46
+ initialValue: 'Acciones',
47
+ });
48
+ /** Signal reacting to language changes so imperative translations are recomputed. */
49
+ this.currentLang = toSignal(this.transloco.langChanges$, {
50
+ initialValue: this.transloco.getActiveLang(),
51
+ });
52
+ /** Reactive signal of loaded translations so pageReport is not stuck with raw keys. */
53
+ this.translations = toSignal(this.transloco.selectTranslation());
54
+ /** Full dataset. In server-side mode only the current page rows. */
55
+ this.data = input([], ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
56
+ /** Column definitions (field, header, filterable, tag cell options, etc.). */
57
+ this.columns = input([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
58
+ /** Number of rows displayed per page. */
59
+ this.rowsPerPage = input(10, ...(ngDevMode ? [{ debugName: "rowsPerPage" }] : /* istanbul ignore next */ []));
60
+ /** Options shown in the rows-per-page selector. */
61
+ this.rowsPerPageOptions = input([10, 25, 50], ...(ngDevMode ? [{ debugName: "rowsPerPageOptions" }] : /* istanbul ignore next */ []));
62
+ /** Whether the table is in loading state (shows the overlay/skeleton). */
63
+ this.loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
64
+ /** Whether to render the row actions column. */
65
+ this.showActionRow = input(true, ...(ngDevMode ? [{ debugName: "showActionRow" }] : /* istanbul ignore next */ []));
66
+ /** Custom cell/header templates keyed by column field. */
67
+ this.customTemplates = input({}, ...(ngDevMode ? [{ debugName: "customTemplates" }] : /* istanbul ignore next */ []));
68
+ /** Actions rendered in the table header. */
69
+ this.headerActions = input([], ...(ngDevMode ? [{ debugName: "headerActions" }] : /* istanbul ignore next */ []));
70
+ /** Actions rendered per row (icon, label, class, visibility/disabled). */
71
+ this.rowActions = input([], ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
72
+ /** Whether the table container renders a drop shadow. */
73
+ this.hasShadow = input(true, ...(ngDevMode ? [{ debugName: "hasShadow" }] : /* istanbul ignore next */ []));
74
+ /** Field used as the default initial sort. */
75
+ this.defaultSortField = input('', ...(ngDevMode ? [{ debugName: "defaultSortField" }] : /* istanbul ignore next */ []));
76
+ /** Order used by the default initial sort (1 asc, -1 desc). */
77
+ this.defaultSortOrder = input(1, ...(ngDevMode ? [{ debugName: "defaultSortOrder" }] : /* istanbul ignore next */ []));
78
+ /** Whether the global search input is shown. */
79
+ this.showSearch = input(true, ...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
80
+ /** Placeholder for the global search input. */
81
+ this.searchPlaceholder = input('', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
82
+ /** Message shown when there are no rows. */
83
+ this.emptyMessage = input('', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
84
+ /** Enables server-side pagination/filtering/sorting mode. */
85
+ this.serverSide = input(false, ...(ngDevMode ? [{ debugName: "serverSide" }] : /* istanbul ignore next */ []));
86
+ /** Total number of records in server-side mode (for pagination math). */
87
+ this.totalRecords = input(0, ...(ngDevMode ? [{ debugName: "totalRecords" }] : /* istanbul ignore next */ []));
88
+ /** Optional prebuilt filter chips (label + value) shown above the table. */
89
+ this.filters = input([], ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
90
+ /** Currently active filter chip value. */
91
+ this.activeFilter = input('', ...(ngDevMode ? [{ debugName: "activeFilter" }] : /* istanbul ignore next */ []));
92
+ /** Emits when the user clicks the refresh button. */
93
+ this.refresh = output();
94
+ /** Emits page changes (server-side mode) with first/rows/page info. */
95
+ this.pageChange = output();
96
+ /** Emits the active column filters. */
97
+ this.filterChange = output();
98
+ /** Emits the debounced global search term. */
99
+ this.searchChange = output();
100
+ /** Emits when a filter chip is clicked. */
101
+ this.filterClick = output();
102
+ /** Internal copy of the input data used as the base for search/filter. */
103
+ this.originalData = signal([], ...(ngDevMode ? [{ debugName: "originalData" }] : /* istanbul ignore next */ []));
104
+ /** Rows after the global search is applied. */
105
+ this.searchedData = signal([], ...(ngDevMode ? [{ debugName: "searchedData" }] : /* istanbul ignore next */ []));
106
+ /** Rows after column filters are applied. */
107
+ this.filteredData = signal([], ...(ngDevMode ? [{ debugName: "filteredData" }] : /* istanbul ignore next */ []));
108
+ /** Rows belonging to the current page (after slicing). */
109
+ this.displayedData = signal([], ...(ngDevMode ? [{ debugName: "displayedData" }] : /* istanbul ignore next */ []));
110
+ /** Local total records after filtering (client mode). */
111
+ this.localTotalRecords = signal(0, ...(ngDevMode ? [{ debugName: "localTotalRecords" }] : /* istanbul ignore next */ []));
112
+ /** Index of the first row of the current page. */
113
+ this.first = signal(0, ...(ngDevMode ? [{ debugName: "first" }] : /* istanbul ignore next */ []));
114
+ /** Visibility map for each column filter input. */
115
+ this.showFilterInput = signal({}, ...(ngDevMode ? [{ debugName: "showFilterInput" }] : /* istanbul ignore next */ []));
116
+ /** Current global search term. */
117
+ this.searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
118
+ /** Currently sorted field. */
119
+ this.sortField = signal('', ...(ngDevMode ? [{ debugName: "sortField" }] : /* istanbul ignore next */ []));
120
+ /** Current sort direction (1 asc, -1 desc). */
121
+ this.sortOrder = signal(1, ...(ngDevMode ? [{ debugName: "sortOrder" }] : /* istanbul ignore next */ []));
122
+ /** Local rows per page, kept in sync with the `rowsPerPage` input. */
123
+ this.rowsPerPageLocal = linkedSignal(() => this.rowsPerPage(), ...(ngDevMode ? [{ debugName: "rowsPerPageLocal" }] : /* istanbul ignore next */ []));
124
+ /** Total number of pages based on the effective record count. */
125
+ this.totalPages = computed(() => Math.max(1, Math.ceil(this.effectiveTotalRecords() / this.rowsPerPageLocal())), ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
126
+ /** Current page number (1-based). */
127
+ this.currentPage = computed(() => Math.floor(this.first() / this.rowsPerPageLocal()) + 1, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
128
+ /** Subjects used to debounce column filters (server mode) and search. */
129
+ this.filterSubject = new Subject();
130
+ this.searchSubject = new Subject();
131
+ /**
132
+ * Signature of the last built set of filterable fields (+filter type) to
133
+ * avoid rebuilding the form (and losing user input) on reference changes.
134
+ */
135
+ this.lastFilterFieldsSignature = '';
136
+ this.columnFilterSubscriptions = [];
137
+ /** Columns array with the leading `actions` column when row actions exist. */
138
+ this.columnsWithActions = computed(() => {
139
+ const columnsArray = this.columns();
140
+ const showAction = this.showActionRow();
141
+ const rowActionsArray = this.rowActions();
142
+ const translatedHeader = this.actionsHeaderLabel();
143
+ if (showAction && rowActionsArray.length > 0) {
144
+ const actionsWidth = rowActionsArray.length <= 2 ? '110px' : rowActionsArray.length <= 3 ? '130px' : '170px';
145
+ return [
146
+ {
147
+ field: 'actions',
148
+ header: translatedHeader || 'Acciones',
149
+ width: actionsWidth,
150
+ defaultSort: true,
151
+ },
152
+ ...columnsArray,
153
+ ];
154
+ }
155
+ return [...columnsArray];
156
+ }, ...(ngDevMode ? [{ debugName: "columnsWithActions" }] : /* istanbul ignore next */ []));
157
+ /** Total records: server-provided in server mode, locally computed otherwise. */
158
+ this.effectiveTotalRecords = computed(() => {
159
+ return this.serverSide() ? this.totalRecords() : this.localTotalRecords();
160
+ }, ...(ngDevMode ? [{ debugName: "effectiveTotalRecords" }] : /* istanbul ignore next */ []));
161
+ /**
162
+ * Rows rendered by the template: the input data in server-side mode or the
163
+ * locally sliced/page current rows otherwise.
164
+ */
165
+ this.effectiveDisplayedData = computed(() => {
166
+ return this.serverSide() ? this.data() : this.displayedData();
167
+ }, ...(ngDevMode ? [{ debugName: "effectiveDisplayedData" }] : /* istanbul ignore next */ []));
168
+ /**
169
+ * Reactive "X to Y of Z" report. Depends on loaded translations and the
170
+ * active language so it always reflects the translated template.
171
+ */
172
+ this.pageReport = computed(() => {
173
+ this.translations(); // forces recomputation when translations finish loading
174
+ this.currentLang(); // forces recomputation when the language changes
175
+ const total = this.effectiveTotalRecords();
176
+ if (total === 0) {
177
+ return this.transloco.translate('table.pageReport', {
178
+ from: 0,
179
+ to: 0,
180
+ total: 0,
181
+ });
182
+ }
183
+ const from = this.first() + 1;
184
+ const to = Math.min(this.first() + this.rowsPerPageLocal(), total);
185
+ return this.transloco.translate('table.pageReport', { from, to, total });
186
+ }, ...(ngDevMode ? [{ debugName: "pageReport" }] : /* istanbul ignore next */ []));
187
+ this.columnFiltersForm = this.fb.group({});
188
+ // Debounced column-filter changes (server-side mode only).
189
+ this.filterSubject
190
+ .pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef))
191
+ .subscribe(() => {
192
+ if (this.serverSide()) {
193
+ this.emitFilterChange();
194
+ }
195
+ });
196
+ // Debounced global search changes (server-side mode only).
197
+ this.searchSubject
198
+ .pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef))
199
+ .subscribe((value) => {
200
+ this.searchChange.emit(value);
201
+ });
202
+ // When input data changes, refresh the internal copy and re-apply filters
203
+ // unless the server already handles the filtering.
204
+ effect(() => {
205
+ const newData = this.data();
206
+ untracked(() => {
207
+ this.originalData.set([...newData]);
208
+ if (!this.serverSide()) {
209
+ this.applyFilters();
210
+ }
211
+ });
212
+ });
213
+ // Rebuild the column filter form whenever the columns change.
214
+ effect(() => {
215
+ const cols = this.columns();
216
+ untracked(() => {
217
+ this.setupColumnFilters(cols);
218
+ });
219
+ });
220
+ // Apply the default sort once when the default fields are provided.
221
+ effect(() => {
222
+ const field = this.defaultSortField();
223
+ const order = this.defaultSortOrder();
224
+ if (field) {
225
+ untracked(() => {
226
+ this.sortField.set(field);
227
+ this.sortOrder.set(order);
228
+ if (!this.serverSide()) {
229
+ this.applySort();
230
+ this.updateDisplayedData();
231
+ }
232
+ });
233
+ }
234
+ });
235
+ // Apply the global search locally whenever the term changes.
236
+ effect(() => {
237
+ const query = this.searchQuery();
238
+ untracked(() => {
239
+ if (!this.serverSide()) {
240
+ this.applySearch();
241
+ }
242
+ });
243
+ });
244
+ }
245
+ /**
246
+ * Builds (or keeps) the reactive form with one control per filterable
247
+ * column. The form is only rebuilt when the set of filterable
248
+ * fields/filter types changes, preserving the values already typed by the
249
+ * user otherwise. Previous subscriptions are unsubscribed to avoid leaks.
250
+ */
251
+ setupColumnFilters(cols) {
252
+ const filterableCols = cols.filter((c) => c.filter);
253
+ const signature = filterableCols.map((c) => `${c.field}:${c.filterType ?? 'text'}`).join('|');
254
+ if (signature === this.lastFilterFieldsSignature && this.columnFiltersForm) {
255
+ return;
256
+ }
257
+ this.lastFilterFieldsSignature = signature;
258
+ // Clean up previous subscriptions before rebuilding.
259
+ this.columnFilterSubscriptions.forEach((sub) => sub.unsubscribe());
260
+ this.columnFilterSubscriptions = [];
261
+ const previousValues = {};
262
+ if (this.columnFiltersForm) {
263
+ Object.keys(this.columnFiltersForm.controls).forEach((field) => {
264
+ previousValues[field] = this.columnFiltersForm.get(field)?.value;
265
+ });
266
+ }
267
+ const filterControls = {};
268
+ const filterVisibility = { ...this.showFilterInput() };
269
+ filterableCols.forEach((col) => {
270
+ const defaultValue = col.filterType === 'select' ? null : '';
271
+ const preserved = previousValues.hasOwnProperty(col.field)
272
+ ? previousValues[col.field]
273
+ : defaultValue;
274
+ filterControls[col.field] = [preserved];
275
+ if (!(col.field in filterVisibility)) {
276
+ filterVisibility[col.field] = false;
277
+ }
278
+ });
279
+ this.columnFiltersForm = this.fb.group(filterControls);
280
+ this.showFilterInput.set(filterVisibility);
281
+ Object.keys(this.columnFiltersForm.controls).forEach((field) => {
282
+ const sub = this.columnFiltersForm
283
+ .get(field)
284
+ ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
285
+ .subscribe(() => {
286
+ this.first.set(0);
287
+ if (this.serverSide()) {
288
+ this.filterSubject.next();
289
+ }
290
+ else {
291
+ this.applyFilters();
292
+ }
293
+ this.cdr.markForCheck();
294
+ });
295
+ if (sub) {
296
+ this.columnFilterSubscriptions.push(sub);
297
+ }
298
+ });
299
+ }
300
+ /** Collects the non-empty column filter values and emits them. */
301
+ emitFilterChange() {
302
+ const filters = {};
303
+ Object.keys(this.columnFiltersForm.controls).forEach((field) => {
304
+ const value = this.columnFiltersForm.get(field)?.value;
305
+ if (value !== null && value !== undefined && value !== '') {
306
+ filters[field] = value;
307
+ }
308
+ });
309
+ this.filterChange.emit({ filters });
310
+ }
311
+ /** Applies the global search over the original data (client mode). */
312
+ applySearch() {
313
+ const original = this.originalData();
314
+ const query = this.searchQuery().toLowerCase();
315
+ if (!query) {
316
+ this.searchedData.set([...original]);
317
+ }
318
+ else {
319
+ const filtered = original.filter((item) => {
320
+ return Object.values(item).some((val) => {
321
+ if (val === null || val === undefined)
322
+ return false;
323
+ return String(val).toLowerCase().includes(query);
324
+ });
325
+ });
326
+ this.searchedData.set(filtered);
327
+ }
328
+ this.applyColumnFilters();
329
+ }
330
+ /** Entry point that applies search (which cascades into column filters). */
331
+ applyFilters() {
332
+ this.applySearch();
333
+ }
334
+ /**
335
+ * Applies every column filter on top of the searched data using each
336
+ * column's `filterType` ('select', 'exact' or text contains).
337
+ */
338
+ applyColumnFilters() {
339
+ const data = this.searchedData();
340
+ let filtered = [...data];
341
+ this.columns().forEach((col) => {
342
+ if (col.filter) {
343
+ const filterValue = this.columnFiltersForm.get(col.field)?.value;
344
+ if (filterValue !== null && filterValue !== undefined && filterValue !== '') {
345
+ if (col.filterType === 'select') {
346
+ filtered = filtered.filter((item) => item[col.field] == filterValue);
347
+ }
348
+ else if (col.filterType === 'exact') {
349
+ const searchTerm = String(filterValue).toLowerCase();
350
+ filtered = filtered.filter((item) => String(item[col.field]).toLowerCase() === searchTerm);
351
+ }
352
+ else {
353
+ const searchTerm = String(filterValue).toLowerCase();
354
+ filtered = filtered.filter((item) => String(item[col.field]).toLowerCase().includes(searchTerm));
355
+ }
356
+ }
357
+ }
358
+ });
359
+ this.filteredData.set(filtered);
360
+ this.localTotalRecords.set(filtered.length);
361
+ this.applySort();
362
+ this.updateDisplayedData();
363
+ }
364
+ /** Slices the filtered rows for the current page. */
365
+ updateDisplayedData() {
366
+ const filtered = this.filteredData();
367
+ const firstIndex = this.first();
368
+ const rows = this.rowsPerPageLocal();
369
+ const displayed = filtered.slice(firstIndex, firstIndex + rows);
370
+ this.displayedData.set(displayed);
371
+ }
372
+ /** Emits the filter chip clicked in the template. */
373
+ onFilterClick(value) {
374
+ this.filterClick.emit(value);
375
+ }
376
+ /** Toggles the sort direction of a column (first click sorts ascending). */
377
+ toggleSort(field) {
378
+ if (this.sortField() === field) {
379
+ this.sortOrder.set(this.sortOrder() * -1);
380
+ }
381
+ else {
382
+ this.sortField.set(field);
383
+ this.sortOrder.set(1);
384
+ }
385
+ this.first.set(0);
386
+ if (!this.serverSide()) {
387
+ this.applySort();
388
+ this.updateDisplayedData();
389
+ }
390
+ }
391
+ /**
392
+ * Sorts the filtered rows by the current field. Rows with null/undefined
393
+ * values are always kept at the end regardless of sort direction.
394
+ */
395
+ applySort() {
396
+ const field = this.sortField();
397
+ const order = this.sortOrder();
398
+ if (!field)
399
+ return;
400
+ const data = [...this.filteredData()];
401
+ data.sort((a, b) => {
402
+ const va = a[field];
403
+ const vb = b[field];
404
+ if (va == null && vb == null)
405
+ return 0;
406
+ if (va == null)
407
+ return 1; // nulls always at the end
408
+ if (vb == null)
409
+ return -1;
410
+ if (va < vb)
411
+ return -1 * order;
412
+ if (va > vb)
413
+ return 1 * order;
414
+ return 0;
415
+ });
416
+ this.filteredData.set(data);
417
+ }
418
+ /** Handles the global search input (updates query, resets page, debounces server mode). */
419
+ onSearchInput(value) {
420
+ this.searchQuery.set(value);
421
+ this.first.set(0);
422
+ if (this.serverSide()) {
423
+ this.searchSubject.next(value);
424
+ }
425
+ }
426
+ /** Clears a column filter value and hides its input. */
427
+ clearFilter(field) {
428
+ const column = this.columns().find((col) => col.field === field);
429
+ const value = column?.filterType === 'select' ? null : '';
430
+ this.columnFiltersForm.get(field)?.setValue(value);
431
+ const visibility = this.showFilterInput();
432
+ this.showFilterInput.set({ ...visibility, [field]: false });
433
+ this.cdr.markForCheck();
434
+ }
435
+ /** Toggles the visibility of a column filter input. */
436
+ toggleFilter(field) {
437
+ const visibility = this.showFilterInput();
438
+ this.showFilterInput.set({ ...visibility, [field]: !visibility[field] });
439
+ this.cdr.markForCheck();
440
+ }
441
+ /**
442
+ * Whether a column filter is non-empty. Avoids treating the number 0 as
443
+ * "no value".
444
+ */
445
+ hasFilterValue(field) {
446
+ const value = this.columnFiltersForm.get(field)?.value;
447
+ return value !== null && value !== undefined && value !== '';
448
+ }
449
+ /** Returns the reactive page report string (template helper). */
450
+ getPageReport() {
451
+ return this.pageReport();
452
+ }
453
+ /** Goes to the first page (emits pageChange in server-side mode). */
454
+ goToFirst() {
455
+ const rows = this.rowsPerPageLocal();
456
+ this.first.set(0);
457
+ if (this.serverSide()) {
458
+ this.pageChange.emit({
459
+ first: 0,
460
+ rows,
461
+ page: 0,
462
+ pageCount: Math.ceil(this.totalRecords() / rows),
463
+ });
464
+ }
465
+ else {
466
+ this.updateDisplayedData();
467
+ }
468
+ }
469
+ /** Goes to the last page (emits pageChange in server-side mode). */
470
+ goToLast() {
471
+ const total = this.serverSide() ? this.totalRecords() : this.filteredData().length;
472
+ const rows = this.rowsPerPageLocal();
473
+ const lastFirst = Math.max(0, Math.ceil(total / rows) * rows - rows);
474
+ this.first.set(lastFirst);
475
+ if (this.serverSide()) {
476
+ const page = Math.floor(lastFirst / rows);
477
+ this.pageChange.emit({
478
+ first: lastFirst,
479
+ rows,
480
+ page,
481
+ pageCount: Math.ceil(total / rows),
482
+ });
483
+ }
484
+ else {
485
+ this.updateDisplayedData();
486
+ }
487
+ }
488
+ /** Updates rows-per-page and resets to the first page. */
489
+ onRowsPerPageChange(rows) {
490
+ rows = Number(rows);
491
+ this.rowsPerPageLocal.set(rows);
492
+ this.first.set(0);
493
+ if (this.serverSide()) {
494
+ this.pageChange.emit({
495
+ first: 0,
496
+ rows,
497
+ page: 0,
498
+ pageCount: Math.ceil(this.totalRecords() / rows),
499
+ });
500
+ }
501
+ else {
502
+ this.updateDisplayedData();
503
+ }
504
+ }
505
+ /** Emits the refresh output. */
506
+ refreshData() {
507
+ this.refresh.emit();
508
+ }
509
+ /** Whether the current page is the first one. */
510
+ isFirstPage() {
511
+ return this.first() === 0;
512
+ }
513
+ /** Whether the current page is the last one. */
514
+ isLastPage() {
515
+ const total = this.effectiveTotalRecords();
516
+ return this.first() + this.rowsPerPageLocal() >= total;
517
+ }
518
+ /** Moves to the next page (emits pageChange in server-side mode). */
519
+ next() {
520
+ const total = this.serverSide() ? this.totalRecords() : this.filteredData().length;
521
+ const rows = this.rowsPerPageLocal();
522
+ let first = this.first() + rows;
523
+ if (first >= total) {
524
+ first = total - rows;
525
+ }
526
+ this.first.set(first);
527
+ if (this.serverSide()) {
528
+ const page = Math.floor(first / rows);
529
+ this.pageChange.emit({
530
+ first,
531
+ rows,
532
+ page,
533
+ pageCount: Math.ceil(total / rows),
534
+ });
535
+ }
536
+ else {
537
+ this.updateDisplayedData();
538
+ }
539
+ }
540
+ /** Moves to the previous page (emits pageChange in server mode). */
541
+ prev() {
542
+ const rows = this.rowsPerPageLocal();
543
+ let first = this.first() - rows;
544
+ const total = this.serverSide() ? this.totalRecords() : this.filteredData().length;
545
+ if (first < 0) {
546
+ first = 0;
547
+ }
548
+ this.first.set(first);
549
+ if (this.serverSide()) {
550
+ const page = Math.floor(first / rows);
551
+ this.pageChange.emit({
552
+ first,
553
+ rows,
554
+ page,
555
+ pageCount: Math.ceil(total / rows),
556
+ });
557
+ }
558
+ else {
559
+ this.updateDisplayedData();
560
+ }
561
+ }
562
+ /** Resets the table to the first page. */
563
+ reset() {
564
+ this.goToFirst();
565
+ }
566
+ /** Resolves the row action label, static or per-row function. */
567
+ getRowActionLabel(action, rowData) {
568
+ return typeof action.label === 'function' ? action.label(rowData) : action.label;
569
+ }
570
+ /**
571
+ * Resolves the row action icon: a lucide component, a function returning
572
+ * the icon name, or a raw string resolved by the central icon map.
573
+ */
574
+ getRowActionIconName(action, rowData) {
575
+ const icon = action.icon;
576
+ if (typeof icon === 'function') {
577
+ if (icon.prototype === undefined) {
578
+ return icon(rowData);
579
+ }
580
+ return icon;
581
+ }
582
+ return icon;
583
+ }
584
+ /** Resolves the CSS class of a row action, static or per-row function. */
585
+ getRowActionClass(action, rowData) {
586
+ if (!action.class)
587
+ return '';
588
+ return typeof action.class === 'function' ? action.class(rowData) : action.class;
589
+ }
590
+ /** Whether a header action is disabled. */
591
+ isHeaderActionDisabled(action) {
592
+ return action.isDisabled ? action.isDisabled() : false;
593
+ }
594
+ /** Whether a header action is visible. */
595
+ isHeaderActionVisible(action) {
596
+ return action.isVisible ? action.isVisible() : true;
597
+ }
598
+ /** Whether a row action is disabled for the given row. */
599
+ isRowActionDisabled(action, rowData) {
600
+ return action.isDisabled ? action.isDisabled(rowData) : false;
601
+ }
602
+ /** Whether a row action is visible for the given row. */
603
+ isRowActionVisible(action, rowData) {
604
+ return action.isVisible ? action.isVisible(rowData) : true;
605
+ }
606
+ /** Truncates a value with an ellipsis past the given character limit. */
607
+ truncate(value, limit = 30) {
608
+ if (value === null || value === undefined)
609
+ return '';
610
+ const str = String(value);
611
+ return str.length > limit ? str.slice(0, limit) + '\u2026' : str;
612
+ }
613
+ /** Resolves the value displayed inside a tag cell, static or per-row function. */
614
+ getTagValue(col, rowData) {
615
+ return col.tagValue ? col.tagValue(rowData) : String(rowData[col.field] ?? '');
616
+ }
617
+ /** Resolves and validates the tag severity, falling back to 'secondary'. */
618
+ getTagSeverity(col, rowData) {
619
+ const raw = col.tagSeverity ? col.tagSeverity(rowData) : 'secondary';
620
+ const allowed = [
621
+ 'secondary',
622
+ 'success',
623
+ 'info',
624
+ 'warning',
625
+ 'danger',
626
+ 'contrast',
627
+ ];
628
+ return allowed.includes(raw) ? raw : 'secondary';
629
+ }
630
+ /** Resolves whether the tag in a cell is rounded, static or per-row function. */
631
+ getTagRounded(col, rowData) {
632
+ if (typeof col.tagRounded === 'function')
633
+ return col.tagRounded(rowData);
634
+ return col.tagRounded ?? false;
635
+ }
636
+ /** Resolves the icon shown inside a tag cell, static or per-row function. */
637
+ getTagIcon(col, rowData) {
638
+ if (typeof col.tagIcon === 'function')
639
+ return col.tagIcon(rowData);
640
+ return col.tagIcon ?? '';
641
+ }
642
+ /** Resolves the CSS styles of a tag cell, static or per-row function. */
643
+ getTagStyle(col, rowData) {
644
+ return col.tagStyle ? col.tagStyle(rowData) || {} : {};
645
+ }
646
+ /**
647
+ * trackBy for row identity: prefers `id`/`_id` and falls back to the index.
648
+ */
649
+ trackByRow(row, index) {
650
+ return row?.id ?? row?._id ?? index;
651
+ }
652
+ /** Keyboard support (Enter/Space) for sorting a column header. */
653
+ onHeaderKeydown(event, col) {
654
+ if (col.field === 'actions' || !col.sortable)
655
+ return;
656
+ if (event.key === 'Enter' || event.key === ' ') {
657
+ event.preventDefault();
658
+ this.toggleSort(col.field);
659
+ }
660
+ }
661
+ /** Accessible aria-sort value for a given column header. */
662
+ getAriaSort(col) {
663
+ if (col.field === 'actions' || !col.sortable)
664
+ return 'none';
665
+ if (this.sortField() !== col.field)
666
+ return 'none';
667
+ return this.sortOrder() === 1 ? 'ascending' : 'descending';
668
+ }
669
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
670
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: TableComponent, isStandalone: true, selector: "shk-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPage: { classPropertyName: "rowsPerPage", publicName: "rowsPerPage", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageOptions: { classPropertyName: "rowsPerPageOptions", publicName: "rowsPerPageOptions", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, showActionRow: { classPropertyName: "showActionRow", publicName: "showActionRow", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null }, headerActions: { classPropertyName: "headerActions", publicName: "headerActions", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, hasShadow: { classPropertyName: "hasShadow", publicName: "hasShadow", isSignal: true, isRequired: false, transformFunction: null }, defaultSortField: { classPropertyName: "defaultSortField", publicName: "defaultSortField", isSignal: true, isRequired: false, transformFunction: null }, defaultSortOrder: { classPropertyName: "defaultSortOrder", publicName: "defaultSortOrder", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, serverSide: { classPropertyName: "serverSide", publicName: "serverSide", isSignal: true, isRequired: false, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, activeFilter: { classPropertyName: "activeFilter", publicName: "activeFilter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { refresh: "refresh", pageChange: "pageChange", filterChange: "filterChange", searchChange: "searchChange", filterClick: "filterClick" }, ngImport: i0, template: "<div class=\"space-y-2 min-[1440px]:space-y-3 mt-2 min-[1440px]:mt-3 animate-fade-in pb-8\">\n @if (showSearch() || filters().length > 0 || headerActions().length > 0) {\n <div class=\"card p-2 min-[1440px]:p-2.5 flex flex-row items-center justify-between gap-2\">\n <div class=\"flex items-center gap-2 min-[1440px]:gap-3 flex-1 min-w-0\">\n @if (showSearch()) {\n <div class=\"relative w-full md:w-52 min-[1440px]:w-56\">\n <label [for]=\"'shk-table-search'\" class=\"sr-only\">{{\n searchPlaceholder() || ('common.search_placeholder' | transloco)\n }}</label>\n <div class=\"absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none\">\n <svg\n [lucideIcon]=\"getIcon('search')\"\n class=\"w-3.5 h-3.5 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n </div>\n <input\n id=\"shk-table-search\"\n type=\"text\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($any($event.target).value)\"\n [placeholder]=\"searchPlaceholder() || ('common.search_placeholder' | transloco)\"\n class=\"block w-full pl-8 pr-3 py-1.5 text-xs border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md leading-5 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:outline-none focus:bg-white dark:focus:[background-color:var(--shk-surface-800)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)]\"\n />\n </div>\n }\n @if (filters().length > 0) {\n <div\n class=\"flex _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] p-0.5 rounded-md ml-auto\"\n role=\"group\"\n >\n @for (f of filters(); track f.value) {\n <button\n type=\"button\"\n (click)=\"onFilterClick(f.value)\"\n [attr.aria-pressed]=\"f.value === activeFilter()\"\n class=\"px-2.5 min-[1440px]:px-3 py-1 text-xs font-medium rounded _shk-text-surface-700 dark:[color:var(--shk-surface-700)] transition-all\"\n [class]=\"\n f.value === activeFilter()\n ? 'bg-white shadow-sm dark:[background-color:var(--shk-surface-700)]'\n : ''\n \"\n >\n {{ f.label }}\n </button>\n }\n </div>\n }\n </div>\n @if (headerActions().length > 0) {\n <div class=\"flex items-center gap-1.5 shrink-0\">\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"\n 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed ' +\n (action.class || '')\n \"\n [disabled]=\"isHeaderActionDisabled(action)\"\n [attr.aria-label]=\"action.label\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n }\n </div>\n }\n <div class=\"card p-0 overflow-hidden\" [class.shadow-lg]=\"hasShadow()\">\n <div class=\"table-scroll-wrapper _shk-scrollbar\">\n <table>\n <thead>\n <tr>\n @for (col of columnsWithActions(); track col.field) {\n <th\n scope=\"col\"\n (click)=\"col.field !== 'actions' && col.sortable && toggleSort(col.field)\"\n (keydown)=\"onHeaderKeydown($event, col)\"\n [tabindex]=\"col.field !== 'actions' && col.sortable ? 0 : -1\"\n [attr.role]=\"col.field !== 'actions' && col.sortable ? 'button' : null\"\n [attr.aria-sort]=\"getAriaSort(col)\"\n [attr.data-shk-actions]=\"col.field === 'actions' ? '' : null\"\n [style.cursor]=\"col.field !== 'actions' && col.sortable ? 'pointer' : 'default'\"\n [style.width]=\"col.field === 'actions' ? col.width : null\"\n [style.min-width]=\"col.field === 'actions' ? col.width : null\"\n [style.--shk-col-width]=\"col.field !== 'actions' ? getColumnWidth(col) : null\"\n class=\"px-2 min-[1440px]:px-3 py-2 min-[1440px]:py-2.5 text-left text-[10px] min-[1440px]:text-xs font-bold _shk-text-surface-500 dark:[color:var(--shk-surface-400)] uppercase tracking-wider _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-b _shk-border dark:[border-color:var(--shk-surface-200)] focus:outline-none focus-visible:ring-1 focus-visible:[--tw-ring-color:var(--shk-primary-500)]\"\n >\n @if (col.field !== 'actions') {\n <div class=\"flex items-center justify-between gap-2\">\n <span>{{ col.header }}</span>\n <div class=\"flex items-center gap-1\">\n @if (col.sortable) {\n <shk-tooltip [text]=\"'table.tooltips.sort' | transloco\">\n @if (sortField() === col.field) {\n <svg\n [lucideIcon]=\"getIcon(sortOrder() === 1 ? 'arrow-up' : 'arrow-down')\"\n class=\"w-3.5 h-3.5 _shk-text-primary\"\n ></svg>\n } @else {\n <svg\n [lucideIcon]=\"getIcon('arrow-up-down')\"\n class=\"w-3.5 h-3.5 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n }\n </shk-tooltip>\n }\n @if (col.filter) {\n <shk-tooltip [text]=\"'table.tooltips.filter' | transloco\">\n <button\n type=\"button\"\n (click)=\"$event.stopPropagation(); toggleFilter(col.field)\"\n [attr.aria-label]=\"'table.tooltips.filter' | transloco\"\n [attr.aria-expanded]=\"showFilterInput()[col.field] || false\"\n class=\"_shk-text-surface-400 dark:[color:var(--shk-surface-500)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-300)] transition-colors\"\n >\n <svg\n [lucideIcon]=\"getIcon('filter')\"\n class=\"w-3.5 h-3.5\"\n [class._shk-text-primary]=\"hasFilterValue(col.field)\"\n ></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n </div>\n @if (col.filter && showFilterInput()[col.field]) {\n <div\n class=\"mt-2\"\n [formGroup]=\"columnFiltersForm\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"relative\">\n @if (col.filterType !== 'select') {\n <label [for]=\"'shk-filter-' + col.field\" class=\"sr-only\">{{\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n }}</label>\n <input\n [id]=\"'shk-filter-' + col.field\"\n type=\"text\"\n [formControlName]=\"col.field\"\n class=\"w-full px-3 py-1.5 text-sm rounded-md border _shk-border dark:[border-color:var(--shk-surface-200)] bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-500)]\"\n [placeholder]=\"\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n \"\n />\n }\n @if (col.filterType === 'select') {\n <label [for]=\"'shk-filter-' + col.field\" class=\"sr-only\">{{\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n }}</label>\n <select\n [id]=\"'shk-filter-' + col.field\"\n [formControlName]=\"col.field\"\n class=\"w-full px-2 py-1 text-xs rounded-md border _shk-border dark:[border-color:var(--shk-surface-200)] bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] appearance-none\"\n >\n <option [ngValue]=\"null\">{{ 'table.filterAll' | transloco }}</option>\n @for (opt of col.filterOptions || []; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n }\n @if (hasFilterValue(col.field) && col.filterType !== 'select') {\n <button\n type=\"button\"\n class=\"absolute right-2 top-1/2 -translate-y-1/2 _shk-text-surface-400 dark:[color:var(--shk-surface-500)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-300)]\"\n (click)=\"clearFilter(col.field)\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <div class=\"flex items-center justify-center\">\n <span>{{ col.header }}</span>\n </div>\n }\n </th>\n }\n </tr>\n </thead>\n @if (loading()) {\n <tbody class=\"border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <tr>\n <td [attr.colspan]=\"columnsWithActions().length\" class=\"text-center py-12\">\n <div\n class=\"flex items-center justify-center\"\n role=\"status\"\n [attr.aria-label]=\"'common.loading' | transloco\"\n >\n <svg\n [lucideIcon]=\"getIcon('loader')\"\n class=\"w-6 h-6 animate-spin _shk-text-primary\"\n ></svg>\n </div>\n </td>\n </tr>\n </tbody>\n } @else if (effectiveDisplayedData().length === 0) {\n <tbody class=\"border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <tr>\n <td [attr.colspan]=\"columnsWithActions().length\" class=\"text-center py-12\">\n <div class=\"flex flex-col items-center\">\n <div\n class=\"w-16 h-16 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center mb-4\"\n >\n <svg\n [lucideIcon]=\"getIcon('inbox')\"\n class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n </div>\n <p\n class=\"text-lg font-semibold _shk-text-surface-600 dark:[color:var(--shk-surface-300)]\"\n >\n {{ emptyMessage() || ('common.no_records' | transloco) }}\n </p>\n </div>\n </td>\n </tr>\n </tbody>\n } @else {\n <tbody>\n @for (rowData of effectiveDisplayedData(); track trackByRow(rowData, $index)) {\n <tr\n class=\"hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\"\n >\n @for (col of columnsWithActions(); track col.field) {\n <td\n [attr.data-shk-actions]=\"col.field === 'actions' ? '' : null\"\n class=\"px-2 min-[1440px]:px-3 py-2 min-[1440px]:py-2.5 text-xs font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)] whitespace-nowrap\"\n [style.overflow]=\"col.field === 'actions' ? 'visible' : 'hidden'\"\n [style.text-overflow]=\"col.field === 'actions' ? 'clip' : 'ellipsis'\"\n [style.--shk-col-width]=\"col.field !== 'actions' ? getColumnWidth(col) : null\"\n >\n @if (col.field !== 'actions') {\n @if (col.template === 'tag') {\n <shk-tag\n [value]=\"getTagValue(col, rowData)\"\n [severity]=\"getTagSeverity(col, rowData)\"\n [rounded]=\"getTagRounded(col, rowData)\"\n [icon]=\"getTagIcon(col, rowData)\"\n [style]=\"getTagStyle(col, rowData)\"\n />\n } @else if (col.format) {\n <span>{{ col.format(rowData) }}</span>\n } @else {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplates()[col.field] || defaultTemplate;\n context: { $implicit: rowData, field: col.field }\n \"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-start gap-px\">\n @for (action of rowActions(); track $index) {\n @if (isRowActionVisible(action, rowData)) {\n <div class=\"relative group\">\n <shk-tooltip\n [text]=\"getRowActionLabel(action, rowData)\"\n position=\"top\"\n >\n <button\n type=\"button\"\n (click)=\"action.onClick(rowData)\"\n [class]=\"\n 'table-action-btn hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-md ' +\n getRowActionClass(action, rowData)\n \"\n [disabled]=\"isRowActionDisabled(action, rowData) || false\"\n [attr.aria-label]=\"getRowActionLabel(action, rowData)\"\n >\n <svg\n [lucideIcon]=\"getIcon(getRowActionIconName(action, rowData))\"\n class=\"w-4 h-4\"\n ></svg>\n </button>\n </shk-tooltip>\n </div>\n }\n }\n </div>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n }\n </table>\n <ng-template #defaultTemplate let-rowData let-field=\"field\">\n {{ truncate(rowData[field], 30) }}\n </ng-template>\n </div>\n <div\n class=\"flex items-center justify-between gap-4 px-2 min-[1440px]:px-3 py-2 border-t _shk-border dark:[border-color:var(--shk-surface-100)]\"\n >\n <div class=\"text-xs _shk-text-surface-600 dark:[color:var(--shk-surface-400)]\">\n {{ pageReport() }}\n </div>\n <div\n class=\"flex items-center gap-0.5\"\n role=\"navigation\"\n >\n <button\n type=\"button\"\n (click)=\"goToFirst()\"\n [disabled]=\"isFirstPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevrons-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"isFirstPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <span\n class=\"px-2 text-xs font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n >{{ currentPage() }} / {{ totalPages() }}</span\n >\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"isLastPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-4 h-4\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"goToLast()\"\n [disabled]=\"isLastPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevrons-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n <div\n class=\"flex items-center gap-1.5 text-xs _shk-text-surface-600 dark:[color:var(--shk-surface-400)]\"\n >\n <label [for]=\"'shk-rows-per-page'\">{{ 'table.rows' | transloco }}</label>\n <select\n id=\"shk-rows-per-page\"\n [value]=\"rowsPerPageLocal()\"\n (change)=\"onRowsPerPageChange($any($event.target).value)\"\n class=\"border _shk-border dark:[border-color:var(--shk-surface-200)] rounded px-2 py-1 text-xs bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:outline-none focus:[border-color:var(--shk-primary-500)]\"\n >\n @for (opt of rowsPerPageOptions(); track opt) {\n <option [value]=\"opt\">{{ opt }}</option>\n }\n </select>\n </div>\n </div>\n </div>\n</div>\n", styles: ["input:focus{outline:none}input[type=text]{font-weight:400!important}table th,table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td button svg{width:1.125rem!important;height:1.125rem!important}td button:disabled{opacity:.4;cursor:not-allowed}.table-action-btn svg{width:1rem!important;height:1rem!important;min-width:1rem!important;min-height:1rem!important}.table-action-btn{padding:.5rem!important;min-width:2rem!important;min-height:2rem!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.table-scroll-wrapper{width:100%;overflow-x:auto;overflow-y:hidden;border-radius:inherit;-webkit-overflow-scrolling:touch;container-type:inline-size}.table-scroll-wrapper table{table-layout:fixed;width:100%}@container (max-width: 768px){.table-scroll-wrapper{scroll-snap-type:x mandatory}.table-scroll-wrapper table{table-layout:auto;width:100%}.table-scroll-wrapper th:not([data-shk-actions]),.table-scroll-wrapper td:not([data-shk-actions]){min-width:var(--shk-col-width, 140px);max-width:var(--shk-col-width, 140px)}.table-scroll-wrapper th,.table-scroll-wrapper td{scroll-snap-align:start}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: TagComponent, selector: "shk-tag", inputs: ["value", "severity", "rounded", "icon", "style", "styleClass"] }, { kind: "component", type: TooltipComponent, selector: "shk-tooltip", inputs: ["text", "position", "mode", "offset"] }, { kind: "pipe", type: i3.TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
671
+ }
672
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: TableComponent, decorators: [{
673
+ type: Component,
674
+ args: [{ selector: 'shk-table', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
675
+ CommonModule,
676
+ ReactiveFormsModule,
677
+ TranslocoModule,
678
+ LucideDynamicIcon,
679
+ TagComponent,
680
+ TooltipComponent,
681
+ ], template: "<div class=\"space-y-2 min-[1440px]:space-y-3 mt-2 min-[1440px]:mt-3 animate-fade-in pb-8\">\n @if (showSearch() || filters().length > 0 || headerActions().length > 0) {\n <div class=\"card p-2 min-[1440px]:p-2.5 flex flex-row items-center justify-between gap-2\">\n <div class=\"flex items-center gap-2 min-[1440px]:gap-3 flex-1 min-w-0\">\n @if (showSearch()) {\n <div class=\"relative w-full md:w-52 min-[1440px]:w-56\">\n <label [for]=\"'shk-table-search'\" class=\"sr-only\">{{\n searchPlaceholder() || ('common.search_placeholder' | transloco)\n }}</label>\n <div class=\"absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none\">\n <svg\n [lucideIcon]=\"getIcon('search')\"\n class=\"w-3.5 h-3.5 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n </div>\n <input\n id=\"shk-table-search\"\n type=\"text\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($any($event.target).value)\"\n [placeholder]=\"searchPlaceholder() || ('common.search_placeholder' | transloco)\"\n class=\"block w-full pl-8 pr-3 py-1.5 text-xs border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md leading-5 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:outline-none focus:bg-white dark:focus:[background-color:var(--shk-surface-800)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)]\"\n />\n </div>\n }\n @if (filters().length > 0) {\n <div\n class=\"flex _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] p-0.5 rounded-md ml-auto\"\n role=\"group\"\n >\n @for (f of filters(); track f.value) {\n <button\n type=\"button\"\n (click)=\"onFilterClick(f.value)\"\n [attr.aria-pressed]=\"f.value === activeFilter()\"\n class=\"px-2.5 min-[1440px]:px-3 py-1 text-xs font-medium rounded _shk-text-surface-700 dark:[color:var(--shk-surface-700)] transition-all\"\n [class]=\"\n f.value === activeFilter()\n ? 'bg-white shadow-sm dark:[background-color:var(--shk-surface-700)]'\n : ''\n \"\n >\n {{ f.label }}\n </button>\n }\n </div>\n }\n </div>\n @if (headerActions().length > 0) {\n <div class=\"flex items-center gap-1.5 shrink-0\">\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"\n 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed ' +\n (action.class || '')\n \"\n [disabled]=\"isHeaderActionDisabled(action)\"\n [attr.aria-label]=\"action.label\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n }\n </div>\n }\n <div class=\"card p-0 overflow-hidden\" [class.shadow-lg]=\"hasShadow()\">\n <div class=\"table-scroll-wrapper _shk-scrollbar\">\n <table>\n <thead>\n <tr>\n @for (col of columnsWithActions(); track col.field) {\n <th\n scope=\"col\"\n (click)=\"col.field !== 'actions' && col.sortable && toggleSort(col.field)\"\n (keydown)=\"onHeaderKeydown($event, col)\"\n [tabindex]=\"col.field !== 'actions' && col.sortable ? 0 : -1\"\n [attr.role]=\"col.field !== 'actions' && col.sortable ? 'button' : null\"\n [attr.aria-sort]=\"getAriaSort(col)\"\n [attr.data-shk-actions]=\"col.field === 'actions' ? '' : null\"\n [style.cursor]=\"col.field !== 'actions' && col.sortable ? 'pointer' : 'default'\"\n [style.width]=\"col.field === 'actions' ? col.width : null\"\n [style.min-width]=\"col.field === 'actions' ? col.width : null\"\n [style.--shk-col-width]=\"col.field !== 'actions' ? getColumnWidth(col) : null\"\n class=\"px-2 min-[1440px]:px-3 py-2 min-[1440px]:py-2.5 text-left text-[10px] min-[1440px]:text-xs font-bold _shk-text-surface-500 dark:[color:var(--shk-surface-400)] uppercase tracking-wider _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-b _shk-border dark:[border-color:var(--shk-surface-200)] focus:outline-none focus-visible:ring-1 focus-visible:[--tw-ring-color:var(--shk-primary-500)]\"\n >\n @if (col.field !== 'actions') {\n <div class=\"flex items-center justify-between gap-2\">\n <span>{{ col.header }}</span>\n <div class=\"flex items-center gap-1\">\n @if (col.sortable) {\n <shk-tooltip [text]=\"'table.tooltips.sort' | transloco\">\n @if (sortField() === col.field) {\n <svg\n [lucideIcon]=\"getIcon(sortOrder() === 1 ? 'arrow-up' : 'arrow-down')\"\n class=\"w-3.5 h-3.5 _shk-text-primary\"\n ></svg>\n } @else {\n <svg\n [lucideIcon]=\"getIcon('arrow-up-down')\"\n class=\"w-3.5 h-3.5 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n }\n </shk-tooltip>\n }\n @if (col.filter) {\n <shk-tooltip [text]=\"'table.tooltips.filter' | transloco\">\n <button\n type=\"button\"\n (click)=\"$event.stopPropagation(); toggleFilter(col.field)\"\n [attr.aria-label]=\"'table.tooltips.filter' | transloco\"\n [attr.aria-expanded]=\"showFilterInput()[col.field] || false\"\n class=\"_shk-text-surface-400 dark:[color:var(--shk-surface-500)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-300)] transition-colors\"\n >\n <svg\n [lucideIcon]=\"getIcon('filter')\"\n class=\"w-3.5 h-3.5\"\n [class._shk-text-primary]=\"hasFilterValue(col.field)\"\n ></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n </div>\n @if (col.filter && showFilterInput()[col.field]) {\n <div\n class=\"mt-2\"\n [formGroup]=\"columnFiltersForm\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"relative\">\n @if (col.filterType !== 'select') {\n <label [for]=\"'shk-filter-' + col.field\" class=\"sr-only\">{{\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n }}</label>\n <input\n [id]=\"'shk-filter-' + col.field\"\n type=\"text\"\n [formControlName]=\"col.field\"\n class=\"w-full px-3 py-1.5 text-sm rounded-md border _shk-border dark:[border-color:var(--shk-surface-200)] bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-500)]\"\n [placeholder]=\"\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n \"\n />\n }\n @if (col.filterType === 'select') {\n <label [for]=\"'shk-filter-' + col.field\" class=\"sr-only\">{{\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n }}</label>\n <select\n [id]=\"'shk-filter-' + col.field\"\n [formControlName]=\"col.field\"\n class=\"w-full px-2 py-1 text-xs rounded-md border _shk-border dark:[border-color:var(--shk-surface-200)] bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] appearance-none\"\n >\n <option [ngValue]=\"null\">{{ 'table.filterAll' | transloco }}</option>\n @for (opt of col.filterOptions || []; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n }\n @if (hasFilterValue(col.field) && col.filterType !== 'select') {\n <button\n type=\"button\"\n class=\"absolute right-2 top-1/2 -translate-y-1/2 _shk-text-surface-400 dark:[color:var(--shk-surface-500)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-300)]\"\n (click)=\"clearFilter(col.field)\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <div class=\"flex items-center justify-center\">\n <span>{{ col.header }}</span>\n </div>\n }\n </th>\n }\n </tr>\n </thead>\n @if (loading()) {\n <tbody class=\"border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <tr>\n <td [attr.colspan]=\"columnsWithActions().length\" class=\"text-center py-12\">\n <div\n class=\"flex items-center justify-center\"\n role=\"status\"\n [attr.aria-label]=\"'common.loading' | transloco\"\n >\n <svg\n [lucideIcon]=\"getIcon('loader')\"\n class=\"w-6 h-6 animate-spin _shk-text-primary\"\n ></svg>\n </div>\n </td>\n </tr>\n </tbody>\n } @else if (effectiveDisplayedData().length === 0) {\n <tbody class=\"border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <tr>\n <td [attr.colspan]=\"columnsWithActions().length\" class=\"text-center py-12\">\n <div class=\"flex flex-col items-center\">\n <div\n class=\"w-16 h-16 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center mb-4\"\n >\n <svg\n [lucideIcon]=\"getIcon('inbox')\"\n class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n </div>\n <p\n class=\"text-lg font-semibold _shk-text-surface-600 dark:[color:var(--shk-surface-300)]\"\n >\n {{ emptyMessage() || ('common.no_records' | transloco) }}\n </p>\n </div>\n </td>\n </tr>\n </tbody>\n } @else {\n <tbody>\n @for (rowData of effectiveDisplayedData(); track trackByRow(rowData, $index)) {\n <tr\n class=\"hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\"\n >\n @for (col of columnsWithActions(); track col.field) {\n <td\n [attr.data-shk-actions]=\"col.field === 'actions' ? '' : null\"\n class=\"px-2 min-[1440px]:px-3 py-2 min-[1440px]:py-2.5 text-xs font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)] whitespace-nowrap\"\n [style.overflow]=\"col.field === 'actions' ? 'visible' : 'hidden'\"\n [style.text-overflow]=\"col.field === 'actions' ? 'clip' : 'ellipsis'\"\n [style.--shk-col-width]=\"col.field !== 'actions' ? getColumnWidth(col) : null\"\n >\n @if (col.field !== 'actions') {\n @if (col.template === 'tag') {\n <shk-tag\n [value]=\"getTagValue(col, rowData)\"\n [severity]=\"getTagSeverity(col, rowData)\"\n [rounded]=\"getTagRounded(col, rowData)\"\n [icon]=\"getTagIcon(col, rowData)\"\n [style]=\"getTagStyle(col, rowData)\"\n />\n } @else if (col.format) {\n <span>{{ col.format(rowData) }}</span>\n } @else {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplates()[col.field] || defaultTemplate;\n context: { $implicit: rowData, field: col.field }\n \"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-start gap-px\">\n @for (action of rowActions(); track $index) {\n @if (isRowActionVisible(action, rowData)) {\n <div class=\"relative group\">\n <shk-tooltip\n [text]=\"getRowActionLabel(action, rowData)\"\n position=\"top\"\n >\n <button\n type=\"button\"\n (click)=\"action.onClick(rowData)\"\n [class]=\"\n 'table-action-btn hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-md ' +\n getRowActionClass(action, rowData)\n \"\n [disabled]=\"isRowActionDisabled(action, rowData) || false\"\n [attr.aria-label]=\"getRowActionLabel(action, rowData)\"\n >\n <svg\n [lucideIcon]=\"getIcon(getRowActionIconName(action, rowData))\"\n class=\"w-4 h-4\"\n ></svg>\n </button>\n </shk-tooltip>\n </div>\n }\n }\n </div>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n }\n </table>\n <ng-template #defaultTemplate let-rowData let-field=\"field\">\n {{ truncate(rowData[field], 30) }}\n </ng-template>\n </div>\n <div\n class=\"flex items-center justify-between gap-4 px-2 min-[1440px]:px-3 py-2 border-t _shk-border dark:[border-color:var(--shk-surface-100)]\"\n >\n <div class=\"text-xs _shk-text-surface-600 dark:[color:var(--shk-surface-400)]\">\n {{ pageReport() }}\n </div>\n <div\n class=\"flex items-center gap-0.5\"\n role=\"navigation\"\n >\n <button\n type=\"button\"\n (click)=\"goToFirst()\"\n [disabled]=\"isFirstPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevrons-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"isFirstPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <span\n class=\"px-2 text-xs font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n >{{ currentPage() }} / {{ totalPages() }}</span\n >\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"isLastPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-4 h-4\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"goToLast()\"\n [disabled]=\"isLastPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevrons-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n <div\n class=\"flex items-center gap-1.5 text-xs _shk-text-surface-600 dark:[color:var(--shk-surface-400)]\"\n >\n <label [for]=\"'shk-rows-per-page'\">{{ 'table.rows' | transloco }}</label>\n <select\n id=\"shk-rows-per-page\"\n [value]=\"rowsPerPageLocal()\"\n (change)=\"onRowsPerPageChange($any($event.target).value)\"\n class=\"border _shk-border dark:[border-color:var(--shk-surface-200)] rounded px-2 py-1 text-xs bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:outline-none focus:[border-color:var(--shk-primary-500)]\"\n >\n @for (opt of rowsPerPageOptions(); track opt) {\n <option [value]=\"opt\">{{ opt }}</option>\n }\n </select>\n </div>\n </div>\n </div>\n</div>\n", styles: ["input:focus{outline:none}input[type=text]{font-weight:400!important}table th,table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td button svg{width:1.125rem!important;height:1.125rem!important}td button:disabled{opacity:.4;cursor:not-allowed}.table-action-btn svg{width:1rem!important;height:1rem!important;min-width:1rem!important;min-height:1rem!important}.table-action-btn{padding:.5rem!important;min-width:2rem!important;min-height:2rem!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.table-scroll-wrapper{width:100%;overflow-x:auto;overflow-y:hidden;border-radius:inherit;-webkit-overflow-scrolling:touch;container-type:inline-size}.table-scroll-wrapper table{table-layout:fixed;width:100%}@container (max-width: 768px){.table-scroll-wrapper{scroll-snap-type:x mandatory}.table-scroll-wrapper table{table-layout:auto;width:100%}.table-scroll-wrapper th:not([data-shk-actions]),.table-scroll-wrapper td:not([data-shk-actions]){min-width:var(--shk-col-width, 140px);max-width:var(--shk-col-width, 140px)}.table-scroll-wrapper th,.table-scroll-wrapper td{scroll-snap-align:start}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
682
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rowsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPage", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], showActionRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActionRow", required: false }] }], customTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "customTemplates", required: false }] }], headerActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerActions", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], hasShadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasShadow", required: false }] }], defaultSortField: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultSortField", required: false }] }], defaultSortOrder: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultSortOrder", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], serverSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverSide", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }], activeFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeFilter", required: false }] }], refresh: [{ type: i0.Output, args: ["refresh"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], filterChange: [{ type: i0.Output, args: ["filterChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], filterClick: [{ type: i0.Output, args: ["filterClick"] }] } });
683
+
684
+ /**
685
+ * Generated bundle index. Do not edit.
686
+ */
687
+
688
+ export { TableComponent };
689
+ //# sourceMappingURL=shirkasoft-ui-components-table.mjs.map