mn-angular-lib 1.0.125 → 1.0.127

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.
@@ -4968,6 +4968,140 @@ var ColumnSortType;
4968
4968
  ColumnSortType["NONE"] = "NONE";
4969
4969
  })(ColumnSortType || (ColumnSortType = {}));
4970
4970
 
4971
+ /**
4972
+ * Pure helpers backing mn-table's per-column filters: the empty value and
4973
+ * "is it set?" test for each filter type, and the default client-side predicate
4974
+ * applied when a column supplies no `filterFn`.
4975
+ *
4976
+ * Kept free of Angular so the filter semantics can be unit-tested directly.
4977
+ */
4978
+ /** The reset/unset value for a filter type. */
4979
+ function emptyFilterValue(type) {
4980
+ switch (type) {
4981
+ case 'multi-select':
4982
+ return [];
4983
+ case 'number-range':
4984
+ return {};
4985
+ case 'date-range':
4986
+ return {};
4987
+ case 'boolean':
4988
+ case 'text':
4989
+ case 'select':
4990
+ default:
4991
+ return '';
4992
+ }
4993
+ }
4994
+ /**
4995
+ * Whether a filter value should actually narrow the rows. Empty strings, empty
4996
+ * arrays and ranges with neither bound set are inactive; `false` on a boolean
4997
+ * filter is active (it means "show only the false rows"), which is why a plain
4998
+ * truthiness check is not enough.
4999
+ */
5000
+ function isFilterValueActive(value) {
5001
+ if (value === undefined || value === null)
5002
+ return false;
5003
+ if (typeof value === 'boolean')
5004
+ return true;
5005
+ if (typeof value === 'string')
5006
+ return value.trim().length > 0;
5007
+ if (Array.isArray(value))
5008
+ return value.length > 0;
5009
+ const range = value;
5010
+ return [range.min, range.max, range.from, range.to].some(bound => bound !== undefined && bound !== null && bound !== '' && !Number.isNaN(bound));
5011
+ }
5012
+ /**
5013
+ * The value a filter compares against for a row: the column's
5014
+ * `getRawValueToSort` when present (the only option for template cells, which
5015
+ * have no string to read), otherwise the rendered cell string.
5016
+ */
5017
+ function resolveFilterableValue(column, row) {
5018
+ if (column.getRawValueToSort)
5019
+ return column.getRawValueToSort(row);
5020
+ if (typeof column.cell === 'function')
5021
+ return column.cell(row);
5022
+ return '';
5023
+ }
5024
+ /** Parses a `YYYY-MM-DD` bound to a timestamp; `endOfDay` makes the upper bound inclusive. */
5025
+ function parseDateBound(bound, endOfDay) {
5026
+ if (!bound)
5027
+ return undefined;
5028
+ const time = new Date(endOfDay ? `${bound}T23:59:59.999` : `${bound}T00:00:00.000`).getTime();
5029
+ return Number.isNaN(time) ? undefined : time;
5030
+ }
5031
+ /** Coerces a raw cell value to a timestamp for date-range comparison. */
5032
+ function toTimestamp(raw) {
5033
+ if (raw instanceof Date)
5034
+ return raw.getTime();
5035
+ if (typeof raw === 'number')
5036
+ return raw;
5037
+ return new Date(String(raw)).getTime();
5038
+ }
5039
+ /**
5040
+ * The default predicate for a filter type, used when the column supplies no
5041
+ * `filterFn`. Semantics per type:
5042
+ * - `text` — case-insensitive substring match
5043
+ * - `select` — exact string equality
5044
+ * - `multi-select` — equality against any selected value (OR)
5045
+ * - `boolean` — truthiness of the raw value equals the chosen state
5046
+ * - `number-range` / `date-range` — inclusive bounds, each side optional
5047
+ *
5048
+ * A row whose raw value cannot be interpreted for the type (a non-numeric value
5049
+ * under a number range, an unparsable date) is excluded rather than kept, so an
5050
+ * active filter never silently passes rows it cannot evaluate.
5051
+ */
5052
+ function defaultFilterPredicate(type, raw, value) {
5053
+ switch (type) {
5054
+ case 'select':
5055
+ return String(raw ?? '') === String(value);
5056
+ case 'multi-select': {
5057
+ const selected = value;
5058
+ return selected.some(option => String(raw ?? '') === option);
5059
+ }
5060
+ case 'boolean':
5061
+ return Boolean(raw) === value;
5062
+ case 'number-range': {
5063
+ const { min, max } = value;
5064
+ const numeric = typeof raw === 'number' ? raw : Number(raw);
5065
+ if (Number.isNaN(numeric))
5066
+ return false;
5067
+ if (min !== undefined && numeric < min)
5068
+ return false;
5069
+ return !(max !== undefined && numeric > max);
5070
+ }
5071
+ case 'date-range': {
5072
+ const { from, to } = value;
5073
+ const time = toTimestamp(raw);
5074
+ if (Number.isNaN(time))
5075
+ return false;
5076
+ const start = parseDateBound(from, false);
5077
+ const end = parseDateBound(to, true);
5078
+ if (start !== undefined && time < start)
5079
+ return false;
5080
+ return !(end !== undefined && time > end);
5081
+ }
5082
+ case 'text':
5083
+ default:
5084
+ return String(raw ?? '')
5085
+ .toLowerCase()
5086
+ .includes(String(value).trim().toLowerCase());
5087
+ }
5088
+ }
5089
+ /**
5090
+ * Whether a row passes a column's active filter — the column's own `filterFn`
5091
+ * when it has one, otherwise {@link defaultFilterPredicate}.
5092
+ *
5093
+ * `filterFn` is declared per filter type on {@link ColumnDefinition}, so at this
5094
+ * generic call site the union of signatures is not callable and the value shape
5095
+ * is widened once here. Consumers keep the precise per-type signature where they
5096
+ * declare the column, which is where it matters.
5097
+ */
5098
+ function matchesColumnFilter(column, row, value) {
5099
+ const filterFn = column.filterFn;
5100
+ if (filterFn)
5101
+ return filterFn(row, value);
5102
+ return defaultFilterPredicate(column.filterType ?? 'text', resolveFilterableValue(column, row), value);
5103
+ }
5104
+
4971
5105
  const mnSkeletonVariants = tv({
4972
5106
  base: 'bg-base-300 relative overflow-hidden',
4973
5107
  variants: {
@@ -5764,8 +5898,19 @@ class MnTable extends MnSelectableCollectionBase {
5764
5898
  currentSort = null;
5765
5899
  /** Per-column filter values keyed by column key. */
5766
5900
  columnFilters = {};
5901
+ /** Filter types compact enough to render directly inside the header cell. */
5902
+ static INLINE_FILTER_TYPES = ['text', 'select', 'boolean'];
5903
+ /** Width (px) of the filter popover, mirrored from its `w-64` class for clamping. */
5904
+ static POPOVER_WIDTH = 256;
5767
5905
  /** Viewport width (px) below which the inline filter row collapses into a panel. */
5768
5906
  static FILTER_COLLAPSE_WIDTH = 640;
5907
+ /**
5908
+ * Key of the column whose filter popover is open, or `null` when none is.
5909
+ * Only the rich filter types ({@link isPopoverFilter}) use a popover.
5910
+ */
5911
+ openFilterKey = null;
5912
+ /** Bounds rendered by a number-range filter, in input order. */
5913
+ numberBounds = ['min', 'max'];
5769
5914
  /**
5770
5915
  * True when the viewport is narrow enough that the per-column filter inputs no
5771
5916
  * longer fit under their headers; the inline row is then replaced by a toggle
@@ -5779,13 +5924,152 @@ class MnTable extends MnSelectableCollectionBase {
5779
5924
  return this.dataSource?.toolbarLeftTemplate;
5780
5925
  }
5781
5926
  collectionBody;
5782
- /** Updates a column filter value and re-applies filtering. */
5783
- onColumnFilter(columnKey, value) {
5784
- this.columnFilters[columnKey] = value;
5927
+ // ── Column Filters ──
5928
+ /** Bounds rendered by a date-range filter, in input order. */
5929
+ dateBounds = ['from', 'to'];
5930
+ /** Viewport coordinates of the open filter popover. */
5931
+ popoverPosition = { top: 0, left: 0 };
5932
+ /** Debounces server-side text filters so typing doesn't fire a request per keystroke. */
5933
+ filterDebounce = new Subject();
5934
+ /** The open filter popover, used to tell inside clicks from outside ones. */
5935
+ filterPopover;
5936
+ constructor() {
5937
+ super();
5938
+ // Server-side text filtering only: client-side filtering stays instant per keystroke.
5939
+ this.filterDebounce
5940
+ .pipe(debounceTime(300), takeUntilDestroyed())
5941
+ .subscribe(() => {
5942
+ this.emitServerFilters();
5943
+ this.cdr.markForCheck();
5944
+ });
5945
+ }
5946
+ /** Whether the consumer owns filtering (server-side), mirroring {@link isServerSearched}. */
5947
+ get isServerFiltered() {
5948
+ return !!this.dataSource.onColumnFilterChange;
5949
+ }
5950
+ /** Every column filter that is actually set, in column order. */
5951
+ get activeColumnFilters() {
5952
+ return this.dataSource.columns
5953
+ .filter(col => col.filterable && isFilterValueActive(this.columnFilters[col.key]))
5954
+ .map(col => ({
5955
+ key: col.key,
5956
+ type: this.filterTypeOf(col),
5957
+ value: this.columnFilters[col.key],
5958
+ }));
5959
+ }
5960
+ /** Whether at least one column filter is active. */
5961
+ get hasActiveFilters() {
5962
+ return this.dataSource.columns.some(col => col.filterable && isFilterValueActive(this.columnFilters[col.key]));
5963
+ }
5964
+ /**
5965
+ * Updates a column filter value and either re-filters locally or hands the
5966
+ * active filters to the consumer. Server-side text filters are debounced;
5967
+ * every other type commits immediately.
5968
+ */
5969
+ onColumnFilter(column, value) {
5970
+ this.columnFilters[column.key] = value;
5785
5971
  this.currentPage = 1;
5786
- this.applyFilter(false);
5972
+ if (this.isServerFiltered) {
5973
+ if (this.filterTypeOf(column) === 'text') {
5974
+ this.filterDebounce.next();
5975
+ }
5976
+ else {
5977
+ this.emitServerFilters();
5978
+ }
5979
+ }
5980
+ else {
5981
+ this.applyFilter(false);
5982
+ }
5787
5983
  this.cdr.markForCheck();
5788
5984
  }
5985
+ /** Updates one bound of a range filter, leaving the other side untouched. */
5986
+ onRangeFilter(column, bound, raw) {
5987
+ const current = { ...(this.columnFilters[column.key] ?? {}) };
5988
+ if (bound === 'min' || bound === 'max') {
5989
+ const parsed = Number(raw);
5990
+ // An emptied input clears that bound rather than pinning it to 0.
5991
+ if (raw === '' || Number.isNaN(parsed))
5992
+ delete current[bound];
5993
+ else
5994
+ current[bound] = parsed;
5995
+ }
5996
+ else {
5997
+ if (raw === '')
5998
+ delete current[bound];
5999
+ else
6000
+ current[bound] = raw;
6001
+ }
6002
+ this.onColumnFilter(column, current);
6003
+ }
6004
+ /** Updates a tri-state boolean filter from its select ('' = any). */
6005
+ onBooleanFilter(column, raw) {
6006
+ this.onColumnFilter(column, raw === '' ? '' : raw === 'true');
6007
+ }
6008
+ /** The effective filter type of a column, defaulting to text. */
6009
+ filterTypeOf(column) {
6010
+ return column.filterType ?? 'text';
6011
+ }
6012
+ /** Whether a column's filter renders inline in the header cell (vs. in a popover). */
6013
+ isInlineFilter(column) {
6014
+ return MnTable.INLINE_FILTER_TYPES.includes(this.filterTypeOf(column));
6015
+ }
6016
+ /** Whether a column's filter is rich enough to need the popover panel. */
6017
+ isPopoverFilter(column) {
6018
+ return !!column.filterable && !this.isInlineFilter(column);
6019
+ }
6020
+ /** Whether a specific column's filter currently narrows the rows. */
6021
+ isColumnFilterActive(column) {
6022
+ return isFilterValueActive(this.columnFilters[column.key]);
6023
+ }
6024
+ /** The column whose filter popover is open, or `null`. */
6025
+ openFilterColumn() {
6026
+ if (this.openFilterKey === null)
6027
+ return null;
6028
+ return this.dataSource.columns.find(col => col.key === this.openFilterKey) ?? null;
6029
+ }
6030
+ /**
6031
+ * Opens/closes a column's filter popover, closing any other that was open, and
6032
+ * anchors it under the trigger. The popover is positioned `fixed` from the
6033
+ * trigger's viewport rect because it renders outside the table's
6034
+ * `overflow-x-auto` wrapper, which would otherwise clip it.
6035
+ */
6036
+ toggleFilterPopover(column, event) {
6037
+ if (this.openFilterKey === column.key) {
6038
+ this.openFilterKey = null;
6039
+ return;
6040
+ }
6041
+ const trigger = event.currentTarget;
6042
+ if (trigger) {
6043
+ const rect = trigger.getBoundingClientRect();
6044
+ const maxLeft = window.innerWidth - MnTable.POPOVER_WIDTH - 8;
6045
+ this.popoverPosition = { top: rect.bottom + 4, left: Math.max(8, Math.min(rect.left, maxLeft)) };
6046
+ }
6047
+ this.openFilterKey = column.key;
6048
+ }
6049
+ /** Resets a single column's filter (from its popover) and re-applies filtering. */
6050
+ clearColumnFilter(column) {
6051
+ this.onColumnFilter(column, emptyFilterValue(this.filterTypeOf(column)));
6052
+ }
6053
+ /** Closes the filter popover, if one is open. */
6054
+ closeFilterPopover() {
6055
+ if (this.openFilterKey === null)
6056
+ return;
6057
+ this.openFilterKey = null;
6058
+ this.cdr.markForCheck();
6059
+ }
6060
+ /** Filter options formatted for mn-multi-select for a given column. */
6061
+ getFilterMultiSelectOptions(column) {
6062
+ return (column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) }));
6063
+ }
6064
+ /** Any / Yes / No options for a boolean column filter. */
6065
+ getBooleanFilterOptions(column) {
6066
+ const labels = this.dataSource.filterLabels;
6067
+ return [
6068
+ { label: column.filterPlaceholder ?? labels?.any ?? 'Any', value: '' },
6069
+ { label: labels?.yes ?? 'Yes', value: 'true' },
6070
+ { label: labels?.no ?? 'No', value: 'false' },
6071
+ ];
6072
+ }
5789
6073
  /** Filter options formatted for mn-select for a given column. */
5790
6074
  getFilterSelectOptions(column) {
5791
6075
  const placeholder = column.filterPlaceholder ?? 'All';
@@ -5794,14 +6078,73 @@ class MnTable extends MnSelectableCollectionBase {
5794
6078
  ...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
5795
6079
  ];
5796
6080
  }
5797
- // ── Column Filters ──
6081
+ /** Current text/select filter value for a column. */
6082
+ textFilterValue(column) {
6083
+ const value = this.columnFilters[column.key];
6084
+ return typeof value === 'string' ? value : '';
6085
+ }
6086
+ /** Current multi-select filter value for a column. */
6087
+ multiFilterValue(column) {
6088
+ const value = this.columnFilters[column.key];
6089
+ return Array.isArray(value) ? value : [];
6090
+ }
6091
+ /** Current boolean filter value for a column, as the select's string value. */
6092
+ booleanFilterValue(column) {
6093
+ const value = this.columnFilters[column.key];
6094
+ return typeof value === 'boolean' ? String(value) : '';
6095
+ }
6096
+ /** Current value of one bound of a range filter, as an input-ready string. */
6097
+ rangeFilterValue(column, bound) {
6098
+ const value = this.columnFilters[column.key];
6099
+ const bounded = value?.[bound];
6100
+ return bounded === undefined || bounded === null ? '' : String(bounded);
6101
+ }
6102
+ /** Label for a range filter bound, falling back to the English default. */
6103
+ rangeBoundLabel(bound) {
6104
+ const labels = this.dataSource.filterLabels;
6105
+ switch (bound) {
6106
+ case 'min':
6107
+ return labels?.min ?? 'Min';
6108
+ case 'max':
6109
+ return labels?.max ?? 'Max';
6110
+ case 'from':
6111
+ return labels?.from ?? 'From';
6112
+ case 'to':
6113
+ return labels?.to ?? 'To';
6114
+ }
6115
+ }
6116
+ /** Resets every column filter and re-applies (or re-requests) filtering. */
6117
+ clearAllFilters() {
6118
+ this.seedFilterValues();
6119
+ this.currentPage = 1;
6120
+ if (this.isServerFiltered) {
6121
+ this.emitServerFilters();
6122
+ }
6123
+ else {
6124
+ this.applyFilter(false);
6125
+ }
6126
+ this.cdr.markForCheck();
6127
+ }
6128
+ /**
6129
+ * Closes the filter popover when the click landed outside it. Membership is
6130
+ * tested against the popover element rather than stopping propagation inside
6131
+ * it, so the popover's own controls stay ordinary, focusable elements.
6132
+ */
6133
+ onDocumentClick(event) {
6134
+ if (this.openFilterKey === null)
6135
+ return;
6136
+ const target = event.target;
6137
+ if (target && this.filterPopover?.nativeElement.contains(target))
6138
+ return;
6139
+ this.closeFilterPopover();
6140
+ }
5798
6141
  /** Whether any column has filtering enabled. */
5799
6142
  get hasColumnFilters() {
5800
6143
  return this.dataSource.columns.some(c => c.filterable);
5801
6144
  }
5802
- /** Whether at least one column filter is active. */
5803
- get hasActiveFilters() {
5804
- return this.dataSource.columns.some(col => col.filterable && !!this.columnFilters[col.key]);
6145
+ /** Closes the filter popover on Escape. */
6146
+ onEscape() {
6147
+ this.closeFilterPopover();
5805
6148
  }
5806
6149
  /** Label for the small-screen filters toggle button. */
5807
6150
  get filtersButtonLabel() {
@@ -5815,15 +6158,23 @@ class MnTable extends MnSelectableCollectionBase {
5815
6158
  toggleFiltersPanel() {
5816
6159
  this.filtersPanelOpen = !this.filtersPanelOpen;
5817
6160
  }
5818
- /** Resets every column filter and re-applies filtering. */
5819
- clearAllFilters() {
5820
- for (const col of this.dataSource.columns) {
5821
- if (col.filterable)
5822
- this.columnFilters[col.key] = '';
5823
- }
5824
- this.currentPage = 1;
5825
- this.applyFilter(false);
5826
- this.cdr.markForCheck();
6161
+ /** Re-evaluate responsive page size and filter layout when the viewport changes. */
6162
+ onWindowResize() {
6163
+ this.applyResponsivePageSize(true);
6164
+ this.updateFilterLayout(true);
6165
+ // The popover is anchored to a viewport rect that a resize invalidates.
6166
+ this.closeFilterPopover();
6167
+ }
6168
+ /** Sets sort/filter state seeded from the data source before the first filter pass. */
6169
+ beforeInitialFilter() {
6170
+ super.beforeInitialFilter();
6171
+ // Force the mobile row count below `md`; use the consumer's pageSize (or 10) above it.
6172
+ this.desktopPageSize = this.dataSource.pageSize ?? 10;
6173
+ this.applyResponsivePageSize(false);
6174
+ // Seed the filter layout for the initial viewport (no markForCheck pre-render).
6175
+ this.updateFilterLayout(false);
6176
+ this.currentSort = this.dataSource.defaultSort ?? null;
6177
+ this.seedFilterValues();
5827
6178
  }
5828
6179
  /** True when the viewport is below the filter-collapse breakpoint. */
5829
6180
  isFilterViewport() {
@@ -5916,30 +6267,52 @@ class MnTable extends MnSelectableCollectionBase {
5916
6267
  if (reflow)
5917
6268
  this.cdr.markForCheck();
5918
6269
  }
5919
- /** Re-evaluate responsive page size and filter layout when the viewport changes. */
5920
- onWindowResize() {
5921
- this.applyResponsivePageSize(true);
5922
- this.updateFilterLayout(true);
6270
+ /**
6271
+ * Resolves table-specific translation keys (column headers/filters) plus the
6272
+ * shared keys handled by the base.
6273
+ */
6274
+ resolveTranslationKeys() {
6275
+ super.resolveTranslationKeys();
6276
+ for (const col of this.dataSource.columns) {
6277
+ if (col.headerKey) {
6278
+ col.header = this.lang.t(col.headerKey);
6279
+ }
6280
+ if (col.filterPlaceholderKey) {
6281
+ col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
6282
+ }
6283
+ }
6284
+ if (this.dataSource.filtersLabelKey) {
6285
+ this.dataSource.filtersLabel = this.lang.t(this.dataSource.filtersLabelKey);
6286
+ }
6287
+ if (this.dataSource.clearFiltersLabelKey) {
6288
+ this.dataSource.clearFiltersLabel = this.lang.t(this.dataSource.clearFiltersLabelKey);
6289
+ }
6290
+ this.resolveFilterLabelKeys();
5923
6291
  }
5924
6292
  /** Tracks the desktop page size when the user picks one (selector only shows at >= md). */
5925
6293
  onPageSizeChange(newSize) {
5926
6294
  this.desktopPageSize = newSize;
5927
6295
  super.onPageSizeChange(newSize);
5928
6296
  }
5929
- /** Sets sort/filter state seeded from the data source before the first filter pass. */
5930
- beforeInitialFilter() {
5931
- super.beforeInitialFilter();
5932
- // Force the mobile row count below `md`; use the consumer's pageSize (or 10) above it.
5933
- this.desktopPageSize = this.dataSource.pageSize ?? 10;
5934
- this.applyResponsivePageSize(false);
5935
- // Seed the filter layout for the initial viewport (no markForCheck pre-render).
5936
- this.updateFilterLayout(false);
5937
- this.currentSort = this.dataSource.defaultSort ?? null;
5938
- for (const col of this.dataSource.columns) {
5939
- if (col.filterable) {
5940
- this.columnFilters[col.key] = '';
6297
+ applyFilter(searchForItems) {
6298
+ let items = this.applySearchFilter(this.dataSource.dataRows.value ?? []);
6299
+ // Per-column filters. Skipped entirely when the consumer owns filtering: the
6300
+ // rows already are the filtered set, and re-filtering them locally would
6301
+ // narrow the current page a second time.
6302
+ if (!this.isServerFiltered) {
6303
+ for (const col of this.dataSource.columns) {
6304
+ const filterValue = this.columnFilters[col.key];
6305
+ if (!col.filterable || !isFilterValueActive(filterValue))
6306
+ continue;
6307
+ items = items.filter(row => matchesColumnFilter(col, row, filterValue));
5941
6308
  }
5942
6309
  }
6310
+ items = this.applySorting(items);
6311
+ this.filteredItems = items;
6312
+ this.applyPagination();
6313
+ if (searchForItems) {
6314
+ this.loadMoreRows();
6315
+ }
5943
6316
  }
5944
6317
  // ── Template helpers ──
5945
6318
  getCellValue(column, row) {
@@ -5966,50 +6339,36 @@ class MnTable extends MnSelectableCollectionBase {
5966
6339
  }
5967
6340
  // ── Skeleton ──
5968
6341
  /**
5969
- * Resolves table-specific translation keys (column headers/filters) plus the
5970
- * shared keys handled by the base.
6342
+ * Hands the active filters to the consumer. Locks the body height first so the
6343
+ * skeleton swap during the refetch can't collapse the layout, matching
6344
+ * {@link goToPage} and {@link onSearch}.
5971
6345
  */
5972
- resolveTranslationKeys() {
5973
- super.resolveTranslationKeys();
6346
+ emitServerFilters() {
6347
+ this.lockBodyHeight();
6348
+ this.dataSource.onColumnFilterChange?.(this.activeColumnFilters);
6349
+ }
6350
+ /** Resets every filterable column to its type's empty value. */
6351
+ seedFilterValues() {
5974
6352
  for (const col of this.dataSource.columns) {
5975
- if (col.headerKey) {
5976
- col.header = this.lang.t(col.headerKey);
5977
- }
5978
- if (col.filterPlaceholderKey) {
5979
- col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
6353
+ if (col.filterable) {
6354
+ this.columnFilters[col.key] = emptyFilterValue(this.filterTypeOf(col));
5980
6355
  }
5981
6356
  }
5982
- if (this.dataSource.filtersLabelKey) {
5983
- this.dataSource.filtersLabel = this.lang.t(this.dataSource.filtersLabelKey);
5984
- }
5985
- if (this.dataSource.clearFiltersLabelKey) {
5986
- this.dataSource.clearFiltersLabel = this.lang.t(this.dataSource.clearFiltersLabelKey);
5987
- }
5988
6357
  }
5989
6358
  // ── Filtering & sorting ──
5990
- applyFilter(searchForItems) {
5991
- let items = this.applySearchFilter(this.dataSource.dataRows.value ?? []);
5992
- // Per-column filters
5993
- for (const col of this.dataSource.columns) {
5994
- const filterValue = this.columnFilters[col.key];
5995
- if (!col.filterable || !filterValue)
5996
- continue;
5997
- if (col.filterFn) {
5998
- items = items.filter(row => col.filterFn(row, filterValue));
5999
- }
6000
- else {
6001
- const term = filterValue.toLowerCase();
6002
- items = items.filter(row => {
6003
- const cellValue = typeof col.cell === 'function' ? col.cell(row) : '';
6004
- return (cellValue ?? '').toLowerCase().includes(term);
6005
- });
6006
- }
6007
- }
6008
- items = this.applySorting(items);
6009
- this.filteredItems = items;
6010
- this.applyPagination();
6011
- if (searchForItems) {
6012
- this.loadMoreRows();
6359
+ /** Resolves the range / boolean filter control labels from their translation keys. */
6360
+ resolveFilterLabelKeys() {
6361
+ const labels = this.dataSource.filterLabels;
6362
+ if (!labels)
6363
+ return;
6364
+ const pairs = [
6365
+ ['minKey', 'min'], ['maxKey', 'max'], ['fromKey', 'from'], ['toKey', 'to'],
6366
+ ['anyKey', 'any'], ['yesKey', 'yes'], ['noKey', 'no'],
6367
+ ];
6368
+ for (const [keyProp, labelProp] of pairs) {
6369
+ const key = labels[keyProp];
6370
+ if (key)
6371
+ labels[labelProp] = this.lang.t(key);
6013
6372
  }
6014
6373
  }
6015
6374
  applySorting(items) {
@@ -6045,19 +6404,28 @@ class MnTable extends MnSelectableCollectionBase {
6045
6404
  }
6046
6405
  });
6047
6406
  }
6048
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: null, target: i0.ɵɵFactoryTarget.Component });
6049
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6407
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
6408
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }, { propertyName: "filterPopover", first: true, predicate: ["filterPopover"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody (scroll)=\"closeFilterPopover()\" [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if (isInlineFilter(column)) {\n <!-- Compact types render directly under the header -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n } @else {\n <!-- Ranges and multi-select don't fit a column width: trigger a popover -->\n <button\n (click)=\"$event.stopPropagation(); toggleFilterPopover(column, $event)\"\n [attr.aria-expanded]=\"openFilterKey === column.key\"\n [attr.aria-label]=\"column.filterPlaceholder ?? filtersButtonLabel\"\n [data]=\"{ variant: isColumnFilterActive(column) ? 'fill' : 'outline', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"w-full gap-1.5\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideFilter></svg>\n @if (isColumnFilterActive(column)) {\n <span class=\"h-1.5 w-1.5 rounded-full bg-current\"></span>\n }\n </button>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n\n<!-- Filter popover for the rich filter types. Rendered outside the table's\n overflow-x-auto wrapper and positioned fixed, so it can't be clipped or\n scrolled away by it. -->\n@if (openFilterColumn(); as popoverColumn) {\n <div\n #filterPopover\n [style.left.px]=\"popoverPosition.left\"\n [style.top.px]=\"popoverPosition.top\"\n class=\"fixed z-30 w-64 rounded-md border border-base-300 bg-base-100 p-3 shadow-lg\"\n role=\"dialog\"\n >\n <p class=\"mb-2 text-xs font-medium text-base-content/70\">\n @if (isTemplateRef(popoverColumn.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(popoverColumn.header)\"></ng-container>\n } @else {\n {{ popoverColumn.header }}\n }\n </p>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: popoverColumn, idScope: 'popover' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n @if (isColumnFilterActive(popoverColumn)) {\n <button\n (click)=\"clearColumnFilter(popoverColumn)\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"mt-2 gap-1\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n}\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row, the popover and the small-screen panel. `idScope` keeps element ids\n unique across the three placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapseThreshold: 2,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('number-range') {\n <div class=\"flex items-end gap-2\">\n @for (bound of numberBounds; track bound) {\n <mn-lib-input-field\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n type: 'number',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"flex-1\"\n ></mn-lib-input-field>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"flex flex-col gap-2\">\n @for (bound of dateBounds; track bound) {\n <mn-lib-datetime\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n mode: 'date',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n ></mn-lib-datetime>\n }\n </div>\n }\n @default {\n <mn-lib-input-field\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6050
6409
  }
6051
6410
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
6052
6411
  type: Component,
6053
- args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n" }]
6054
- }], propDecorators: { sortChange: [{
6412
+ args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnDatetime, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody (scroll)=\"closeFilterPopover()\" [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if (isInlineFilter(column)) {\n <!-- Compact types render directly under the header -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n } @else {\n <!-- Ranges and multi-select don't fit a column width: trigger a popover -->\n <button\n (click)=\"$event.stopPropagation(); toggleFilterPopover(column, $event)\"\n [attr.aria-expanded]=\"openFilterKey === column.key\"\n [attr.aria-label]=\"column.filterPlaceholder ?? filtersButtonLabel\"\n [data]=\"{ variant: isColumnFilterActive(column) ? 'fill' : 'outline', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"w-full gap-1.5\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideFilter></svg>\n @if (isColumnFilterActive(column)) {\n <span class=\"h-1.5 w-1.5 rounded-full bg-current\"></span>\n }\n </button>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n\n<!-- Filter popover for the rich filter types. Rendered outside the table's\n overflow-x-auto wrapper and positioned fixed, so it can't be clipped or\n scrolled away by it. -->\n@if (openFilterColumn(); as popoverColumn) {\n <div\n #filterPopover\n [style.left.px]=\"popoverPosition.left\"\n [style.top.px]=\"popoverPosition.top\"\n class=\"fixed z-30 w-64 rounded-md border border-base-300 bg-base-100 p-3 shadow-lg\"\n role=\"dialog\"\n >\n <p class=\"mb-2 text-xs font-medium text-base-content/70\">\n @if (isTemplateRef(popoverColumn.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(popoverColumn.header)\"></ng-container>\n } @else {\n {{ popoverColumn.header }}\n }\n </p>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: popoverColumn, idScope: 'popover' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n @if (isColumnFilterActive(popoverColumn)) {\n <button\n (click)=\"clearColumnFilter(popoverColumn)\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"mt-2 gap-1\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n}\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row, the popover and the small-screen panel. `idScope` keeps element ids\n unique across the three placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapseThreshold: 2,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('number-range') {\n <div class=\"flex items-end gap-2\">\n @for (bound of numberBounds; track bound) {\n <mn-lib-input-field\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n type: 'number',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"flex-1\"\n ></mn-lib-input-field>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"flex flex-col gap-2\">\n @for (bound of dateBounds; track bound) {\n <mn-lib-datetime\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n mode: 'date',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n ></mn-lib-datetime>\n }\n </div>\n }\n @default {\n <mn-lib-input-field\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n" }]
6413
+ }], ctorParameters: () => [], propDecorators: { sortChange: [{
6055
6414
  type: Output
6056
6415
  }], rowClick: [{
6057
6416
  type: Output
6058
6417
  }], collectionBody: [{
6059
6418
  type: ViewChild,
6060
6419
  args: ['collectionBody']
6420
+ }], filterPopover: [{
6421
+ type: ViewChild,
6422
+ args: ['filterPopover']
6423
+ }], onDocumentClick: [{
6424
+ type: HostListener,
6425
+ args: ['document:click', ['$event']]
6426
+ }], onEscape: [{
6427
+ type: HostListener,
6428
+ args: ['document:keydown.escape']
6061
6429
  }], onWindowResize: [{
6062
6430
  type: HostListener,
6063
6431
  args: ['window:resize']
@@ -9639,11 +10007,11 @@ class MnTabComponent {
9639
10007
  this.currentActive = items[index];
9640
10008
  }
9641
10009
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9642
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
10010
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
9643
10011
  }
9644
10012
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, decorators: [{
9645
10013
  type: Component,
9646
- args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
10014
+ args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
9647
10015
  }], propDecorators: { scrollContainer: [{
9648
10016
  type: ViewChild,
9649
10017
  args: ['scrollContainer']
@@ -10279,5 +10647,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
10279
10647
  * Generated bundle index. Do not edit.
10280
10648
  */
10281
10649
 
10282
- export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultIconForStyle, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
10650
+ export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
10283
10651
  //# sourceMappingURL=mn-angular-lib.mjs.map