mn-angular-lib 1.0.76 → 1.0.77

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.
@@ -3784,6 +3784,62 @@ var ColumnSortType;
3784
3784
  ColumnSortType["NONE"] = "NONE";
3785
3785
  })(ColumnSortType || (ColumnSortType = {}));
3786
3786
 
3787
+ const mnSkeletonVariants = tv({
3788
+ base: 'bg-base-300 relative overflow-hidden',
3789
+ variants: {
3790
+ shape: {
3791
+ // Each shape carries a sensible default size so a bare <mn-skeleton> stays
3792
+ // visible. The width/height inputs are applied as inline styles, which win
3793
+ // over these utility classes whenever the consumer supplies dimensions.
3794
+ rectangle: 'rounded-md h-4',
3795
+ circle: 'rounded-full h-10 w-10',
3796
+ text: 'rounded h-4',
3797
+ },
3798
+ },
3799
+ defaultVariants: {
3800
+ shape: 'rectangle',
3801
+ },
3802
+ });
3803
+
3804
+ class MnSkeleton {
3805
+ data = {};
3806
+ // Skeletons are decorative loading placeholders; hide them from assistive tech.
3807
+ // The container that swaps skeleton↔content should carry aria-busy / a live region.
3808
+ ariaHidden = 'true';
3809
+ get hostClasses() {
3810
+ return mnSkeletonVariants({ shape: this.data.shape });
3811
+ }
3812
+ get width() {
3813
+ return this.data.width ?? null;
3814
+ }
3815
+ get height() {
3816
+ return this.data.height ?? null;
3817
+ }
3818
+ get isAnimated() {
3819
+ return this.data.animated !== false;
3820
+ }
3821
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
3822
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSkeleton, isStandalone: true, selector: "mn-skeleton", inputs: { data: "data" }, host: { properties: { "attr.aria-hidden": "this.ariaHidden", "class": "this.hostClasses", "style.width": "this.width", "style.height": "this.height" } }, ngImport: i0, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,var(--color-base-100) 65%,transparent) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.mn-skeleton-shimmer-bar{animation:none}}\n"] });
3823
+ }
3824
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, decorators: [{
3825
+ type: Component,
3826
+ args: [{ selector: 'mn-skeleton', standalone: true, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,var(--color-base-100) 65%,transparent) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.mn-skeleton-shimmer-bar{animation:none}}\n"] }]
3827
+ }], propDecorators: { data: [{
3828
+ type: Input
3829
+ }], ariaHidden: [{
3830
+ type: HostBinding,
3831
+ args: ['attr.aria-hidden']
3832
+ }], hostClasses: [{
3833
+ type: HostBinding,
3834
+ args: ['class']
3835
+ }], width: [{
3836
+ type: HostBinding,
3837
+ args: ['style.width']
3838
+ }], height: [{
3839
+ type: HostBinding,
3840
+ args: ['style.height']
3841
+ }] } });
3842
+
3787
3843
  /**
3788
3844
  * Attribute directive that applies responsive-hiding classes to table cells/headers.
3789
3845
  * Hides the element by default and shows it as `table-cell` at the specified breakpoint.
@@ -3919,39 +3975,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3919
3975
  type: Input
3920
3976
  }] } });
3921
3977
 
3922
- class MnTable {
3978
+ /**
3979
+ * Shared chrome for MnLib collection components (table, list, grid):
3980
+ * data subscription, client/server search, every pagination mode, load-more,
3981
+ * skeleton-row count, empty-state plumbing, common i18n key resolution and
3982
+ * toolbar change-detection.
3983
+ *
3984
+ * Concrete components extend this (or {@link MnSelectableCollectionBase}) and
3985
+ * implement only their rendering. The class is decorated `@Directive()` so it can
3986
+ * declare `@Input`s and use `inject()` while remaining abstract.
3987
+ *
3988
+ * Init runs in a fixed order (see {@link ngOnInit}); subclasses hook in via the
3989
+ * `protected` template methods rather than overriding `ngOnInit`.
3990
+ */
3991
+ class MnCollectionBase {
3923
3992
  dataSource;
3924
- sortChange = new EventEmitter();
3925
- selectionChange = new EventEmitter();
3926
- rowClick = new EventEmitter();
3927
3993
  filteredItems = [];
3928
3994
  paginatedItems = [];
3929
3995
  searchValue = '';
3930
3996
  loadingMoreRows = false;
3931
- currentSort = null;
3932
- selectedIds = new Set();
3933
3997
  currentPage = 1;
3934
3998
  pageSize = 10;
3935
- /** Per-column filter values keyed by column key. */
3936
- columnFilters = {};
3937
3999
  cdr = inject(ChangeDetectorRef);
3938
4000
  lang = inject(MnLanguageService);
4001
+ /** Prefix used in validation error messages, e.g. `MnList`. Overridden by subclasses. */
4002
+ componentName = 'MnCollection';
3939
4003
  dataSubscription;
3940
4004
  searchSubject = new Subject();
3941
4005
  searchSubscription;
3942
4006
  langSubscription;
3943
- /** Tracks the previous toolbar left template reference for change detection. */
3944
- previousToolbarLeftTemplate;
3945
- /**
3946
- * Checks for changes to dataSource properties that are not covered
3947
- * by Angular's default change detection (e.g. toolbarLeftTemplate).
3948
- */
3949
- ngDoCheck() {
3950
- const currentTemplate = this.dataSource?.toolbarLeftTemplate;
3951
- if (currentTemplate !== this.previousToolbarLeftTemplate) {
3952
- this.previousToolbarLeftTemplate = currentTemplate;
3953
- this.cdr.markForCheck();
3954
- }
4007
+ /** Tracks the previous toolbar template reference for change detection. */
4008
+ previousToolbarTemplate;
4009
+ // ── Template-method hooks ──
4010
+ /** Whether the component delegates search to the consumer (server-side). */
4011
+ get isServerSearched() {
4012
+ return !!this.dataSource.onServerSearch;
4013
+ }
4014
+ get isPaginated() {
4015
+ const mode = this.dataSource.paginationMode;
4016
+ return mode === 'paginated' || mode === 'client-side-pagination';
4017
+ }
4018
+ /** Whether the component delegates pagination to the consumer (server-side). */
4019
+ get isServerPaginated() {
4020
+ const mode = this.dataSource.paginationMode ?? 'load-more';
4021
+ return mode === 'paginated' || mode === 'load-more';
3955
4022
  }
3956
4023
  get showLoadMore() {
3957
4024
  const mode = this.dataSource.paginationMode ?? 'load-more';
@@ -3964,109 +4031,7 @@ class MnTable {
3964
4031
  const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
3965
4032
  return mode === 'load-more' && hasMore;
3966
4033
  }
3967
- ngOnDestroy() {
3968
- this.dataSubscription?.unsubscribe();
3969
- this.searchSubscription?.unsubscribe();
3970
- this.langSubscription?.unsubscribe();
3971
- }
3972
- // ── Search ──
3973
- get isPaginated() {
3974
- const mode = this.dataSource.paginationMode;
3975
- return mode === 'paginated' || mode === 'client-side-pagination';
3976
- }
3977
- // ── Column Filters ──
3978
- /** Whether any column has filtering enabled. */
3979
- get hasColumnFilters() {
3980
- return this.dataSource.columns.some(c => c.filterable);
3981
- }
3982
- /** Updates a column filter value and re-applies filtering. */
3983
- onColumnFilter(columnKey, value) {
3984
- this.columnFilters[columnKey] = value;
3985
- this.currentPage = 1;
3986
- this.applyFilterAndSort(false);
3987
- this.cdr.markForCheck();
3988
- }
3989
- // ── Sorting ──
3990
- sort(column) {
3991
- if (!column.sortType || column.sortType === ColumnSortType.NONE)
3992
- return;
3993
- if (this.currentSort?.columnKey === column.key) {
3994
- this.currentSort = this.currentSort.direction === 'asc'
3995
- ? { columnKey: column.key, direction: 'desc' }
3996
- : null;
3997
- }
3998
- else {
3999
- this.currentSort = { columnKey: column.key, direction: 'asc' };
4000
- }
4001
- this.sortChange.emit(this.currentSort);
4002
- this.applyFilterAndSort(false);
4003
- }
4004
- getSortIcon(column) {
4005
- if (!this.currentSort || this.currentSort.columnKey !== column.key)
4006
- return '';
4007
- return this.currentSort.direction === 'asc' ? '▲' : '▼';
4008
- }
4009
- isSortable(column) {
4010
- return !!column.sortType && column.sortType !== ColumnSortType.NONE;
4011
- }
4012
- // ── Selection ──
4013
- isSelected(row) {
4014
- return this.selectedIds.has(this.dataSource.getID(row));
4015
- }
4016
- toggleRow(row) {
4017
- const id = this.dataSource.getID(row);
4018
- const mode = this.dataSource.selectionMode ?? 'none';
4019
- if (mode === 'single') {
4020
- this.selectedIds.clear();
4021
- this.selectedIds.add(id);
4022
- }
4023
- else if (mode === 'multi') {
4024
- if (this.selectedIds.has(id)) {
4025
- this.selectedIds.delete(id);
4026
- }
4027
- else {
4028
- this.selectedIds.add(id);
4029
- }
4030
- }
4031
- this.emitSelection();
4032
- }
4033
- toggleAll() {
4034
- if (this.selectedIds.size === this.filteredItems.length) {
4035
- this.selectedIds.clear();
4036
- }
4037
- else {
4038
- this.filteredItems.forEach(row => this.selectedIds.add(this.dataSource.getID(row)));
4039
- }
4040
- this.emitSelection();
4041
- }
4042
- get allSelected() {
4043
- return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
4044
- }
4045
- get hasSelection() {
4046
- return (this.dataSource.selectionMode ?? 'none') !== 'none';
4047
- }
4048
- get isMultiSelect() {
4049
- return this.dataSource.selectionMode === 'multi';
4050
- }
4051
- // ── Row interaction ──
4052
- onRowClick(row) {
4053
- if (this.hasSelection) {
4054
- this.toggleRow(row);
4055
- }
4056
- this.dataSource.onRowClick?.(row);
4057
- this.rowClick.emit(row);
4058
- }
4059
- // ── Pagination ──
4060
- /** Whether the table delegates pagination to the consumer (server-side). */
4061
- get isServerPaginated() {
4062
- const mode = this.dataSource.paginationMode ?? 'load-more';
4063
- return mode === 'paginated' || mode === 'load-more';
4064
- }
4065
- /** Whether the table delegates search to the consumer (server-side). */
4066
- get isServerSearched() {
4067
- return !!this.dataSource.onServerSearch;
4068
- }
4069
- // ── Paginated Mode ──
4034
+ // ── Lifecycle ──
4070
4035
  /** Total number of items, accounting for server-side pagination. */
4071
4036
  get totalItemCount() {
4072
4037
  if (this.isServerPaginated && this.dataSource.totalItems != null) {
@@ -4077,83 +4042,104 @@ class MnTable {
4077
4042
  get totalPages() {
4078
4043
  return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
4079
4044
  }
4045
+ get resolvedPageSizeOptions() {
4046
+ return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
4047
+ }
4048
+ // ── Search ──
4049
+ /** Page-size options formatted for mn-select. */
4050
+ get pageSizeSelectOptions() {
4051
+ return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
4052
+ }
4053
+ get visiblePages() {
4054
+ const total = this.totalPages;
4055
+ const current = this.currentPage;
4056
+ const maxVisible = 3;
4057
+ let start = Math.max(1, current - Math.floor(maxVisible / 2));
4058
+ let end = start + maxVisible - 1;
4059
+ if (end > total) {
4060
+ end = total;
4061
+ start = Math.max(1, end - maxVisible + 1);
4062
+ }
4063
+ const pages = [];
4064
+ for (let i = start; i <= end; i++) {
4065
+ pages.push(i);
4066
+ }
4067
+ return pages;
4068
+ }
4069
+ // ── Pagination ──
4070
+ get skeletonRows() {
4071
+ return Array.from({ length: this.dataSource.skeletonRowCount ?? 5 });
4072
+ }
4080
4073
  ngOnInit() {
4081
4074
  this.validateDataSource();
4082
4075
  this.resolveTranslationKeys();
4083
- this.currentSort = this.dataSource.defaultSort ?? null;
4084
4076
  this.pageSize = this.dataSource.pageSize ?? 10;
4085
- // Initialize all filterable columns with empty string to avoid undefined values.
4086
- for (const col of this.dataSource.columns) {
4087
- if (col.filterable) {
4088
- this.columnFilters[col.key] = '';
4089
- }
4090
- }
4091
- // Pre-select rows from initialSelectedIds if provided
4092
- if (this.dataSource.initialSelectedIds?.length) {
4093
- for (const id of this.dataSource.initialSelectedIds) {
4094
- this.selectedIds.add(id);
4095
- }
4096
- this.emitSelection();
4097
- }
4098
- this.applyFilterAndSort(false);
4099
- // Skip the initial BehaviorSubject emission (already handled above)
4100
- // to avoid triggering markForCheck during the first change detection cycle.
4077
+ this.beforeInitialFilter();
4078
+ this.applyFilter(false);
4079
+ // Skip the initial BehaviorSubject emission (already handled above).
4101
4080
  this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
4102
- this.applyFilterAndSort(false);
4081
+ this.applyFilter(false);
4103
4082
  this.cdr.markForCheck();
4104
4083
  });
4105
4084
  this.searchSubscription = this.searchSubject
4106
4085
  .pipe(debounceTime(300))
4107
4086
  .subscribe(value => {
4108
4087
  this.searchValue = value;
4109
- this.applyFilterAndSort(true);
4088
+ this.applyFilter(true);
4110
4089
  this.cdr.markForCheck();
4111
4090
  });
4112
- // Re-resolve translation keys when the locale changes.
4091
+ // Re-resolve translation keys whenever the locale changes.
4113
4092
  this.langSubscription = this.lang.locale$.pipe(skip(1)).subscribe(() => {
4114
4093
  this.resolveTranslationKeys();
4115
4094
  this.cdr.markForCheck();
4116
4095
  });
4117
4096
  }
4118
- /**
4119
- * Resolves all translation keys (headerKey, filterPlaceholderKey, emptyMessageKey, etc.)
4120
- * into their corresponding display strings using MnLanguageService.
4121
- */
4122
- resolveTranslationKeys() {
4123
- for (const col of this.dataSource.columns) {
4124
- if (col.headerKey) {
4125
- col.header = this.lang.t(col.headerKey);
4126
- }
4127
- if (col.filterPlaceholderKey) {
4128
- col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
4129
- }
4130
- }
4131
- if (this.dataSource.emptyMessageKey) {
4132
- this.dataSource.emptyMessage = this.lang.t(this.dataSource.emptyMessageKey);
4133
- }
4134
- if (this.dataSource.searchPlaceholderKey) {
4135
- this.dataSource.searchPlaceholder = this.lang.t(this.dataSource.searchPlaceholderKey);
4136
- }
4137
- if (this.dataSource.labels) {
4138
- if (this.dataSource.labels.loadMoreKey) {
4139
- this.dataSource.labels.loadMore = this.lang.t(this.dataSource.labels.loadMoreKey);
4140
- }
4141
- if (this.dataSource.labels.rowsPerPageKey) {
4142
- this.dataSource.labels.rowsPerPage = this.lang.t(this.dataSource.labels.rowsPerPageKey);
4143
- }
4097
+ ngDoCheck() {
4098
+ const currentTemplate = this.trackedToolbarTemplate;
4099
+ if (currentTemplate !== this.previousToolbarTemplate) {
4100
+ this.previousToolbarTemplate = currentTemplate;
4101
+ this.cdr.markForCheck();
4144
4102
  }
4145
4103
  }
4104
+ ngOnDestroy() {
4105
+ this.dataSubscription?.unsubscribe();
4106
+ this.searchSubscription?.unsubscribe();
4107
+ this.langSubscription?.unsubscribe();
4108
+ }
4146
4109
  onSearch(searchString) {
4147
4110
  this.currentPage = 1;
4148
4111
  if (this.isServerSearched) {
4149
4112
  this.searchValue = searchString;
4150
- this.dataSource.onServerSearch(searchString);
4113
+ this.dataSource.onServerSearch?.(searchString);
4151
4114
  this.cdr.markForCheck();
4152
4115
  }
4153
4116
  else {
4154
4117
  this.searchSubject.next(searchString);
4155
4118
  }
4156
4119
  }
4120
+ goToPage(page) {
4121
+ if (page < 1 || page > this.totalPages)
4122
+ return;
4123
+ this.currentPage = page;
4124
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
4125
+ this.applyPagination();
4126
+ }
4127
+ else {
4128
+ this.dataSource.onPageChange?.(page);
4129
+ }
4130
+ this.cdr.markForCheck();
4131
+ }
4132
+ onPageSizeChange(newSize) {
4133
+ this.pageSize = newSize;
4134
+ this.currentPage = 1;
4135
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
4136
+ this.applyPagination();
4137
+ }
4138
+ else {
4139
+ this.dataSource.onPageSizeChange?.(newSize);
4140
+ }
4141
+ this.cdr.markForCheck();
4142
+ }
4157
4143
  loadMoreRows() {
4158
4144
  // Server-side infinite scroll: delegate to consumer callback.
4159
4145
  if (this.dataSource.onLoadMore) {
@@ -4170,12 +4156,245 @@ class MnTable {
4170
4156
  .then(rows => this.processLoadedRows(rows))
4171
4157
  .catch(() => this.loadingMoreRows = false);
4172
4158
  }
4173
- get resolvedPageSizeOptions() {
4174
- return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
4159
+ isTemplateRef(value) {
4160
+ return value instanceof TemplateRef;
4161
+ }
4162
+ trackByID = (_index, item) => {
4163
+ return this.dataSource.getID(item);
4164
+ };
4165
+ // ── Template helpers ──
4166
+ /** Runs after pageSize is set but before the first {@link applyFilter}. */
4167
+ beforeInitialFilter() {
4168
+ // no-op by default
4169
+ }
4170
+ /**
4171
+ * Resolves translation keys to display strings via {@link MnLanguageService}.
4172
+ * Subclasses override to resolve their own keys; call `super` to keep these.
4173
+ */
4174
+ resolveTranslationKeys() {
4175
+ if (this.dataSource.emptyMessageKey) {
4176
+ this.dataSource.emptyMessage = this.lang.t(this.dataSource.emptyMessageKey);
4177
+ }
4178
+ if (this.dataSource.searchPlaceholderKey) {
4179
+ this.dataSource.searchPlaceholder = this.lang.t(this.dataSource.searchPlaceholderKey);
4180
+ }
4181
+ if (this.dataSource.labels) {
4182
+ if (this.dataSource.labels.loadMoreKey) {
4183
+ this.dataSource.labels.loadMore = this.lang.t(this.dataSource.labels.loadMoreKey);
4184
+ }
4185
+ if (this.dataSource.labels.rowsPerPageKey) {
4186
+ this.dataSource.labels.rowsPerPage = this.lang.t(this.dataSource.labels.rowsPerPageKey);
4187
+ }
4188
+ }
4189
+ }
4190
+ // ── Shared internals ──
4191
+ applyPagination() {
4192
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
4193
+ const start = (this.currentPage - 1) * this.pageSize;
4194
+ this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
4195
+ }
4196
+ else {
4197
+ // Server always provides the correct page/slice — no client-side slicing.
4198
+ this.paginatedItems = this.filteredItems;
4199
+ }
4200
+ }
4201
+ /** Client-side search filtering shared by list and grid. */
4202
+ applySearchFilter(items) {
4203
+ if (!this.isServerSearched && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue && this.searchValue.length > 0) {
4204
+ const term = this.searchValue.toLowerCase();
4205
+ return items.filter(row => this.dataSource.isInSearch(row, term));
4206
+ }
4207
+ return items;
4208
+ }
4209
+ processLoadedRows(rows) {
4210
+ const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
4211
+ this.dataSource.dataRows.next(merged);
4212
+ this.loadingMoreRows = false;
4213
+ this.applyFilter(false);
4214
+ }
4215
+ validateDataSource() {
4216
+ const mode = this.dataSource.paginationMode;
4217
+ if (mode === 'paginated') {
4218
+ if (!this.dataSource.onPageChange) {
4219
+ throw new Error(`[${this.componentName}] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
4220
+ }
4221
+ if (this.dataSource.totalItems == null) {
4222
+ throw new Error(`[${this.componentName}] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
4223
+ }
4224
+ }
4225
+ if (mode === 'load-more' || mode === 'infinite-scroll') {
4226
+ if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
4227
+ throw new Error(`[${this.componentName}] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
4228
+ }
4229
+ }
4230
+ // Validate pageSize is one of pageSizeOptions when pagination is active
4231
+ if (mode && mode !== 'none') {
4232
+ const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
4233
+ const size = this.dataSource.pageSize ?? 10;
4234
+ if (!options.includes(size)) {
4235
+ throw new Error(`[${this.componentName}] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
4236
+ }
4237
+ }
4238
+ }
4239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4240
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnCollectionBase, isStandalone: true, inputs: { dataSource: "dataSource" }, ngImport: i0 });
4241
+ }
4242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionBase, decorators: [{
4243
+ type: Directive
4244
+ }], propDecorators: { dataSource: [{
4245
+ type: Input
4246
+ }] } });
4247
+
4248
+ /**
4249
+ * Extends {@link MnCollectionBase} with single/multi row selection, shared by
4250
+ * components that support it (table, list). Grid extends the plain base instead.
4251
+ */
4252
+ class MnSelectableCollectionBase extends MnCollectionBase {
4253
+ selectionChange = new EventEmitter();
4254
+ selectedIds = new Set();
4255
+ get allSelected() {
4256
+ return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
4257
+ }
4258
+ get hasSelection() {
4259
+ return (this.dataSource.selectionMode ?? 'none') !== 'none';
4260
+ }
4261
+ get isMultiSelect() {
4262
+ return this.dataSource.selectionMode === 'multi';
4263
+ }
4264
+ isSelected(item) {
4265
+ return this.selectedIds.has(this.dataSource.getID(item));
4266
+ }
4267
+ toggle(item) {
4268
+ const id = this.dataSource.getID(item);
4269
+ const mode = this.dataSource.selectionMode ?? 'none';
4270
+ if (mode === 'single') {
4271
+ this.selectedIds.clear();
4272
+ this.selectedIds.add(id);
4273
+ }
4274
+ else if (mode === 'multi') {
4275
+ if (this.selectedIds.has(id)) {
4276
+ this.selectedIds.delete(id);
4277
+ }
4278
+ else {
4279
+ this.selectedIds.add(id);
4280
+ }
4281
+ }
4282
+ this.emitSelection();
4283
+ }
4284
+ toggleAll() {
4285
+ if (this.selectedIds.size === this.filteredItems.length) {
4286
+ this.selectedIds.clear();
4287
+ }
4288
+ else {
4289
+ this.filteredItems.forEach(item => this.selectedIds.add(this.dataSource.getID(item)));
4290
+ }
4291
+ this.emitSelection();
4292
+ }
4293
+ /** Seeds selection from `initialSelectedIds` before the first filter pass. */
4294
+ beforeInitialFilter() {
4295
+ super.beforeInitialFilter();
4296
+ if (this.dataSource.initialSelectedIds?.length) {
4297
+ for (const id of this.dataSource.initialSelectedIds) {
4298
+ this.selectedIds.add(id);
4299
+ }
4300
+ this.emitSelection();
4301
+ }
4302
+ }
4303
+ emitSelection() {
4304
+ const rows = (this.dataSource.dataRows.value ?? []).filter(r => this.selectedIds.has(this.dataSource.getID(r)));
4305
+ this.dataSource.selectedRows?.next(rows);
4306
+ this.selectionChange.emit(rows);
4307
+ }
4308
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelectableCollectionBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
4309
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSelectableCollectionBase, isStandalone: true, outputs: { selectionChange: "selectionChange" }, usesInheritance: true, ngImport: i0 });
4310
+ }
4311
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelectableCollectionBase, decorators: [{
4312
+ type: Directive
4313
+ }], propDecorators: { selectionChange: [{
4314
+ type: Output
4315
+ }] } });
4316
+
4317
+ /**
4318
+ * Presentational pagination footer shared by every MnLib collection component
4319
+ * (table, list, grid): the load-more button, the page-size selector and the
4320
+ * page navigator. It holds no state — the host component owns pagination state
4321
+ * (via {@link import('./mn-collection-base.directive').MnCollectionBase}) and
4322
+ * reacts to the outputs.
4323
+ */
4324
+ class MnCollectionPagination {
4325
+ /** Prefix for the page-size select's id, keeping it unique per host. */
4326
+ idPrefix = 'mn-collection';
4327
+ isPaginated = false;
4328
+ isServerPaginated = false;
4329
+ showLoadMore = false;
4330
+ loadingMoreRows = false;
4331
+ currentPage = 1;
4332
+ pageSize = 10;
4333
+ totalPages = 1;
4334
+ totalItemCount = 0;
4335
+ visiblePages = [];
4336
+ pageSizeSelectOptions = [];
4337
+ labels;
4338
+ loadMore = new EventEmitter();
4339
+ pageChange = new EventEmitter();
4340
+ pageSizeChange = new EventEmitter();
4341
+ get showPagination() {
4342
+ return this.isPaginated && (this.totalPages > 1 || this.isServerPaginated);
4343
+ }
4344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, deps: [], target: i0.ɵɵFactoryTarget.Component });
4345
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCollectionPagination, isStandalone: true, selector: "mn-collection-pagination", inputs: { idPrefix: "idPrefix", isPaginated: "isPaginated", isServerPaginated: "isServerPaginated", showLoadMore: "showLoadMore", loadingMoreRows: "loadingMoreRows", currentPage: "currentPage", pageSize: "pageSize", totalPages: "totalPages", totalItemCount: "totalItemCount", visiblePages: "visiblePages", pageSizeSelectOptions: "pageSizeSelectOptions", labels: "labels" }, outputs: { loadMore: "loadMore", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n mnButton\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"First page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Previous page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u2039\n </button>\n\n @for (page of visiblePages; track page) {\n <button\n (click)=\"pageChange.emit(page)\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"'Page ' + page\"\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n >{{ page }}\n </button>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Next page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Last page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00BB\n </button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4346
+ }
4347
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, decorators: [{
4348
+ type: Component,
4349
+ args: [{ selector: 'mn-collection-pagination', standalone: true, imports: [MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n mnButton\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"First page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Previous page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u2039\n </button>\n\n @for (page of visiblePages; track page) {\n <button\n (click)=\"pageChange.emit(page)\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"'Page ' + page\"\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n >{{ page }}\n </button>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Next page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Last page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00BB\n </button>\n </div>\n </div>\n}\n" }]
4350
+ }], propDecorators: { idPrefix: [{
4351
+ type: Input
4352
+ }], isPaginated: [{
4353
+ type: Input
4354
+ }], isServerPaginated: [{
4355
+ type: Input
4356
+ }], showLoadMore: [{
4357
+ type: Input
4358
+ }], loadingMoreRows: [{
4359
+ type: Input
4360
+ }], currentPage: [{
4361
+ type: Input
4362
+ }], pageSize: [{
4363
+ type: Input
4364
+ }], totalPages: [{
4365
+ type: Input
4366
+ }], totalItemCount: [{
4367
+ type: Input
4368
+ }], visiblePages: [{
4369
+ type: Input
4370
+ }], pageSizeSelectOptions: [{
4371
+ type: Input
4372
+ }], labels: [{
4373
+ type: Input
4374
+ }], loadMore: [{
4375
+ type: Output
4376
+ }], pageChange: [{
4377
+ type: Output
4378
+ }], pageSizeChange: [{
4379
+ type: Output
4380
+ }] } });
4381
+
4382
+ class MnTable extends MnSelectableCollectionBase {
4383
+ sortChange = new EventEmitter();
4384
+ rowClick = new EventEmitter();
4385
+ currentSort = null;
4386
+ /** Per-column filter values keyed by column key. */
4387
+ columnFilters = {};
4388
+ componentName = 'MnTable';
4389
+ get trackedToolbarTemplate() {
4390
+ return this.dataSource?.toolbarLeftTemplate;
4175
4391
  }
4176
- /** Page-size options formatted for mn-select. */
4177
- get pageSizeSelectOptions() {
4178
- return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
4392
+ /** Updates a column filter value and re-applies filtering. */
4393
+ onColumnFilter(columnKey, value) {
4394
+ this.columnFilters[columnKey] = value;
4395
+ this.currentPage = 1;
4396
+ this.applyFilter(false);
4397
+ this.cdr.markForCheck();
4179
4398
  }
4180
4399
  /** Filter options formatted for mn-select for a given column. */
4181
4400
  getFilterSelectOptions(column) {
@@ -4185,59 +4404,63 @@ class MnTable {
4185
4404
  ...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
4186
4405
  ];
4187
4406
  }
4188
- goToPage(page) {
4189
- if (page < 1 || page > this.totalPages)
4407
+ // ── Column Filters ──
4408
+ /** Whether any column has filtering enabled. */
4409
+ get hasColumnFilters() {
4410
+ return this.dataSource.columns.some(c => c.filterable);
4411
+ }
4412
+ sort(column) {
4413
+ if (!column.sortType || column.sortType === ColumnSortType.NONE)
4190
4414
  return;
4191
- this.currentPage = page;
4192
- if (this.dataSource.paginationMode === 'client-side-pagination') {
4193
- this.applyPagination();
4415
+ if (this.currentSort?.columnKey === column.key) {
4416
+ this.currentSort = this.currentSort.direction === 'asc'
4417
+ ? { columnKey: column.key, direction: 'desc' }
4418
+ : null;
4194
4419
  }
4195
4420
  else {
4196
- this.dataSource.onPageChange?.(page);
4421
+ this.currentSort = { columnKey: column.key, direction: 'asc' };
4197
4422
  }
4198
- this.cdr.markForCheck();
4423
+ this.sortChange.emit(this.currentSort);
4424
+ this.applyFilter(false);
4199
4425
  }
4200
- onPageSizeChange(newSize) {
4201
- this.pageSize = newSize;
4202
- this.currentPage = 1;
4203
- if (this.dataSource.paginationMode === 'client-side-pagination') {
4204
- this.applyPagination();
4205
- }
4206
- else {
4207
- this.dataSource.onPageSizeChange?.(newSize);
4426
+ onRowClick(row) {
4427
+ if (this.hasSelection) {
4428
+ this.toggle(row);
4208
4429
  }
4209
- this.cdr.markForCheck();
4430
+ this.dataSource.onRowClick?.(row);
4431
+ this.rowClick.emit(row);
4210
4432
  }
4211
- get visiblePages() {
4212
- const total = this.totalPages;
4213
- const current = this.currentPage;
4214
- const maxVisible = 3;
4215
- let start = Math.max(1, current - Math.floor(maxVisible / 2));
4216
- let end = start + maxVisible - 1;
4217
- if (end > total) {
4218
- end = total;
4219
- start = Math.max(1, end - maxVisible + 1);
4220
- }
4221
- const pages = [];
4222
- for (let i = start; i <= end; i++) {
4223
- pages.push(i);
4224
- }
4225
- return pages;
4433
+ // ── Sorting ──
4434
+ /**
4435
+ * Resolves the skeleton placeholder config for a column's cells.
4436
+ * Falls back to a text-shaped bar at 75% width (the previous default); any
4437
+ * fields the column provides override that default.
4438
+ */
4439
+ getColumnSkeletonData(column) {
4440
+ const skeleton = column.skeleton;
4441
+ const overrides = skeleton && !this.isTemplateRef(skeleton) ? skeleton : {};
4442
+ return { shape: 'text', width: '75%', ...overrides };
4226
4443
  }
4227
- applyPagination() {
4228
- if (this.dataSource.paginationMode === 'client-side-pagination') {
4229
- const start = (this.currentPage - 1) * this.pageSize;
4230
- this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
4231
- }
4232
- else {
4233
- // Server always provides the correct page/slice — no client-side slicing.
4234
- this.paginatedItems = this.filteredItems;
4444
+ getSortIcon(column) {
4445
+ if (!this.currentSort || this.currentSort.columnKey !== column.key)
4446
+ return '';
4447
+ return this.currentSort.direction === 'asc' ? '▲' : '▼';
4448
+ }
4449
+ isSortable(column) {
4450
+ return !!column.sortType && column.sortType !== ColumnSortType.NONE;
4451
+ }
4452
+ // ── Row interaction ──
4453
+ /** Sets sort/filter state seeded from the data source before the first filter pass. */
4454
+ beforeInitialFilter() {
4455
+ super.beforeInitialFilter();
4456
+ this.currentSort = this.dataSource.defaultSort ?? null;
4457
+ for (const col of this.dataSource.columns) {
4458
+ if (col.filterable) {
4459
+ this.columnFilters[col.key] = '';
4460
+ }
4235
4461
  }
4236
4462
  }
4237
4463
  // ── Template helpers ──
4238
- isTemplateRef(value) {
4239
- return value instanceof TemplateRef;
4240
- }
4241
4464
  getCellValue(column, row) {
4242
4465
  if (typeof column.cell === 'function')
4243
4466
  return column.cell(row);
@@ -4249,9 +4472,6 @@ class MnTable {
4249
4472
  return column.cellSm.cell(row);
4250
4473
  return '';
4251
4474
  }
4252
- trackByID = (_index, row) => {
4253
- return this.dataSource.getID(row);
4254
- };
4255
4475
  trackByKey = (_index, column) => {
4256
4476
  return column.key;
4257
4477
  };
@@ -4263,18 +4483,25 @@ class MnTable {
4263
4483
  count++;
4264
4484
  return count;
4265
4485
  }
4266
- // ── Skeleton rows for loading ──
4267
- get skeletonRows() {
4268
- return Array.from({ length: 5 });
4269
- }
4270
- // ── Private ──
4271
- applyFilterAndSort(searchForItems) {
4272
- let items = this.dataSource.dataRows.value ?? [];
4273
- // Skip client-side search filtering when server handles it.
4274
- if (!this.isServerSearched && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue && this.searchValue.length > 0) {
4275
- const term = this.searchValue.toLowerCase();
4276
- items = items.filter(row => this.dataSource.isInSearch(row, term));
4486
+ // ── Skeleton ──
4487
+ /**
4488
+ * Resolves table-specific translation keys (column headers/filters) plus the
4489
+ * shared keys handled by the base.
4490
+ */
4491
+ resolveTranslationKeys() {
4492
+ super.resolveTranslationKeys();
4493
+ for (const col of this.dataSource.columns) {
4494
+ if (col.headerKey) {
4495
+ col.header = this.lang.t(col.headerKey);
4496
+ }
4497
+ if (col.filterPlaceholderKey) {
4498
+ col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
4499
+ }
4277
4500
  }
4501
+ }
4502
+ // ── Filtering & sorting ──
4503
+ applyFilter(searchForItems) {
4504
+ let items = this.applySearchFilter(this.dataSource.dataRows.value ?? []);
4278
4505
  // Per-column filters
4279
4506
  for (const col of this.dataSource.columns) {
4280
4507
  const filterValue = this.columnFilters[col.key];
@@ -4331,52 +4558,13 @@ class MnTable {
4331
4558
  }
4332
4559
  });
4333
4560
  }
4334
- processLoadedRows(rows) {
4335
- const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
4336
- this.dataSource.dataRows.next(merged);
4337
- this.loadingMoreRows = false;
4338
- this.applyFilterAndSort(false);
4339
- }
4340
- validateDataSource() {
4341
- const mode = this.dataSource.paginationMode;
4342
- if (mode === 'paginated') {
4343
- if (!this.dataSource.onPageChange) {
4344
- throw new Error(`[MnTable] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
4345
- }
4346
- if (this.dataSource.totalItems == null) {
4347
- throw new Error(`[MnTable] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
4348
- }
4349
- }
4350
- if (mode === 'load-more' || mode === 'infinite-scroll') {
4351
- if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
4352
- throw new Error(`[MnTable] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
4353
- }
4354
- }
4355
- // Validate pageSize is one of pageSizeOptions when pagination is active
4356
- if (mode && mode !== 'none') {
4357
- const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
4358
- const size = this.dataSource.pageSize ?? 10;
4359
- if (!options.includes(size)) {
4360
- throw new Error(`[MnTable] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
4361
- }
4362
- }
4363
- }
4364
- emitSelection() {
4365
- const rows = (this.dataSource.dataRows.value ?? []).filter(r => this.selectedIds.has(this.dataSource.getID(r)));
4366
- this.dataSource.selectedRows?.next(rows);
4367
- this.selectionChange.emit(rows);
4368
- }
4369
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
4370
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", inputs: { dataSource: "dataSource" }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick" }, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 -->\n @if (hasColumnFilters) {\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 (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 <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)=\"toggleRow(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 button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'outline', color: 'primary' }\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-table-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { 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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4561
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: null, target: i0.ɵɵFactoryTarget.Component });
4562
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 -->\n @if (hasColumnFilters) {\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 (dataSource.isDataLoading) {\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 {\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 <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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4371
4563
  }
4372
4564
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
4373
4565
  type: Component,
4374
- args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 -->\n @if (hasColumnFilters) {\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 (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 <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)=\"toggleRow(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 button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'outline', color: 'primary' }\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-table-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n" }]
4375
- }], propDecorators: { dataSource: [{
4376
- type: Input
4377
- }], sortChange: [{
4378
- type: Output
4379
- }], selectionChange: [{
4566
+ args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 -->\n @if (hasColumnFilters) {\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 (dataSource.isDataLoading) {\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 {\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 <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" }]
4567
+ }], propDecorators: { sortChange: [{
4380
4568
  type: Output
4381
4569
  }], rowClick: [{
4382
4570
  type: Output
@@ -5129,7 +5317,7 @@ class MnFormBodyComponent {
5129
5317
  }
5130
5318
  }
5131
5319
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5132
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n &#9733;\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">&times;</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
5320
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n &#9733;\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">&times;</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", outputs: ["sortChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
5133
5321
  }
5134
5322
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
5135
5323
  type: Component,
@@ -5619,11 +5807,11 @@ class MnWizardBodyComponent {
5619
5807
  }
5620
5808
  }
5621
5809
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5622
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col h-full min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions"], outputs: ["actionClick"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
5810
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard -->\n <div class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions"], outputs: ["actionClick"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
5623
5811
  }
5624
5812
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
5625
5813
  type: Component,
5626
- args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent], template: "<div class=\"flex flex-col h-full min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
5814
+ args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent], template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard -->\n <div class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
5627
5815
  }], propDecorators: { config: [{
5628
5816
  type: Input
5629
5817
  }], modalRef: [{
@@ -5815,17 +6003,7 @@ class MnModalShellComponent {
5815
6003
  asCustom(config) {
5816
6004
  return config;
5817
6005
  }
5818
- get hostClasses() {
5819
- const size = this.config.sizeWidth || ModalSize.MD;
5820
- const closing = this.isClosing ? ' closing' : '';
5821
- const animType = typeof this.config.animation === 'string'
5822
- ? this.config.animation
5823
- : this.config.animation?.type || 'slide';
5824
- const animation = ` anim-${animType}`;
5825
- const stacked = this.isStacked ? ' is-stacked' : '';
5826
- const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
5827
- return `modal-shell modal-${size}${closing}${animation}${stacked}${mobileSheet}`;
5828
- }
6006
+ static SWIPE_DISMISS_THRESHOLD = 150;
5829
6007
  /** Whether this modal renders as a bottom sheet on small screens (default: true). */
5830
6008
  get isMobileSheet() {
5831
6009
  return this.config.mobileBottomSheet !== false;
@@ -5856,7 +6034,9 @@ class MnModalShellComponent {
5856
6034
  onCloseButtonClick() {
5857
6035
  this.handleClose(ModalCloseReason.DISMISSED);
5858
6036
  }
5859
- static SWIPE_DISMISS_THRESHOLD = 100;
6037
+ /** True once a swipe has crossed the dismiss threshold — slides the sheet off-screen
6038
+ * via the transform transition instead of replaying the slide-up keyframe. */
6039
+ swipeDismissing = false;
5860
6040
  // =========================
5861
6041
  // Mobile bottom-sheet swipe-to-dismiss (via the grabber handle)
5862
6042
  // =========================
@@ -5864,6 +6044,18 @@ class MnModalShellComponent {
5864
6044
  sheetDragY = 0;
5865
6045
  /** True while the user is actively dragging the grabber (disables snap transition). */
5866
6046
  isDraggingSheet = false;
6047
+ get hostClasses() {
6048
+ const size = this.config.sizeWidth || ModalSize.MD;
6049
+ const closing = this.isClosing ? ' closing' : '';
6050
+ const animType = typeof this.config.animation === 'string'
6051
+ ? this.config.animation
6052
+ : this.config.animation?.type || 'slide';
6053
+ const animation = ` anim-${animType}`;
6054
+ const stacked = this.isStacked ? ' is-stacked' : '';
6055
+ const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
6056
+ const swiping = this.swipeDismissing ? ' swipe-dismissing' : '';
6057
+ return `modal-shell modal-${size}${closing}${animation}${stacked}${mobileSheet}${swiping}`;
6058
+ }
5867
6059
  dragStartY = 0;
5868
6060
  /** Whether the sheet can be dismissed at all (drives whether the swipe is armed). */
5869
6061
  get canClose() {
@@ -5896,10 +6088,16 @@ class MnModalShellComponent {
5896
6088
  this.isDraggingSheet = false;
5897
6089
  if (this.sheetDragY > MnModalShellComponent.SWIPE_DISMISS_THRESHOLD) {
5898
6090
  const closed = await this.handleClose(ModalCloseReason.DISMISSED);
5899
- if (!closed) {
6091
+ if (closed) {
6092
+ // Continue the gesture: glide the sheet the rest of the way down rather than
6093
+ // snapping back to 0 and replaying the slide-up keyframe (which looked un-animated).
6094
+ this.swipeDismissing = true;
6095
+ this.sheetDragY = window.innerHeight;
6096
+ this.cdr.detectChanges();
6097
+ }
6098
+ else {
5900
6099
  this.sheetDragY = 0; // guard rejected — spring back
5901
6100
  }
5902
- // closed === true: leave it; the closing animation takes over.
5903
6101
  }
5904
6102
  else {
5905
6103
  this.sheetDragY = 0; // not far enough — spring back
@@ -6033,7 +6231,7 @@ class MnModalShellComponent {
6033
6231
  }
6034
6232
  }
6035
6233
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6036
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-1 min-h-0 overflow-hidden\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .2s ease}.modal-container.sheet-dragging{transition:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;max-height:92vh;border-radius:1rem 1rem 0 0;animation:slideUpIn .25s ease-out}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .25s ease-out}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:slideUpOut .2s ease-in forwards}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions"], outputs: ["actionClick"] }] });
6234
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .2s ease}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .2s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;max-height:92vh;border-radius:1rem 1rem 0 0;animation:slideUpIn .25s ease-out}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .25s ease-out}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:slideUpOut .2s ease-in forwards}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions"], outputs: ["actionClick"] }] });
6037
6235
  }
6038
6236
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
6039
6237
  type: Component,
@@ -6044,17 +6242,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6044
6242
  MnConfirmationBodyComponent,
6045
6243
  MnCustomBodyHostComponent,
6046
6244
  MnFooterActionsComponent,
6047
- ], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-1 min-h-0 overflow-hidden\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .2s ease}.modal-container.sheet-dragging{transition:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;max-height:92vh;border-radius:1rem 1rem 0 0;animation:slideUpIn .25s ease-out}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .25s ease-out}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:slideUpOut .2s ease-in forwards}}\n"] }]
6245
+ ], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .2s ease}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .2s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;max-height:92vh;border-radius:1rem 1rem 0 0;animation:slideUpIn .25s ease-out}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .25s ease-out}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:slideUpOut .2s ease-in forwards}}\n"] }]
6048
6246
  }], propDecorators: { config: [{
6049
6247
  type: Input
6050
6248
  }], modalRef: [{
6051
6249
  type: Input
6052
- }], hostClasses: [{
6053
- type: HostBinding,
6054
- args: ['class']
6055
6250
  }], onEscapeKey: [{
6056
6251
  type: HostListener,
6057
6252
  args: ['document:keydown.escape', ['$event']]
6253
+ }], hostClasses: [{
6254
+ type: HostBinding,
6255
+ args: ['class']
6058
6256
  }] } });
