mn-angular-lib 1.0.131 → 1.0.132

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.
@@ -5223,6 +5223,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5223
5223
  * window, so viewport breakpoints would reveal columns the table has no room for.
5224
5224
  * mn-table marks its chrome `@container` for exactly this.
5225
5225
  *
5226
+ * Because of that, `sm`/`md`/`lg` mean "the table is at least this wide", and the
5227
+ * thresholds are **not** the viewport values of the same names — see {@link classMap}.
5228
+ *
5226
5229
  * Uses a static class map so Tailwind CSS can detect the full class names at build time.
5227
5230
  *
5228
5231
  * Usage: `<td [mnHiddenBelow]="column.hiddenBelow">`
@@ -5233,11 +5236,22 @@ class MnHiddenBelowDirective {
5233
5236
  el = inject(ElementRef);
5234
5237
  renderer = inject(Renderer2);
5235
5238
  appliedClasses = [];
5236
- /** Static mapping of breakpoints to their full Tailwind class names. */
5239
+ /**
5240
+ * Static mapping of breakpoints to their full Tailwind class names, so Tailwind
5241
+ * can detect them at build time.
5242
+ *
5243
+ * These are **container** widths, deliberately lower than the viewport
5244
+ * breakpoints they are named after. A table almost never gets the whole window:
5245
+ * a page table sits inside a docked sidebar plus page padding, which on a
5246
+ * 1280px screen leaves it under 900px. Reusing 1024px for `lg` would demand a
5247
+ * ~1400px window before an `lg` column ever appeared — hiding columns on the
5248
+ * most ordinary laptop. These values instead express how much room the column
5249
+ * itself needs, which is what a container query should measure.
5250
+ */
5237
5251
  classMap = {
5238
- sm: ['hidden', '@min-[640px]:table-cell'],
5239
- md: ['hidden', '@min-[768px]:table-cell'],
5240
- lg: ['hidden', '@min-[1024px]:table-cell'],
5252
+ sm: ['hidden', '@min-[480px]:table-cell'],
5253
+ md: ['hidden', '@min-[640px]:table-cell'],
5254
+ lg: ['hidden', '@min-[800px]:table-cell'],
5241
5255
  };
5242
5256
  ngOnChanges() {
5243
5257
  // Remove previously applied classes
@@ -5280,9 +5294,9 @@ class MnShowAboveDirective {
5280
5294
  appliedClasses = [];
5281
5295
  /** Static mapping of breakpoints to their full Tailwind class names. */
5282
5296
  classMap = {
5283
- sm: ['hidden', '@min-[640px]:inline'],
5284
- md: ['hidden', '@min-[768px]:inline'],
5285
- lg: ['hidden', '@min-[1024px]:inline'],
5297
+ sm: ['hidden', '@min-[480px]:inline'],
5298
+ md: ['hidden', '@min-[640px]:inline'],
5299
+ lg: ['hidden', '@min-[800px]:inline'],
5286
5300
  };
5287
5301
  ngOnChanges() {
5288
5302
  for (const cls of this.appliedClasses) {
@@ -5324,9 +5338,9 @@ class MnShowBelowDirective {
5324
5338
  appliedClasses = [];
5325
5339
  /** Static mapping of breakpoints to their full Tailwind class names. */
5326
5340
  classMap = {
5327
- sm: ['inline', '@min-[640px]:hidden'],
5328
- md: ['inline', '@min-[768px]:hidden'],
5329
- lg: ['inline', '@min-[1024px]:hidden'],
5341
+ sm: ['inline', '@min-[480px]:hidden'],
5342
+ md: ['inline', '@min-[640px]:hidden'],
5343
+ lg: ['inline', '@min-[800px]:hidden'],
5330
5344
  };
5331
5345
  ngOnChanges() {
5332
5346
  for (const cls of this.appliedClasses) {
@@ -6032,8 +6046,12 @@ class MnTable extends MnSelectableCollectionBase {
6032
6046
  * and its footer is pinned over the bottom of the table.
6033
6047
  */
6034
6048
  static MOBILE_PAGE_SIZE = 10;
6035
- /** The component's own element, measured for every responsive decision. */
6036
- host = inject((ElementRef));
6049
+ /**
6050
+ * The component's own element, measured for every responsive decision. Typed via
6051
+ * the annotation, not `inject(ElementRef<HTMLElement>)` — that form is a generic
6052
+ * call on the token and leaves `nativeElement` untyped.
6053
+ */
6054
+ host = inject(ElementRef);
6037
6055
  /** Whether the consumer owns filtering (server-side), mirroring {@link isServerSearched}. */
6038
6056
  get isServerFiltered() {
6039
6057
  return !!this.dataSource.onColumnFilterChange;
@@ -6250,29 +6268,12 @@ class MnTable extends MnSelectableCollectionBase {
6250
6268
  this.filtersPanelOpen = !this.filtersPanelOpen;
6251
6269
  }
6252
6270
  baseTableClasses = 'w-full border-collapse overflow-y-hidden';
6253
- constructor() {
6254
- super();
6255
- // Server-side text filtering only: client-side filtering stays instant per keystroke.
6256
- this.filterDebounce
6257
- .pipe(debounceTime(300), takeUntilDestroyed())
6258
- .subscribe(() => {
6259
- this.emitServerFilters();
6260
- this.cdr.markForCheck();
6261
- });
6262
- // Watch the table's own box rather than the window: inside a modal, a sidebar
6263
- // or a narrow grid cell the table resizes without the window ever changing,
6264
- // and the window resizes without the table's share of it changing.
6265
- if (typeof ResizeObserver !== 'undefined') {
6266
- const observer = new ResizeObserver(() => this.onHostResize());
6267
- observer.observe(this.host.nativeElement);
6268
- inject(DestroyRef).onDestroy(() => observer.disconnect());
6269
- }
6270
- // `beforeInitialFilter` runs while the host may not be attached or laid out
6271
- // yet, so its width reads 0 and {@link measuredWidth} has to guess from the
6272
- // window — the one guess that is wrong for a table in a modal. Re-evaluate
6273
- // once after the first render, when the real width is available.
6274
- afterNextRender(() => this.onHostResize());
6275
- }
6271
+ /**
6272
+ * Column widths measured from the automatic layout and pinned, keyed by column
6273
+ * key, for `stable`. Empty until the first render that has real rows on screen,
6274
+ * and cleared whenever the table is resized so the next render re-measures.
6275
+ */
6276
+ pinnedWidths = new Map();
6276
6277
  /** Sets sort/filter state seeded from the data source before the first filter pass. */
6277
6278
  beforeInitialFilter() {
6278
6279
  super.beforeInitialFilter();
@@ -6285,13 +6286,11 @@ class MnTable extends MnSelectableCollectionBase {
6285
6286
  this.seedFilterValues();
6286
6287
  }
6287
6288
  /**
6288
- * Classes for the `<table>` element. `table-fixed` is added for the `fixed`
6289
- * layout so column widths come from the header row and the declared widths
6290
- * only, keeping them stable as the rows change.
6289
+ * Whether {@link pinColumnWidths} has run. Tracked separately from
6290
+ * {@link pinnedWidths} being non-empty, because the widest column is deliberately
6291
+ * left unpinned and a table with a single flexible column therefore pins nothing.
6291
6292
  */
6292
- get tableClasses() {
6293
- return this.isFixedLayout ? `${this.baseTableClasses} table-fixed` : this.baseTableClasses;
6294
- }
6293
+ widthsPinned = false;
6295
6294
  /**
6296
6295
  * Recomputes whether the inline filter row should collapse into the panel.
6297
6296
  * Closes the panel when returning to the wide layout so reopened state never
@@ -6348,14 +6347,77 @@ class MnTable extends MnSelectableCollectionBase {
6348
6347
  return !!column.sortType && column.sortType !== ColumnSortType.NONE;
6349
6348
  }
6350
6349
  // ── Row interaction ──
6351
- /** Whether column widths are content-independent (see {@link TableAppearance.layout}). */
6352
- get isFixedLayout() {
6353
- return this.dataSource.appearance?.layout === 'fixed';
6350
+ constructor() {
6351
+ super();
6352
+ // Server-side text filtering only: client-side filtering stays instant per keystroke.
6353
+ this.filterDebounce
6354
+ .pipe(debounceTime(300), takeUntilDestroyed())
6355
+ .subscribe(() => {
6356
+ this.emitServerFilters();
6357
+ this.cdr.markForCheck();
6358
+ });
6359
+ // Watch the table's own box rather than the window: inside a modal, a sidebar
6360
+ // or a narrow grid cell the table resizes without the window ever changing,
6361
+ // and the window resizes without the table's share of it changing.
6362
+ if (typeof ResizeObserver !== 'undefined') {
6363
+ const observer = new ResizeObserver(() => this.onHostResize());
6364
+ observer.observe(this.host.nativeElement);
6365
+ inject(DestroyRef).onDestroy(() => observer.disconnect());
6366
+ }
6367
+ // `beforeInitialFilter` runs while the host may not be attached or laid out
6368
+ // yet, so its width reads 0 and {@link measuredWidth} has to guess from the
6369
+ // window — the one guess that is wrong for a table in a modal. Re-evaluate
6370
+ // once after the first render, when the real width is available.
6371
+ afterNextRender(() => this.onHostResize());
6372
+ // The `stable` layout has to let the browser lay the table out automatically
6373
+ // once before it can capture the result. This runs after every render because
6374
+ // the first render usually has no rows yet (a server fetch is still in flight);
6375
+ // the guards below make it a no-op until real rows are on screen, and the
6376
+ // pinned widths then stop it from measuring again.
6377
+ afterEveryRender(() => {
6378
+ if (this.layoutMode !== 'stable')
6379
+ return;
6380
+ if (this.widthsPinned || this.isLoadingState)
6381
+ return;
6382
+ if (this.paginatedItems.length === 0)
6383
+ return;
6384
+ this.pinColumnWidths();
6385
+ });
6386
+ }
6387
+ /**
6388
+ * Classes for the `<table>` element. `table-fixed` is added once column widths
6389
+ * are no longer allowed to follow the content: always for the `fixed` layout, and
6390
+ * for `stable` from the moment its widths have been measured and pinned.
6391
+ */
6392
+ get tableClasses() {
6393
+ return this.widthsArePinned ? `${this.baseTableClasses} table-fixed` : this.baseTableClasses;
6354
6394
  }
6355
6395
  /** Page size to use at/above the `md` breakpoint (consumer's pageSize, or the user's selection). */
6356
6396
  desktopPageSize = 10;
6397
+ /** The effective column-width strategy, defaulting to `stable`. */
6398
+ get layoutMode() {
6399
+ return this.dataSource.appearance?.layout ?? 'stable';
6400
+ }
6401
+ /**
6402
+ * Whether column widths have stopped following the cell content — `fixed` always,
6403
+ * `stable` once {@link pinColumnWidths} has captured them. Drives `table-fixed`
6404
+ * and the cell truncation together, so a cell is never clipped while the column
6405
+ * it sits in could still have grown to fit it.
6406
+ */
6407
+ get widthsArePinned() {
6408
+ return this.layoutMode === 'fixed' || (this.layoutMode === 'stable' && this.widthsPinned);
6409
+ }
6357
6410
  /**
6358
- * The `title` tooltip for a cell, so text truncated by the fixed layout stays
6411
+ * The width to render for a column: the consumer's own declared width always
6412
+ * wins, then a width pinned by the `stable` layout, otherwise none.
6413
+ * @param column The column being rendered.
6414
+ * @returns A CSS width, or `null` to leave it to the layout algorithm.
6415
+ */
6416
+ columnWidth(column) {
6417
+ return column.width ?? this.pinnedWidths.get(column.key) ?? null;
6418
+ }
6419
+ /**
6420
+ * The `title` tooltip for a cell, so text truncated by a pinned column stays
6359
6421
  * readable. Only string cells have text to expose; template cells render their
6360
6422
  * own markup and are left alone.
6361
6423
  * @param column The column being rendered.
@@ -6363,10 +6425,63 @@ class MnTable extends MnSelectableCollectionBase {
6363
6425
  * @returns The full cell text, or `null` when there is nothing to expose.
6364
6426
  */
6365
6427
  cellTitle(column, row) {
6366
- if (!this.isFixedLayout || typeof column.cell !== 'function')
6428
+ if (!this.widthsArePinned || typeof column.cell !== 'function')
6367
6429
  return null;
6368
6430
  return column.cell(row) || null;
6369
6431
  }
6432
+ /**
6433
+ * Captures the current, automatically-derived width of every visible column and
6434
+ * pins it, which flips the table to `table-fixed` on the next render.
6435
+ *
6436
+ * Runs only with real rows on screen: measuring the loading skeletons would pin
6437
+ * the placeholder bars' widths rather than the data's. Hidden columns
6438
+ * ({@link ColumnBase.hiddenBelow}) measure 0 and are skipped, so they are free to
6439
+ * size themselves if a resize later reveals them.
6440
+ *
6441
+ * The **widest** column is measured but deliberately left unpinned, so it absorbs
6442
+ * whatever space the pinned ones leave over. Pinning every column instead makes the
6443
+ * widths sum to slightly more than the container — `border-collapse` shares borders
6444
+ * between neighbours, so rounding each cell's measured width over-counts them — and
6445
+ * the table then overflows into a spurious horizontal scrollbar. Leaving one column
6446
+ * elastic also means a later resize squeezes the widest column first instead of
6447
+ * clipping every column equally.
6448
+ */
6449
+ pinColumnWidths() {
6450
+ const headerCells = this.host.nativeElement.querySelectorAll('thead tr:first-child th[data-column-key]');
6451
+ const measured = [];
6452
+ for (const cell of Array.from(headerCells)) {
6453
+ const key = cell.dataset['columnKey'];
6454
+ const width = cell.getBoundingClientRect().width;
6455
+ // A width of 0 means the column is hidden at this container width.
6456
+ if (!key || width <= 0)
6457
+ continue;
6458
+ measured.push({ key, width });
6459
+ }
6460
+ if (measured.length === 0)
6461
+ return;
6462
+ const widest = measured.reduce((a, b) => (b.width > a.width ? b : a));
6463
+ const pinned = new Map();
6464
+ for (const { key, width } of measured) {
6465
+ if (key === widest.key)
6466
+ continue;
6467
+ pinned.set(key, `${Math.round(width)}px`);
6468
+ }
6469
+ this.pinnedWidths = pinned;
6470
+ this.widthsPinned = true;
6471
+ this.cdr.markForCheck();
6472
+ }
6473
+ /**
6474
+ * Drops the pinned widths so the next render with rows re-measures them. Called
6475
+ * when the table is resized: the old pixel widths were shares of a box that no
6476
+ * longer exists, and a resize is also what makes `hiddenBelow` columns come and
6477
+ * go, changing which columns need a share at all.
6478
+ */
6479
+ unpinColumnWidths() {
6480
+ if (!this.widthsPinned)
6481
+ return;
6482
+ this.pinnedWidths = new Map();
6483
+ this.widthsPinned = false;
6484
+ }
6370
6485
  /**
6371
6486
  * Re-evaluate on a window resize too. The ResizeObserver covers every change to
6372
6487
  * the table's own box, but {@link isMobileViewport} reads the window, which can
@@ -6378,6 +6493,8 @@ class MnTable extends MnSelectableCollectionBase {
6378
6493
  }
6379
6494
  /** Re-evaluate responsive page size and filter layout when the table is resized. */
6380
6495
  onHostResize() {
6496
+ // Pixel widths captured for the old box are meaningless in the new one.
6497
+ this.unpinColumnWidths();
6381
6498
  this.applyResponsivePageSize(true);
6382
6499
  this.updateFilterLayout(true);
6383
6500
  // The popover is anchored to a viewport rect that a resize invalidates.
@@ -6577,11 +6694,11 @@ class MnTable extends MnSelectableCollectionBase {
6577
6694
  });
6578
6695
  }
6579
6696
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
6580
- 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()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }, { propertyName: "filterPopover", first: true, predicate: ["filterPopover"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for.\n The filter popover is deliberately left outside: `container-type` makes an\n element the containing block for `position: fixed` descendants, which would\n re-anchor the popover away from its trigger. -->\n<div class=\"@container\">\n<!-- 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-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]: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-[420px]:w-auto @min-[420px]: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-[420px]: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.truncate]=\"isFixedLayout\"\n class=\"text-sm px-2 py-1 @min-[768px]:px-4 @min-[768px]: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 [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"isFixedLayout\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n class=\"text-xs px-2 py-1 @min-[768px]:px-4 @min-[768px]:py-2\"\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</div>\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 });
6697
+ 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()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }, { propertyName: "filterPopover", first: true, predicate: ["filterPopover"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for.\n The filter popover is deliberately left outside: `container-type` makes an\n element the containing block for `position: fixed` descendants, which would\n re-anchor the popover away from its trigger. -->\n<div class=\"@container\">\n<!-- 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-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]: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-[420px]:w-auto @min-[420px]: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-[420px]: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 [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\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]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\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]=\"columnWidth(column)\"\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 [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\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</div>\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 });
6581
6698
  }
6582
6699
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
6583
6700
  type: Component,
6584
- 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, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for.\n The filter popover is deliberately left outside: `container-type` makes an\n element the containing block for `position: fixed` descendants, which would\n re-anchor the popover away from its trigger. -->\n<div class=\"@container\">\n<!-- 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-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]: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-[420px]:w-auto @min-[420px]: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-[420px]: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.truncate]=\"isFixedLayout\"\n class=\"text-sm px-2 py-1 @min-[768px]:px-4 @min-[768px]: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 [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"isFixedLayout\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n class=\"text-xs px-2 py-1 @min-[768px]:px-4 @min-[768px]:py-2\"\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</div>\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" }]
6701
+ 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, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for.\n The filter popover is deliberately left outside: `container-type` makes an\n element the containing block for `position: fixed` descendants, which would\n re-anchor the popover away from its trigger. -->\n<div class=\"@container\">\n<!-- 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-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]: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-[420px]:w-auto @min-[420px]: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-[420px]: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 [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\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]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\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]=\"columnWidth(column)\"\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 [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\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</div>\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" }]
6585
6702
  }], ctorParameters: () => [], propDecorators: { sortChange: [{
6586
6703
  type: Output
6587
6704
  }], rowClick: [{