6059
6257
 
6060
6258
  class MnModalService {
@@ -6080,260 +6278,123 @@ class MnModalService {
6080
6278
  // Attach to application
6081
6279
  this.appRef.attachView(componentRef.hostView);
6082
6280
  const domElem = componentRef.location.nativeElement;
6083
- document.body.appendChild(domElem);
6084
- // Clean up on close
6085
- modalRef.afterClosed$.subscribe(() => {
6086
- this.appRef.detachView(componentRef.hostView);
6087
- domElem.remove();
6088
- // Update stack
6089
- const index = this.modalStack.indexOf(modalRef);
6090
- if (index > -1) {
6091
- this.modalStack.splice(index, 1);
6092
- if (this.modalStack.length > 0) {
6093
- const topModal = this.modalStack[this.modalStack.length - 1];
6094
- topModal.component.isStacked = false;
6095
- }
6096
- }
6097
- });
6098
- return modalRef;
6099
- }
6100
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6101
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, providedIn: 'root' });
6102
- }
6103
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, decorators: [{
6104
- type: Injectable,
6105
- args: [{
6106
- providedIn: 'root',
6107
- }]
6108
- }] });
6109
-
6110
- // Types
6111
-
6112
- class MnList {
6113
- dataSource;
6114
- selectionChange = new EventEmitter();
6115
- itemClick = new EventEmitter();
6116
- filteredItems = [];
6117
- paginatedItems = [];
6118
- searchValue = '';
6119
- loadingMoreRows = false;
6120
- selectedIds = new Set();
6121
- currentPage = 1;
6122
- pageSize = 10;
6123
- cdr = inject(ChangeDetectorRef);
6124
- dataSubscription;
6125
- searchSubject = new Subject();
6126
- searchSubscription;
6127
- /** Tracks the previous toolbar template reference for change detection. */
6128
- previousToolbarTemplate;
6129
- ngDoCheck() {
6130
- const currentTemplate = this.dataSource?.toolbarTemplate;
6131
- if (currentTemplate !== this.previousToolbarTemplate) {
6132
- this.previousToolbarTemplate = currentTemplate;
6133
- this.cdr.markForCheck();
6134
- }
6135
- }
6136
- get showLoadMore() {
6137
- const mode = this.dataSource.paginationMode ?? 'load-more';
6138
- // Server-side load-more: check if there are more items to load.
6139
- if (this.dataSource.onLoadMore) {
6140
- const totalItems = this.dataSource.totalItems ?? 0;
6141
- return mode === 'load-more' && this.filteredItems.length < totalItems;
6142
- }
6143
- const strategy = this.dataSource.paginationStrategy;
6144
- const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
6145
- return mode === 'load-more' && hasMore;
6146
- }
6147
- ngOnDestroy() {
6148
- this.dataSubscription?.unsubscribe();
6149
- this.searchSubscription?.unsubscribe();
6150
- }
6151
- // ── Search ──
6152
- get isPaginated() {
6153
- const mode = this.dataSource.paginationMode;
6154
- return mode === 'paginated' || mode === 'client-side-pagination';
6155
- }
6156
- // ── Selection ──
6157
- isSelected(item) {
6158
- return this.selectedIds.has(this.dataSource.getID(item));
6159
- }
6160
- toggleItem(item) {
6161
- const id = this.dataSource.getID(item);
6162
- const mode = this.dataSource.selectionMode ?? 'none';
6163
- if (mode === 'single') {
6164
- this.selectedIds.clear();
6165
- this.selectedIds.add(id);
6166
- }
6167
- else if (mode === 'multi') {
6168
- if (this.selectedIds.has(id)) {
6169
- this.selectedIds.delete(id);
6170
- }
6171
- else {
6172
- this.selectedIds.add(id);
6281
+ document.body.appendChild(domElem);
6282
+ // Clean up on close
6283
+ modalRef.afterClosed$.subscribe(() => {
6284
+ this.appRef.detachView(componentRef.hostView);
6285
+ domElem.remove();
6286
+ // Update stack
6287
+ const index = this.modalStack.indexOf(modalRef);
6288
+ if (index > -1) {
6289
+ this.modalStack.splice(index, 1);
6290
+ if (this.modalStack.length > 0) {
6291
+ const topModal = this.modalStack[this.modalStack.length - 1];
6292
+ topModal.component.isStacked = false;
6293
+ }
6173
6294
  }
6174
- }
6175
- this.emitSelection();
6295
+ });
6296
+ return modalRef;
6176
6297
  }
6177
- toggleAll() {
6178
- if (this.selectedIds.size === this.filteredItems.length) {
6179
- this.selectedIds.clear();
6180
- }
6181
- else {
6182
- this.filteredItems.forEach(item => this.selectedIds.add(this.dataSource.getID(item)));
6298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6299
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, providedIn: 'root' });
6300
+ }
6301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, decorators: [{
6302
+ type: Injectable,
6303
+ args: [{
6304
+ providedIn: 'root',
6305
+ }]
6306
+ }] });
6307
+
6308
+ // Types
6309
+
6310
+ /** Default skeleton lines reproducing the previous two-bar placeholder. */
6311
+ const DEFAULT_LIST_SKELETON_LINES = [
6312
+ { shape: 'text', width: '75%' },
6313
+ { shape: 'text', width: '50%', height: '0.75rem' },
6314
+ ];
6315
+ class MnList extends MnSelectableCollectionBase {
6316
+ itemClick = new EventEmitter();
6317
+ componentName = 'MnList';
6318
+ /** Skeleton lines rendered for each placeholder item, falling back to the default two-bar layout. */
6319
+ get skeletonLines() {
6320
+ const skeleton = this.dataSource.skeleton;
6321
+ if (skeleton && !this.isTemplateRef(skeleton)) {
6322
+ return skeleton.lines;
6183
6323
  }
6184
- this.emitSelection();
6185
- }
6186
- get allSelected() {
6187
- return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
6188
- }
6189
- get hasSelection() {
6190
- return (this.dataSource.selectionMode ?? 'none') !== 'none';
6191
- }
6192
- get isMultiSelect() {
6193
- return this.dataSource.selectionMode === 'multi';
6324
+ return DEFAULT_LIST_SKELETON_LINES;
6194
6325
  }
6195
6326
  // ── Item interaction ──
6196
6327
  onItemClick(item) {
6197
6328
  this.dataSource.onItemClick?.(item);
6198
6329
  this.itemClick.emit(item);
6199
6330
  }
6200
- // ── Pagination ──
6201
- /** Whether the list is in server-side paginated mode (always true when paginated or load-more). */
6202
- get isServerPaginated() {
6203
- const mode = this.dataSource.paginationMode ?? 'load-more';
6204
- return mode === 'paginated' || mode === 'load-more';
6205
- }
6206
- /** Total number of items, accounting for server-side pagination. */
6207
- get totalItemCount() {
6208
- if (this.isServerPaginated && this.dataSource.totalItems != null) {
6209
- return this.dataSource.totalItems;
6210
- }
6211
- return this.filteredItems.length;
6212
- }
6213
- // ── Paginated Mode ──
6214
- get totalPages() {
6215
- return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
6216
- }
6217
- ngOnInit() {
6218
- this.validateDataSource();
6219
- this.pageSize = this.dataSource.pageSize ?? 10;
6220
- // Pre-select items from initialSelectedIds if provided
6221
- if (this.dataSource.initialSelectedIds?.length) {
6222
- for (const id of this.dataSource.initialSelectedIds) {
6223
- this.selectedIds.add(id);
6224
- }
6225
- this.emitSelection();
6226
- }
6227
- this.applyFilter(false);
6228
- this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
6229
- this.applyFilter(false);
6230
- this.cdr.markForCheck();
6231
- });
6232
- this.searchSubscription = this.searchSubject
6233
- .pipe(debounceTime(300))
6234
- .subscribe(value => {
6235
- this.searchValue = value;
6236
- this.applyFilter(true);
6237
- this.cdr.markForCheck();
6238
- });
6239
- }
6240
- onSearch(searchString) {
6241
- this.currentPage = 1;
6242
- if (this.isServerPaginated) {
6243
- this.searchValue = searchString;
6244
- this.dataSource.onServerSearch?.(searchString);
6245
- this.cdr.markForCheck();
6246
- }
6247
- else {
6248
- this.searchSubject.next(searchString);
6249
- }
6250
- }
6251
- loadMoreRows() {
6252
- // Server-side infinite scroll: delegate to consumer callback.
6253
- if (this.dataSource.onLoadMore) {
6254
- this.dataSource.onLoadMore();
6255
- return;
6256
- }
6257
- if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
6258
- return;
6259
- this.loadingMoreRows = true;
6260
- const promise = (this.searchValue.length > 0 && this.dataSource.searchForAdditionalItems)
6261
- ? this.dataSource.searchForAdditionalItems(this.searchValue)
6262
- : this.dataSource.loadAdditionalRows();
6263
- promise
6264
- .then(rows => this.processLoadedRows(rows))
6265
- .catch(() => this.loadingMoreRows = false);
6266
- }
6267
- get resolvedPageSizeOptions() {
6268
- return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
6269
- }
6270
- /** Page-size options formatted for mn-select. */
6271
- get pageSizeSelectOptions() {
6272
- return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
6331
+ // ── Skeleton ──
6332
+ get trackedToolbarTemplate() {
6333
+ return this.dataSource?.toolbarTemplate;
6273
6334
  }
6274
- goToPage(page) {
6275
- if (page < 1 || page > this.totalPages)
6276
- return;
6277
- this.currentPage = page;
6278
- if (this.dataSource.paginationMode === 'client-side-pagination') {
6279
- this.applyPagination();
6280
- }
6281
- else {
6282
- this.dataSource.onPageChange?.(page);
6335
+ // ── Filtering ──
6336
+ applyFilter(searchForItems) {
6337
+ this.filteredItems = this.applySearchFilter(this.dataSource.dataRows.value);
6338
+ this.applyPagination();
6339
+ if (searchForItems) {
6340
+ this.loadMoreRows();
6283
6341
  }
6284
- this.cdr.markForCheck();
6285
6342
  }
6286
- onPageSizeChange(newSize) {
6287
- this.pageSize = newSize;
6288
- this.currentPage = 1;
6289
- if (this.dataSource.paginationMode === 'client-side-pagination') {
6290
- this.applyPagination();
6291
- }
6292
- else {
6293
- this.dataSource.onPageSizeChange?.(newSize);
6294
- }
6295
- this.cdr.markForCheck();
6343
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: null, target: i0.ɵɵFactoryTarget.Component });
6344
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\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-list\"\n></mn-collection-pagination>\n", styles: [""], 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: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6345
+ }
6346
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
6347
+ type: Component,
6348
+ args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnSkeleton, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\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-list\"\n></mn-collection-pagination>\n" }]
6349
+ }], propDecorators: { itemClick: [{
6350
+ type: Output
6351
+ }] } });
6352
+
6353
+ /** Default card skeleton: an image block plus two text bars. */
6354
+ const DEFAULT_GRID_SKELETON_LINES = [
6355
+ { shape: 'rectangle', width: '100%', height: '8rem' },
6356
+ { shape: 'text', width: '75%' },
6357
+ { shape: 'text', width: '50%', height: '0.75rem' },
6358
+ ];
6359
+ /**
6360
+ * Responsive card-grid component. Shares the collection chrome (search, every
6361
+ * pagination mode, loading skeleton, empty state, toolbar, i18n) with
6362
+ * {@link import('../mn-list').MnList} and {@link import('../mn-table').MnTable}
6363
+ * via {@link MnCollectionBase}, and lays items out as cards instead of rows.
6364
+ * Selection is intentionally not supported.
6365
+ */
6366
+ class MnGrid extends MnCollectionBase {
6367
+ itemClick = new EventEmitter();
6368
+ componentName = 'MnGrid';
6369
+ /** Whether the grid uses auto-fit (minCardWidth) instead of explicit columns. */
6370
+ get isAutoLayout() {
6371
+ return !!this.dataSource.layout?.minCardWidth;
6296
6372
  }
6297
- get visiblePages() {
6298
- const total = this.totalPages;
6299
- const current = this.currentPage;
6300
- const maxVisible = 3;
6301
- let start = Math.max(1, current - Math.floor(maxVisible / 2));
6302
- let end = start + maxVisible - 1;
6303
- if (end > total) {
6304
- end = total;
6305
- start = Math.max(1, end - maxVisible + 1);
6306
- }
6307
- const pages = [];
6308
- for (let i = start; i <= end; i++) {
6309
- pages.push(i);
6373
+ // ── Layout ──
6374
+ /** Skeleton lines for the default/lines placeholder; null when a custom template is used. */
6375
+ get skeletonLines() {
6376
+ const skeleton = this.dataSource.skeleton;
6377
+ if (skeleton && !this.isTemplateRef(skeleton)) {
6378
+ return skeleton.lines;
6310
6379
  }
6311
- return pages;
6380
+ return DEFAULT_GRID_SKELETON_LINES;
6312
6381
  }
6313
- trackByID = (_index, item) => {
6314
- return this.dataSource.getID(item);
6315
- };
6316
- // ── Skeleton rows for loading ──
6317
- get skeletonRows() {
6318
- return Array.from({ length: 5 });
6382
+ // ── Item interaction ──
6383
+ get trackedToolbarTemplate() {
6384
+ return this.dataSource?.toolbarTemplate;
6319
6385
  }
6320
- // ── Private ──
6321
- applyPagination() {
6322
- if (this.dataSource.paginationMode === 'client-side-pagination') {
6323
- const start = (this.currentPage - 1) * this.pageSize;
6324
- this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
6325
- }
6326
- else {
6327
- // Server always provides the correct page/slice — no client-side slicing.
6328
- this.paginatedItems = this.filteredItems;
6329
- }
6386
+ // ── Skeleton ──
6387
+ onItemClick(item) {
6388
+ this.dataSource.onItemClick?.(item);
6389
+ this.itemClick.emit(item);
6330
6390
  }
6391
+ // ── Filtering ──
6331
6392
  applyFilter(searchForItems) {
6332
- let items = this.dataSource.dataRows.value;
6333
- // Skip client-side search filtering when server handles it.
6334
- if (!this.isServerPaginated && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
6335
- const term = this.searchValue.toLowerCase();
6336
- items = items.filter(row => this.dataSource.isInSearch(row, term));
6393
+ let items = this.applySearchFilter(this.dataSource.dataRows.value ?? []);
6394
+ // Preview cap: show only the first `maxItems` cards (pager stays hidden).
6395
+ const maxItems = this.dataSource.layout?.maxItems;
6396
+ if (maxItems != null) {
6397
+ items = items.slice(0, maxItems);
6337
6398
  }
6338
6399
  this.filteredItems = items;
6339
6400
  this.applyPagination();
@@ -6341,52 +6402,13 @@ class MnList {
6341
6402
  this.loadMoreRows();
6342
6403
  }
6343
6404
  }
6344
- processLoadedRows(rows) {
6345
- const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
6346
- this.dataSource.dataRows.next(merged);
6347
- this.loadingMoreRows = false;
6348
- this.applyFilter(false);
6349
- }
6350
- validateDataSource() {
6351
- const mode = this.dataSource.paginationMode;
6352
- if (mode === 'paginated') {
6353
- if (!this.dataSource.onPageChange) {
6354
- throw new Error(`[MnList] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
6355
- }
6356
- if (this.dataSource.totalItems == null) {
6357
- throw new Error(`[MnList] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
6358
- }
6359
- }
6360
- if (mode === 'load-more' || mode === 'infinite-scroll') {
6361
- if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
6362
- throw new Error(`[MnList] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
6363
- }
6364
- }
6365
- // Validate pageSize is one of pageSizeOptions when pagination is active
6366
- if (mode && mode !== 'none') {
6367
- const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
6368
- const size = this.dataSource.pageSize ?? 10;
6369
- if (!options.includes(size)) {
6370
- throw new Error(`[MnList] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
6371
- }
6372
- }
6373
- }
6374
- emitSelection() {
6375
- const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
6376
- this.dataSource.selectedRows?.next(rows);
6377
- this.selectionChange.emit(rows);
6378
- }
6379
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: [], target: i0.ɵɵFactoryTarget.Component });
6380
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", inputs: { dataSource: "dataSource" }, outputs: { selectionChange: "selectionChange", itemClick: "itemClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleItem(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-list-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6405
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, deps: null, target: i0.ɵɵFactoryTarget.Component });
6406
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n@if (dataSource.canSearch || dataSource.toolbarTemplate) {\n <div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full max-w-xs\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n </div>\n}\n\n<!-- Loading state -->\n@if (dataSource.isDataLoading) {\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-busy=\"true\"\n aria-label=\"Loading\"\n class=\"mn-grid\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n} @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] rounded-xl border border-dashed border-base-content/15 bg-base-100\">\n <p class=\"text-sm text-base-content/40\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Card grid -->\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-label=\"Card grid\"\n class=\"mn-grid\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\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-grid\"\n></mn-collection-pagination>\n", styles: [":host{display:block}.mn-grid{display:grid;gap:var(--mn-grid-gap, 1rem);grid-template-columns:repeat(var(--mn-grid-cols-base, 1),minmax(0,1fr))}@media(min-width:640px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)),minmax(0,1fr))}}@media(min-width:768px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))),minmax(0,1fr))}}@media(min-width:1024px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)))),minmax(0,1fr))}}@media(min-width:1280px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-xl, var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))))),minmax(0,1fr))}}.mn-grid.mn-grid--auto{grid-template-columns:repeat(auto-fit,minmax(var(--mn-grid-min, 18rem),1fr))}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6381
6407
  }
6382
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
6408
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, decorators: [{
6383
6409
  type: Component,
6384
- args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnCheckbox, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleItem(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-list-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n" }]
6385
- }], propDecorators: { dataSource: [{
6386
- type: Input
6387
- }], selectionChange: [{
6388
- type: Output
6389
- }], itemClick: [{
6410
+ args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n@if (dataSource.canSearch || dataSource.toolbarTemplate) {\n <div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full max-w-xs\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n </div>\n}\n\n<!-- Loading state -->\n@if (dataSource.isDataLoading) {\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-busy=\"true\"\n aria-label=\"Loading\"\n class=\"mn-grid\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n} @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] rounded-xl border border-dashed border-base-content/15 bg-base-100\">\n <p class=\"text-sm text-base-content/40\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Card grid -->\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-label=\"Card grid\"\n class=\"mn-grid\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\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-grid\"\n></mn-collection-pagination>\n", styles: [":host{display:block}.mn-grid{display:grid;gap:var(--mn-grid-gap, 1rem);grid-template-columns:repeat(var(--mn-grid-cols-base, 1),minmax(0,1fr))}@media(min-width:640px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)),minmax(0,1fr))}}@media(min-width:768px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))),minmax(0,1fr))}}@media(min-width:1024px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)))),minmax(0,1fr))}}@media(min-width:1280px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-xl, var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))))),minmax(0,1fr))}}.mn-grid.mn-grid--auto{grid-template-columns:repeat(auto-fit,minmax(var(--mn-grid-min, 18rem),1fr))}\n"] }]
6411
+ }], propDecorators: { itemClick: [{
6390
6412
  type: Output
6391
6413
  }] } });
6392
6414
 
@@ -7865,53 +7887,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
7865
7887
  }]
7866
7888
  }] });
7867
7889
 
7868
- const mnSkeletonVariants = tv({
7869
- base: 'bg-base-300 relative overflow-hidden',
7870
- variants: {
7871
- shape: {
7872
- rectangle: 'rounded-md',
7873
- circle: 'rounded-full',
7874
- text: 'rounded h-4',
7875
- },
7876
- },
7877
- defaultVariants: {
7878
- shape: 'rectangle',
7879
- },
7880
- });
7881
-
7882
- class MnSkeleton {
7883
- data = {};
7884
- get hostClasses() {
7885
- return mnSkeletonVariants({ shape: this.data.shape });
7886
- }
7887
- get width() {
7888
- return this.data.width ?? null;
7889
- }
7890
- get height() {
7891
- return this.data.height ?? null;
7892
- }
7893
- get isAnimated() {
7894
- return this.data.animated !== false;
7895
- }
7896
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
7897
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSkeleton, isStandalone: true, selector: "mn-skeleton", inputs: { data: "data" }, host: { properties: { "class": "this.hostClasses", "style.width": "this.width", "style.height": "this.height" } }, ngImport: i0, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.15) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}\n"] });
7898
- }
7899
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, decorators: [{
7900
- type: Component,
7901
- args: [{ selector: 'mn-skeleton', standalone: true, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.15) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}\n"] }]
7902
- }], propDecorators: { data: [{
7903
- type: Input
7904
- }], hostClasses: [{
7905
- type: HostBinding,
7906
- args: ['class']
7907
- }], width: [{
7908
- type: HostBinding,
7909
- args: ['style.width']
7910
- }], height: [{
7911
- type: HostBinding,
7912
- args: ['style.height']
7913
- }] } });
7914
-
7915
7890
  /**
7916
7891
  * Injection token for the base URL used by all CRUD service requests.
7917
7892
  *
@@ -8413,5 +8388,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
8413
8388
  * Generated bundle index. Do not edit.
8414
8389
  */
8415
8390
 
8416
- 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_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, 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, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
8391
+ 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_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, 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, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
8417
8392
  //# sourceMappingURL=mn-angular-lib.mjs.map