mn-angular-lib 1.0.2 → 1.0.5

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, Optional, Inject, HostBinding, Input, Component, inject, ChangeDetectionStrategy, signal, ElementRef, DestroyRef, Self, APP_INITIALIZER, SkipSelf, Attribute, Directive, Pipe, HostListener, ViewChild, forwardRef, Renderer2, EventEmitter, ChangeDetectorRef, TemplateRef, Output, ViewContainerRef, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent } from '@angular/core';
2
+ import { InjectionToken, Injectable, Optional, Inject, HostBinding, Input, Component, inject, ChangeDetectionStrategy, signal, APP_INITIALIZER, DestroyRef, SkipSelf, Attribute, Directive, Pipe, ElementRef, Self, HostListener, ViewChild, forwardRef, Renderer2, EventEmitter, ChangeDetectorRef, TemplateRef, Output, ViewContainerRef, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent } from '@angular/core';
3
3
  export { TemplateRef, Type } from '@angular/core';
4
4
  import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
5
5
  import * as i1 from '@angular/common';
@@ -220,7 +220,7 @@ const mnAlertVariants = tv({
220
220
  });
221
221
 
222
222
  const mnButtonVariants = tv({
223
- base: 'hover:cursor-pointer transition-colors duration-300 ease-in-out',
223
+ base: 'hover:cursor-pointer transition-all duration-300 ease-in-out',
224
224
  variants: {
225
225
  size: {
226
226
  sm: 'px-2 py-1 text-sm',
@@ -231,6 +231,7 @@ const mnButtonVariants = tv({
231
231
  fill: '',
232
232
  outline: 'bg-transparent border',
233
233
  text: 'bg-transparent',
234
+ textUnderline: 'bg-transparent underline underline-offset-2',
234
235
  },
235
236
  // Intentionally empty; resolved via compoundVariants
236
237
  color: {
@@ -258,19 +259,19 @@ const mnButtonVariants = tv({
258
259
  },
259
260
  compoundVariants: [
260
261
  // Fill
261
- { variant: 'fill', color: 'primary', class: 'bg-primary text-primary-content hover:bg-primary/80' },
262
- { variant: 'fill', color: 'secondary', class: 'bg-neutral text-neutral-content hover:bg-neutral/80' },
263
- { variant: 'fill', color: 'danger', class: 'bg-error text-error-content hover:bg-error/80' },
264
- { variant: 'fill', color: 'warning', class: 'bg-warning text-warning-content hover:bg-warning/80' },
265
- { variant: 'fill', color: 'success', class: 'bg-success text-success-content hover:bg-success/80' },
266
- { variant: 'fill', color: 'accent', class: 'bg-accent text-accent-content hover:bg-accent/80' },
262
+ { variant: 'fill', color: 'primary', class: 'bg-primary text-primary-content hover:brightness-60' },
263
+ { variant: 'fill', color: 'secondary', class: 'bg-neutral text-neutral-content hover:brightness-60' },
264
+ { variant: 'fill', color: 'danger', class: 'bg-error text-error-content hover:brightness-60' },
265
+ { variant: 'fill', color: 'warning', class: 'bg-warning text-warning-content hover:brightness-60' },
266
+ { variant: 'fill', color: 'success', class: 'bg-success text-success-content hover:brightness-60' },
267
+ { variant: 'fill', color: 'accent', class: 'bg-accent text-accent-content hover:brightness-60' },
267
268
  // Outline
268
- { variant: 'outline', color: 'primary', class: 'border-primary text-primary hover:bg-primary/10' },
269
- { variant: 'outline', color: 'secondary', class: 'border-neutral text-neutral hover:bg-neutral/10' },
270
- { variant: 'outline', color: 'danger', class: 'border-error text-error hover:bg-error/10' },
271
- { variant: 'outline', color: 'warning', class: 'border-warning text-warning hover:bg-warning/10' },
272
- { variant: 'outline', color: 'success', class: 'border-success text-success hover:bg-success/10' },
273
- { variant: 'outline', color: 'accent', class: 'border-accent text-accent hover:bg-accent/10' },
269
+ { variant: 'outline', color: 'primary', class: 'border-primary text-primary hover:bg-primary/40' },
270
+ { variant: 'outline', color: 'secondary', class: 'border-neutral text-neutral hover:bg-neutral/40' },
271
+ { variant: 'outline', color: 'danger', class: 'border-error text-error hover:bg-error/40' },
272
+ { variant: 'outline', color: 'warning', class: 'border-warning text-warning hover:bg-warning/40' },
273
+ { variant: 'outline', color: 'success', class: 'border-success text-success hover:bg-success/40' },
274
+ { variant: 'outline', color: 'accent', class: 'border-accent text-accent hover:bg-accent/40' },
274
275
  // Text
275
276
  { variant: 'text', color: 'primary', class: 'text-primary hover:bg-primary/10' },
276
277
  { variant: 'text', color: 'secondary', class: 'text-neutral hover:bg-neutral/10' },
@@ -278,6 +279,13 @@ const mnButtonVariants = tv({
278
279
  { variant: 'text', color: 'warning', class: 'text-warning hover:bg-warning/10' },
279
280
  { variant: 'text', color: 'success', class: 'text-success hover:bg-success/10' },
280
281
  { variant: 'text', color: 'accent', class: 'text-accent hover:bg-accent/10' },
282
+ // Text Underline
283
+ { variant: 'textUnderline', color: 'primary', class: 'text-primary underline underline-offset-2 hover:bg-primary/10' },
284
+ { variant: 'textUnderline', color: 'secondary', class: 'text-neutral underline underline-offset-2 hover:bg-neutral/10' },
285
+ { variant: 'textUnderline', color: 'danger', class: 'text-error underline underline-offset-2 hover:bg-error/10' },
286
+ { variant: 'textUnderline', color: 'warning', class: 'text-warning underline underline-offset-2 hover:bg-warning/10' },
287
+ { variant: 'textUnderline', color: 'success', class: 'text-success underline underline-offset-2 hover:bg-success/10' },
288
+ { variant: 'textUnderline', color: 'accent', class: 'text-accent underline underline-offset-2 hover:bg-accent/10' },
281
289
  ],
282
290
  defaultVariants: {
283
291
  size: 'md',
@@ -554,7 +562,7 @@ function pickAdapter(type) {
554
562
  }
555
563
 
556
564
  const mnInputFieldVariants = tv({
557
- base: 'bg-base-100 border-1 border-base-300 placeholder-base-content/50 text-base-content text-sm',
565
+ base: 'bg-base-100 border-1 border-base-300 placeholder-base-content/50 text-base-content text-sm outline-none focus:ring-1 focus:ring-primary',
558
566
  variants: {
559
567
  shadow: {
560
568
  true: 'shadow-lg',
@@ -606,6 +614,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
606
614
  args: [{ required: true }]
607
615
  }] } });
608
616
 
617
+ /**
618
+ * Types for mn-lib configuration.
619
+ */
620
+
609
621
  class MnLanguageService {
610
622
  http;
611
623
  _translations = {};
@@ -954,6 +966,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
954
966
  args: [{ providedIn: 'root' }]
955
967
  }], ctorParameters: () => [{ type: i1$1.HttpClient }] });
956
968
 
969
+ /**
970
+ * Provides an APP_INITIALIZER that loads the mn-lib configuration from the given URL
971
+ * during application bootstrap. The consuming application is responsible for providing
972
+ * HttpClient (e.g., via HttpClientModule or provideHttpClient()).
973
+ */
974
+ function provideMnConfig(url, debugMode = false) {
975
+ return [
976
+ {
977
+ provide: APP_INITIALIZER,
978
+ multi: true,
979
+ useFactory: (svc) => () => svc.load(url, debugMode),
980
+ deps: [MnConfigService],
981
+ },
982
+ ];
983
+ }
984
+
957
985
  /**
958
986
  * Represents the current section path based on nested mn-section directives.
959
987
  */
@@ -969,6 +997,185 @@ const MN_INSTANCE_ID = new InjectionToken('MN_INSTANCE_ID', {
969
997
  factory: () => null,
970
998
  });
971
999
 
1000
+ /**
1001
+ * Helper to provide a resolved, typed component config via DI.
1002
+ *
1003
+ * Usage in a component/module providers:
1004
+ * const MY_CFG = new InjectionToken<MyCfg>('MY_CFG');
1005
+ * providers: [ provideMnComponentConfig(MY_CFG, 'my-component') ]
1006
+ * Then in the component:
1007
+ * readonly cfg = inject(MY_CFG)
1008
+ *
1009
+ * The returned config object is **reactive**: when the active locale changes,
1010
+ * all translatable values are re-resolved in place so that templates using
1011
+ * `cfg.someLabel` automatically reflect the new language on the next change-detection cycle.
1012
+ */
1013
+ function provideMnComponentConfig(token, componentName, initial) {
1014
+ return {
1015
+ provide: token,
1016
+ deps: [
1017
+ MnConfigService,
1018
+ MnLanguageService,
1019
+ DestroyRef,
1020
+ [new Optional(), MN_SECTION_PATH],
1021
+ [new Optional(), MN_INSTANCE_ID],
1022
+ ],
1023
+ useFactory: (svc, lang, destroyRef, sectionPath, instanceId) => {
1024
+ const resolveConfig = () => {
1025
+ const resolved = svc.resolve(componentName, sectionPath ?? [], instanceId ?? undefined);
1026
+ return Object.assign({}, initial ?? {}, resolved);
1027
+ };
1028
+ // Create the initial config object that will be shared by reference.
1029
+ const cfg = resolveConfig();
1030
+ // Re-resolve translatable values whenever the locale changes.
1031
+ // skip(1) because the current locale was already used for the initial resolve.
1032
+ const sub = lang.locale$.subscribe(() => {
1033
+ const updated = resolveConfig();
1034
+ // Mutate the existing object in place so all template bindings pick up the new values.
1035
+ for (const key of Object.keys(updated)) {
1036
+ cfg[key] = updated[key];
1037
+ }
1038
+ });
1039
+ destroyRef.onDestroy(() => sub.unsubscribe());
1040
+ return cfg;
1041
+ },
1042
+ };
1043
+ }
1044
+
1045
+ class MnSectionDirective {
1046
+ /** Section name contributed by this DOM node to the section path */
1047
+ mnSection;
1048
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1049
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSectionDirective, isStandalone: true, selector: "[mn-section]", inputs: { mnSection: ["mn-section", "mnSection"] }, providers: [
1050
+ {
1051
+ provide: MN_SECTION_PATH,
1052
+ // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
1053
+ // and read the attribute value using Attribute so it's available at provider creation time.
1054
+ deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
1055
+ useFactory: (parentPath, attr) => {
1056
+ const parent = Array.isArray(parentPath) ? parentPath : [];
1057
+ const name = (attr ?? '').trim();
1058
+ return name ? [...parent, name] : [...parent];
1059
+ },
1060
+ },
1061
+ ], ngImport: i0 });
1062
+ }
1063
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
1064
+ type: Directive,
1065
+ args: [{
1066
+ selector: '[mn-section]',
1067
+ standalone: true,
1068
+ providers: [
1069
+ {
1070
+ provide: MN_SECTION_PATH,
1071
+ // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
1072
+ // and read the attribute value using Attribute so it's available at provider creation time.
1073
+ deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
1074
+ useFactory: (parentPath, attr) => {
1075
+ const parent = Array.isArray(parentPath) ? parentPath : [];
1076
+ const name = (attr ?? '').trim();
1077
+ return name ? [...parent, name] : [...parent];
1078
+ },
1079
+ },
1080
+ ],
1081
+ }]
1082
+ }], propDecorators: { mnSection: [{
1083
+ type: Input,
1084
+ args: ['mn-section']
1085
+ }] } });
1086
+
1087
+ class MnInstanceDirective {
1088
+ /** Instance id for targeting per-component instance overrides */
1089
+ mnInstance;
1090
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1091
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnInstanceDirective, isStandalone: true, selector: "[mn-instance]", inputs: { mnInstance: ["mn-instance", "mnInstance"] }, providers: [
1092
+ {
1093
+ provide: MN_INSTANCE_ID,
1094
+ // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
1095
+ deps: [new Attribute('mn-instance')],
1096
+ useFactory: (attr) => (attr ?? '').trim() || null,
1097
+ },
1098
+ ], ngImport: i0 });
1099
+ }
1100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
1101
+ type: Directive,
1102
+ args: [{
1103
+ selector: '[mn-instance]',
1104
+ standalone: true,
1105
+ providers: [
1106
+ {
1107
+ provide: MN_INSTANCE_ID,
1108
+ // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
1109
+ deps: [new Attribute('mn-instance')],
1110
+ useFactory: (attr) => (attr ?? '').trim() || null,
1111
+ },
1112
+ ],
1113
+ }]
1114
+ }], propDecorators: { mnInstance: [{
1115
+ type: Input,
1116
+ args: ['mn-instance']
1117
+ }] } });
1118
+
1119
+ /**
1120
+ * Provides an APP_INITIALIZER that configures the MnLanguageService and
1121
+ * preloads the requested locales during application bootstrap.
1122
+ *
1123
+ * Usage in app.config.ts:
1124
+ * ...provideMnLanguage({
1125
+ * urlPattern: 'assets/i18n/{locale}.json',
1126
+ * defaultLocale: 'en',
1127
+ * preload: ['en', 'nl'],
1128
+ * })
1129
+ */
1130
+ function provideMnLanguage(config) {
1131
+ return [
1132
+ {
1133
+ provide: APP_INITIALIZER,
1134
+ multi: true,
1135
+ useFactory: (svc) => async () => {
1136
+ if (config.debug) {
1137
+ svc.setDebug(true);
1138
+ }
1139
+ svc.configure(config.urlPattern);
1140
+ const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
1141
+ const localesToLoad = config.preload ?? [effectiveLocale];
1142
+ await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
1143
+ await svc.setLocale(effectiveLocale);
1144
+ },
1145
+ deps: [MnLanguageService],
1146
+ },
1147
+ ];
1148
+ }
1149
+
1150
+ /**
1151
+ * Pipe that translates a key via MnLanguageService.
1152
+ *
1153
+ * Usage in templates:
1154
+ * {{ 'form.email.label' | mnTranslate }}
1155
+ * {{ 'greeting' | mnTranslate:{ name: 'World' } }}
1156
+ *
1157
+ * Note: This pipe is impure so it re-evaluates when the locale changes.
1158
+ */
1159
+ class MnTranslatePipe {
1160
+ lang;
1161
+ constructor(lang) {
1162
+ this.lang = lang;
1163
+ }
1164
+ transform(key, params) {
1165
+ return this.lang.translate(key, params);
1166
+ }
1167
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [{ token: MnLanguageService }], target: i0.ɵɵFactoryTarget.Pipe });
1168
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
1169
+ }
1170
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
1171
+ type: Pipe,
1172
+ args: [{
1173
+ name: 'mnTranslate',
1174
+ standalone: true,
1175
+ pure: false,
1176
+ }]
1177
+ }], ctorParameters: () => [{ type: MnLanguageService }] });
1178
+
972
1179
  const MN_INPUT_FIELD_CONFIG = new InjectionToken('MN_INPUT_FIELD_CONFIG');
973
1180
  /**
974
1181
  * MnInputField Component
@@ -1067,10 +1274,8 @@ class MnInputField {
1067
1274
  const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
1068
1275
  this.uiConfig = this.configService.resolve('mn-input-field', this.sectionPath, instanceId);
1069
1276
  // Allow props to override uiConfig for label and placeholder
1070
- if (this.props.label) {
1277
+ if (this.props) {
1071
1278
  this.uiConfig = { ...this.uiConfig, label: this.props.label };
1072
- }
1073
- if (this.props.placeholder) {
1074
1279
  this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
1075
1280
  }
1076
1281
  }
@@ -1306,71 +1511,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1306
1511
  args: [{ required: true }]
1307
1512
  }] } });
1308
1513
 
1309
- /**
1310
- * Types for mn-lib configuration.
1311
- */
1312
-
1313
- /**
1314
- * Provides an APP_INITIALIZER that loads the mn-lib configuration from the given URL
1315
- * during application bootstrap. The consuming application is responsible for providing
1316
- * HttpClient (e.g., via HttpClientModule or provideHttpClient()).
1317
- */
1318
- function provideMnConfig(url, debugMode = false) {
1319
- return [
1320
- {
1321
- provide: APP_INITIALIZER,
1322
- multi: true,
1323
- useFactory: (svc) => () => svc.load(url, debugMode),
1324
- deps: [MnConfigService],
1325
- },
1326
- ];
1327
- }
1328
-
1329
- /**
1330
- * Helper to provide a resolved, typed component config via DI.
1331
- *
1332
- * Usage in a component/module providers:
1333
- * const MY_CFG = new InjectionToken<MyCfg>('MY_CFG');
1334
- * providers: [ provideMnComponentConfig(MY_CFG, 'my-component') ]
1335
- * Then in the component:
1336
- * readonly cfg = inject(MY_CFG)
1337
- *
1338
- * The returned config object is **reactive**: when the active locale changes,
1339
- * all translatable values are re-resolved in place so that templates using
1340
- * `cfg.someLabel` automatically reflect the new language on the next change-detection cycle.
1341
- */
1342
- function provideMnComponentConfig(token, componentName, initial) {
1343
- return {
1344
- provide: token,
1345
- deps: [
1346
- MnConfigService,
1347
- MnLanguageService,
1348
- DestroyRef,
1349
- [new Optional(), MN_SECTION_PATH],
1350
- [new Optional(), MN_INSTANCE_ID],
1351
- ],
1352
- useFactory: (svc, lang, destroyRef, sectionPath, instanceId) => {
1353
- const resolveConfig = () => {
1354
- const resolved = svc.resolve(componentName, sectionPath ?? [], instanceId ?? undefined);
1355
- return Object.assign({}, initial ?? {}, resolved);
1356
- };
1357
- // Create the initial config object that will be shared by reference.
1358
- const cfg = resolveConfig();
1359
- // Re-resolve translatable values whenever the locale changes.
1360
- // skip(1) because the current locale was already used for the initial resolve.
1361
- const sub = lang.locale$.subscribe(() => {
1362
- const updated = resolveConfig();
1363
- // Mutate the existing object in place so all template bindings pick up the new values.
1364
- for (const key of Object.keys(updated)) {
1365
- cfg[key] = updated[key];
1366
- }
1367
- });
1368
- destroyRef.onDestroy(() => sub.unsubscribe());
1369
- return cfg;
1370
- },
1371
- };
1372
- }
1373
-
1374
1514
  const MN_LIB_DUAL_HORIZONTAL_IMAGE = new InjectionToken('MN_LIB_DUAL_HORIZONTAL_IMAGE');
1375
1515
  class MnDualHorizontalImage {
1376
1516
  componentConfig = inject(MN_LIB_DUAL_HORIZONTAL_IMAGE);
@@ -1924,163 +2064,29 @@ const mnDatetimeVariants = tv({
1924
2064
  lg: 'p-4',
1925
2065
  },
1926
2066
  borderRadius: {
1927
- none: 'rounded-none',
1928
- xs: 'rounded-xs',
1929
- sm: 'rounded-sm',
1930
- md: 'rounded-md',
1931
- lg: 'rounded-lg',
1932
- xl: 'rounded-xl',
1933
- two_xl: 'rounded-2xl',
1934
- three_xl: 'rounded-3xl',
1935
- four_xl: 'rounded-4xl',
1936
- },
1937
- fullWidth: {
1938
- true: 'w-full',
1939
- },
1940
- hover: {
1941
- true: 'hover:cursor-pointer hover:bg-base-200 transition-colors duration-300 ease-in-out',
1942
- },
1943
- },
1944
- defaultVariants: {
1945
- size: 'md',
1946
- borderRadius: 'md',
1947
- hover: true,
1948
- },
1949
- });
1950
-
1951
- class MnSectionDirective {
1952
- /** Section name contributed by this DOM node to the section path */
1953
- mnSection;
1954
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1955
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSectionDirective, isStandalone: true, selector: "[mn-section]", inputs: { mnSection: ["mn-section", "mnSection"] }, providers: [
1956
- {
1957
- provide: MN_SECTION_PATH,
1958
- // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
1959
- // and read the attribute value using Attribute so it's available at provider creation time.
1960
- deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
1961
- useFactory: (parentPath, attr) => {
1962
- const parent = Array.isArray(parentPath) ? parentPath : [];
1963
- const name = (attr ?? '').trim();
1964
- return name ? [...parent, name] : [...parent];
1965
- },
1966
- },
1967
- ], ngImport: i0 });
1968
- }
1969
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
1970
- type: Directive,
1971
- args: [{
1972
- selector: '[mn-section]',
1973
- standalone: true,
1974
- providers: [
1975
- {
1976
- provide: MN_SECTION_PATH,
1977
- // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
1978
- // and read the attribute value using Attribute so it's available at provider creation time.
1979
- deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
1980
- useFactory: (parentPath, attr) => {
1981
- const parent = Array.isArray(parentPath) ? parentPath : [];
1982
- const name = (attr ?? '').trim();
1983
- return name ? [...parent, name] : [...parent];
1984
- },
1985
- },
1986
- ],
1987
- }]
1988
- }], propDecorators: { mnSection: [{
1989
- type: Input,
1990
- args: ['mn-section']
1991
- }] } });
1992
-
1993
- class MnInstanceDirective {
1994
- /** Instance id for targeting per-component instance overrides */
1995
- mnInstance;
1996
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1997
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnInstanceDirective, isStandalone: true, selector: "[mn-instance]", inputs: { mnInstance: ["mn-instance", "mnInstance"] }, providers: [
1998
- {
1999
- provide: MN_INSTANCE_ID,
2000
- // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
2001
- deps: [new Attribute('mn-instance')],
2002
- useFactory: (attr) => (attr ?? '').trim() || null,
2003
- },
2004
- ], ngImport: i0 });
2005
- }
2006
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
2007
- type: Directive,
2008
- args: [{
2009
- selector: '[mn-instance]',
2010
- standalone: true,
2011
- providers: [
2012
- {
2013
- provide: MN_INSTANCE_ID,
2014
- // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
2015
- deps: [new Attribute('mn-instance')],
2016
- useFactory: (attr) => (attr ?? '').trim() || null,
2017
- },
2018
- ],
2019
- }]
2020
- }], propDecorators: { mnInstance: [{
2021
- type: Input,
2022
- args: ['mn-instance']
2023
- }] } });
2024
-
2025
- /**
2026
- * Provides an APP_INITIALIZER that configures the MnLanguageService and
2027
- * preloads the requested locales during application bootstrap.
2028
- *
2029
- * Usage in app.config.ts:
2030
- * ...provideMnLanguage({
2031
- * urlPattern: 'assets/i18n/{locale}.json',
2032
- * defaultLocale: 'en',
2033
- * preload: ['en', 'nl'],
2034
- * })
2035
- */
2036
- function provideMnLanguage(config) {
2037
- return [
2038
- {
2039
- provide: APP_INITIALIZER,
2040
- multi: true,
2041
- useFactory: (svc) => async () => {
2042
- if (config.debug) {
2043
- svc.setDebug(true);
2044
- }
2045
- svc.configure(config.urlPattern);
2046
- const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
2047
- const localesToLoad = config.preload ?? [effectiveLocale];
2048
- await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
2049
- await svc.setLocale(effectiveLocale);
2050
- },
2051
- deps: [MnLanguageService],
2067
+ none: 'rounded-none',
2068
+ xs: 'rounded-xs',
2069
+ sm: 'rounded-sm',
2070
+ md: 'rounded-md',
2071
+ lg: 'rounded-lg',
2072
+ xl: 'rounded-xl',
2073
+ two_xl: 'rounded-2xl',
2074
+ three_xl: 'rounded-3xl',
2075
+ four_xl: 'rounded-4xl',
2052
2076
  },
2053
- ];
2054
- }
2055
-
2056
- /**
2057
- * Pipe that translates a key via MnLanguageService.
2058
- *
2059
- * Usage in templates:
2060
- * {{ 'form.email.label' | mnTranslate }}
2061
- * {{ 'greeting' | mnTranslate:{ name: 'World' } }}
2062
- *
2063
- * Note: This pipe is impure so it re-evaluates when the locale changes.
2064
- */
2065
- class MnTranslatePipe {
2066
- lang;
2067
- constructor(lang) {
2068
- this.lang = lang;
2069
- }
2070
- transform(key, params) {
2071
- return this.lang.translate(key, params);
2072
- }
2073
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [{ token: MnLanguageService }], target: i0.ɵɵFactoryTarget.Pipe });
2074
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
2075
- }
2076
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
2077
- type: Pipe,
2078
- args: [{
2079
- name: 'mnTranslate',
2080
- standalone: true,
2081
- pure: false,
2082
- }]
2083
- }], ctorParameters: () => [{ type: MnLanguageService }] });
2077
+ fullWidth: {
2078
+ true: 'w-full',
2079
+ },
2080
+ hover: {
2081
+ true: 'hover:cursor-pointer hover:bg-base-200 transition-colors duration-300 ease-in-out',
2082
+ },
2083
+ },
2084
+ defaultVariants: {
2085
+ size: 'md',
2086
+ borderRadius: 'md',
2087
+ hover: true,
2088
+ },
2089
+ });
2084
2090
 
2085
2091
  const MN_DATETIME_CONFIG = new InjectionToken('MN_DATETIME_CONFIG');
2086
2092
  class MnDatetime {
@@ -3576,45 +3582,25 @@ class MnTable {
3576
3582
  this.cdr.markForCheck();
3577
3583
  }
3578
3584
  }
3579
- ngOnInit() {
3580
- this.currentSort = this.dataSource.defaultSort ?? null;
3581
- this.pageSize = this.dataSource.pageSize ?? 10;
3582
- // Initialize all filterable columns with empty string to avoid undefined values.
3583
- for (const col of this.dataSource.columns) {
3584
- if (col.filterable) {
3585
- this.columnFilters[col.key] = '';
3586
- }
3587
- }
3588
- // Pre-select rows from initialSelectedIds if provided
3589
- if (this.dataSource.initialSelectedIds?.length) {
3590
- for (const id of this.dataSource.initialSelectedIds) {
3591
- this.selectedIds.add(id);
3592
- }
3593
- this.emitSelection();
3585
+ get showLoadMore() {
3586
+ const mode = this.dataSource.paginationMode ?? 'load-more';
3587
+ // Server-side load-more: check if there are more items to load.
3588
+ if (this.dataSource.onLoadMore) {
3589
+ const totalItems = this.dataSource.totalItems ?? 0;
3590
+ return mode === 'load-more' && this.filteredItems.length < totalItems;
3594
3591
  }
3595
- this.applyFilterAndSort(false);
3596
- // Skip the initial BehaviorSubject emission (already handled above)
3597
- // to avoid triggering markForCheck during the first change detection cycle.
3598
- this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
3599
- this.applyFilterAndSort(false);
3600
- this.cdr.markForCheck();
3601
- });
3602
- this.searchSubscription = this.searchSubject
3603
- .pipe(debounceTime(300))
3604
- .subscribe(value => {
3605
- this.searchValue = value;
3606
- this.applyFilterAndSort(true);
3607
- this.cdr.markForCheck();
3608
- });
3592
+ const strategy = this.dataSource.paginationStrategy;
3593
+ const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
3594
+ return mode === 'load-more' && hasMore;
3609
3595
  }
3610
3596
  ngOnDestroy() {
3611
3597
  this.dataSubscription?.unsubscribe();
3612
3598
  this.searchSubscription?.unsubscribe();
3613
3599
  }
3614
3600
  // ── Search ──
3615
- onSearch(searchString) {
3616
- this.currentPage = 1;
3617
- this.searchSubject.next(searchString);
3601
+ get isPaginated() {
3602
+ const mode = this.dataSource.paginationMode;
3603
+ return mode === 'paginated' || mode === 'client-side-pagination';
3618
3604
  }
3619
3605
  // ── Column Filters ──
3620
3606
  /** Whether any column has filtering enabled. */
@@ -3696,7 +3682,75 @@ class MnTable {
3696
3682
  this.rowClick.emit(row);
3697
3683
  }
3698
3684
  // ── Pagination ──
3685
+ /** Whether the table delegates pagination to the consumer (server-side). */
3686
+ get isServerPaginated() {
3687
+ const mode = this.dataSource.paginationMode ?? 'load-more';
3688
+ return mode === 'paginated' || mode === 'load-more';
3689
+ }
3690
+ /** Whether the table delegates search to the consumer (server-side). */
3691
+ get isServerSearched() {
3692
+ return !!this.dataSource.onServerSearch;
3693
+ }
3694
+ // ── Paginated Mode ──
3695
+ /** Total number of items, accounting for server-side pagination. */
3696
+ get totalItemCount() {
3697
+ if (this.isServerPaginated && this.dataSource.totalItems != null) {
3698
+ return this.dataSource.totalItems;
3699
+ }
3700
+ return this.filteredItems.length;
3701
+ }
3702
+ get totalPages() {
3703
+ return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
3704
+ }
3705
+ ngOnInit() {
3706
+ this.validateDataSource();
3707
+ this.currentSort = this.dataSource.defaultSort ?? null;
3708
+ this.pageSize = this.dataSource.pageSize ?? 10;
3709
+ // Initialize all filterable columns with empty string to avoid undefined values.
3710
+ for (const col of this.dataSource.columns) {
3711
+ if (col.filterable) {
3712
+ this.columnFilters[col.key] = '';
3713
+ }
3714
+ }
3715
+ // Pre-select rows from initialSelectedIds if provided
3716
+ if (this.dataSource.initialSelectedIds?.length) {
3717
+ for (const id of this.dataSource.initialSelectedIds) {
3718
+ this.selectedIds.add(id);
3719
+ }
3720
+ this.emitSelection();
3721
+ }
3722
+ this.applyFilterAndSort(false);
3723
+ // Skip the initial BehaviorSubject emission (already handled above)
3724
+ // to avoid triggering markForCheck during the first change detection cycle.
3725
+ this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
3726
+ this.applyFilterAndSort(false);
3727
+ this.cdr.markForCheck();
3728
+ });
3729
+ this.searchSubscription = this.searchSubject
3730
+ .pipe(debounceTime(300))
3731
+ .subscribe(value => {
3732
+ this.searchValue = value;
3733
+ this.applyFilterAndSort(true);
3734
+ this.cdr.markForCheck();
3735
+ });
3736
+ }
3737
+ onSearch(searchString) {
3738
+ this.currentPage = 1;
3739
+ if (this.isServerSearched) {
3740
+ this.searchValue = searchString;
3741
+ this.dataSource.onServerSearch(searchString);
3742
+ this.cdr.markForCheck();
3743
+ }
3744
+ else {
3745
+ this.searchSubject.next(searchString);
3746
+ }
3747
+ }
3699
3748
  loadMoreRows() {
3749
+ // Server-side infinite scroll: delegate to consumer callback.
3750
+ if (this.dataSource.onLoadMore) {
3751
+ this.dataSource.onLoadMore();
3752
+ return;
3753
+ }
3700
3754
  if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
3701
3755
  return;
3702
3756
  this.loadingMoreRows = true;
@@ -3707,19 +3761,6 @@ class MnTable {
3707
3761
  .then(rows => this.processLoadedRows(rows))
3708
3762
  .catch(() => this.loadingMoreRows = false);
3709
3763
  }
3710
- get showLoadMore() {
3711
- const mode = this.dataSource.paginationMode ?? 'load-more';
3712
- const strategy = this.dataSource.paginationStrategy;
3713
- const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
3714
- return mode === 'load-more' && hasMore;
3715
- }
3716
- // ── Paginated Mode ──
3717
- get isPaginated() {
3718
- return this.dataSource.paginationMode === 'paginated';
3719
- }
3720
- get totalPages() {
3721
- return Math.max(1, Math.ceil(this.filteredItems.length / this.pageSize));
3722
- }
3723
3764
  get resolvedPageSizeOptions() {
3724
3765
  return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
3725
3766
  }
@@ -3727,15 +3768,24 @@ class MnTable {
3727
3768
  if (page < 1 || page > this.totalPages)
3728
3769
  return;
3729
3770
  this.currentPage = page;
3730
- this.applyPagination();
3771
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
3772
+ this.applyPagination();
3773
+ }
3774
+ else {
3775
+ this.dataSource.onPageChange?.(page);
3776
+ }
3731
3777
  this.cdr.markForCheck();
3732
3778
  }
3733
3779
  onPageSizeChange(newSize) {
3734
3780
  this.pageSize = newSize;
3735
3781
  this.currentPage = 1;
3736
- this.applyPagination();
3782
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
3783
+ this.applyPagination();
3784
+ }
3785
+ else {
3786
+ this.dataSource.onPageSizeChange?.(newSize);
3787
+ }
3737
3788
  this.cdr.markForCheck();
3738
- this.dataSource.onPageSizeChange?.(newSize);
3739
3789
  }
3740
3790
  get visiblePages() {
3741
3791
  const total = this.totalPages;
@@ -3754,14 +3804,12 @@ class MnTable {
3754
3804
  return pages;
3755
3805
  }
3756
3806
  applyPagination() {
3757
- if (this.isPaginated) {
3758
- if (this.currentPage > this.totalPages) {
3759
- this.currentPage = this.totalPages;
3760
- }
3807
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
3761
3808
  const start = (this.currentPage - 1) * this.pageSize;
3762
3809
  this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
3763
3810
  }
3764
3811
  else {
3812
+ // Server always provides the correct page/slice — no client-side slicing.
3765
3813
  this.paginatedItems = this.filteredItems;
3766
3814
  }
3767
3815
  }
@@ -3797,8 +3845,8 @@ class MnTable {
3797
3845
  // ── Private ──
3798
3846
  applyFilterAndSort(searchForItems) {
3799
3847
  let items = this.dataSource.dataRows.value;
3800
- // Global search filter
3801
- if (this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
3848
+ // Skip client-side search filtering when server handles it.
3849
+ if (!this.isServerSearched && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
3802
3850
  const term = this.searchValue.toLowerCase();
3803
3851
  items = items.filter(row => this.dataSource.isInSearch(row, term));
3804
3852
  }
@@ -3864,17 +3912,33 @@ class MnTable {
3864
3912
  this.loadingMoreRows = false;
3865
3913
  this.applyFilterAndSort(false);
3866
3914
  }
3915
+ validateDataSource() {
3916
+ const mode = this.dataSource.paginationMode;
3917
+ if (mode === 'paginated') {
3918
+ if (!this.dataSource.onPageChange) {
3919
+ throw new Error(`[MnTable] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
3920
+ }
3921
+ if (this.dataSource.totalItems == null) {
3922
+ throw new Error(`[MnTable] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
3923
+ }
3924
+ }
3925
+ if (mode === 'load-more' || mode === 'infinite-scroll') {
3926
+ if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
3927
+ throw new Error(`[MnTable] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
3928
+ }
3929
+ }
3930
+ }
3867
3931
  emitSelection() {
3868
3932
  const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
3869
3933
  this.dataSource.selectedRows?.next(rows);
3870
3934
  this.selectionChange.emit(rows);
3871
3935
  }
3872
3936
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
3873
- 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: 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-100 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-primary w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search table\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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-1\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <input\n type=\"text\"\n class=\"input input-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content placeholder-base-content/50 mb-2 focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer\"\n [placeholder]=\"column.filterPlaceholder ?? ''\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n [attr.autocomplete]=\"column.filterAutocomplete ?? null\"\n [attr.maxlength]=\"column.filterMaxLength ?? null\"\n (input)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n />\n } @else if (column.filterType === 'select') {\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer mb-2\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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) {\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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\u2013{{ currentPage * pageSize > filteredItems.length ? filteredItems.length : currentPage * pageSize }} of {{ filteredItems.length }}</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: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3937
+ 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: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-sm text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer \"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3874
3938
  }
3875
3939
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
3876
3940
  type: Component,
3877
- args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnHiddenBelowDirective], 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-100 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-primary w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search table\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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-1\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <input\n type=\"text\"\n class=\"input input-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content placeholder-base-content/50 mb-2 focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer\"\n [placeholder]=\"column.filterPlaceholder ?? ''\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n [attr.autocomplete]=\"column.filterAutocomplete ?? null\"\n [attr.maxlength]=\"column.filterMaxLength ?? null\"\n (input)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n />\n } @else if (column.filterType === 'select') {\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer mb-2\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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) {\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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\u2013{{ currentPage * pageSize > filteredItems.length ? filteredItems.length : currentPage * pageSize }} of {{ filteredItems.length }}</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" }]
3941
+ args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnHiddenBelowDirective, MnInputField, 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 <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-sm text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer \"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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" }]
3878
3942
  }], propDecorators: { dataSource: [{
3879
3943
  type: Input
3880
3944
  }], sortChange: [{
@@ -5480,36 +5544,25 @@ class MnList {
5480
5544
  this.cdr.markForCheck();
5481
5545
  }
5482
5546
  }
5483
- ngOnInit() {
5484
- this.pageSize = this.dataSource.pageSize ?? 10;
5485
- // Pre-select items from initialSelectedIds if provided
5486
- if (this.dataSource.initialSelectedIds?.length) {
5487
- for (const id of this.dataSource.initialSelectedIds) {
5488
- this.selectedIds.add(id);
5489
- }
5490
- this.emitSelection();
5547
+ get showLoadMore() {
5548
+ const mode = this.dataSource.paginationMode ?? 'load-more';
5549
+ // Server-side load-more: check if there are more items to load.
5550
+ if (this.dataSource.onLoadMore) {
5551
+ const totalItems = this.dataSource.totalItems ?? 0;
5552
+ return mode === 'load-more' && this.filteredItems.length < totalItems;
5491
5553
  }
5492
- this.applyFilter(false);
5493
- this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
5494
- this.applyFilter(false);
5495
- this.cdr.markForCheck();
5496
- });
5497
- this.searchSubscription = this.searchSubject
5498
- .pipe(debounceTime(300))
5499
- .subscribe(value => {
5500
- this.searchValue = value;
5501
- this.applyFilter(true);
5502
- this.cdr.markForCheck();
5503
- });
5554
+ const strategy = this.dataSource.paginationStrategy;
5555
+ const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
5556
+ return mode === 'load-more' && hasMore;
5504
5557
  }
5505
5558
  ngOnDestroy() {
5506
5559
  this.dataSubscription?.unsubscribe();
5507
5560
  this.searchSubscription?.unsubscribe();
5508
5561
  }
5509
5562
  // ── Search ──
5510
- onSearch(searchString) {
5511
- this.currentPage = 1;
5512
- this.searchSubject.next(searchString);
5563
+ get isPaginated() {
5564
+ const mode = this.dataSource.paginationMode;
5565
+ return mode === 'paginated' || mode === 'client-side-pagination';
5513
5566
  }
5514
5567
  // ── Selection ──
5515
5568
  isSelected(item) {
@@ -5556,7 +5609,62 @@ class MnList {
5556
5609
  this.itemClick.emit(item);
5557
5610
  }
5558
5611
  // ── Pagination ──
5612
+ /** Whether the list is in server-side paginated mode (always true when paginated or load-more). */
5613
+ get isServerPaginated() {
5614
+ const mode = this.dataSource.paginationMode ?? 'load-more';
5615
+ return mode === 'paginated' || mode === 'load-more';
5616
+ }
5617
+ /** Total number of items, accounting for server-side pagination. */
5618
+ get totalItemCount() {
5619
+ if (this.isServerPaginated && this.dataSource.totalItems != null) {
5620
+ return this.dataSource.totalItems;
5621
+ }
5622
+ return this.filteredItems.length;
5623
+ }
5624
+ // ── Paginated Mode ──
5625
+ get totalPages() {
5626
+ return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
5627
+ }
5628
+ ngOnInit() {
5629
+ this.validateDataSource();
5630
+ this.pageSize = this.dataSource.pageSize ?? 10;
5631
+ // Pre-select items from initialSelectedIds if provided
5632
+ if (this.dataSource.initialSelectedIds?.length) {
5633
+ for (const id of this.dataSource.initialSelectedIds) {
5634
+ this.selectedIds.add(id);
5635
+ }
5636
+ this.emitSelection();
5637
+ }
5638
+ this.applyFilter(false);
5639
+ this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
5640
+ this.applyFilter(false);
5641
+ this.cdr.markForCheck();
5642
+ });
5643
+ this.searchSubscription = this.searchSubject
5644
+ .pipe(debounceTime(300))
5645
+ .subscribe(value => {
5646
+ this.searchValue = value;
5647
+ this.applyFilter(true);
5648
+ this.cdr.markForCheck();
5649
+ });
5650
+ }
5651
+ onSearch(searchString) {
5652
+ this.currentPage = 1;
5653
+ if (this.isServerPaginated) {
5654
+ this.searchValue = searchString;
5655
+ this.dataSource.onServerSearch?.(searchString);
5656
+ this.cdr.markForCheck();
5657
+ }
5658
+ else {
5659
+ this.searchSubject.next(searchString);
5660
+ }
5661
+ }
5559
5662
  loadMoreRows() {
5663
+ // Server-side infinite scroll: delegate to consumer callback.
5664
+ if (this.dataSource.onLoadMore) {
5665
+ this.dataSource.onLoadMore();
5666
+ return;
5667
+ }
5560
5668
  if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
5561
5669
  return;
5562
5670
  this.loadingMoreRows = true;
@@ -5567,19 +5675,6 @@ class MnList {
5567
5675
  .then(rows => this.processLoadedRows(rows))
5568
5676
  .catch(() => this.loadingMoreRows = false);
5569
5677
  }
5570
- get showLoadMore() {
5571
- const mode = this.dataSource.paginationMode ?? 'load-more';
5572
- const strategy = this.dataSource.paginationStrategy;
5573
- const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
5574
- return mode === 'load-more' && hasMore;
5575
- }
5576
- // ── Paginated Mode ──
5577
- get isPaginated() {
5578
- return this.dataSource.paginationMode === 'paginated';
5579
- }
5580
- get totalPages() {
5581
- return Math.max(1, Math.ceil(this.filteredItems.length / this.pageSize));
5582
- }
5583
5678
  get resolvedPageSizeOptions() {
5584
5679
  return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
5585
5680
  }
@@ -5587,13 +5682,23 @@ class MnList {
5587
5682
  if (page < 1 || page > this.totalPages)
5588
5683
  return;
5589
5684
  this.currentPage = page;
5590
- this.applyPagination();
5685
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
5686
+ this.applyPagination();
5687
+ }
5688
+ else {
5689
+ this.dataSource.onPageChange?.(page);
5690
+ }
5591
5691
  this.cdr.markForCheck();
5592
5692
  }
5593
5693
  onPageSizeChange(newSize) {
5594
5694
  this.pageSize = newSize;
5595
5695
  this.currentPage = 1;
5596
- this.applyPagination();
5696
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
5697
+ this.applyPagination();
5698
+ }
5699
+ else {
5700
+ this.dataSource.onPageSizeChange?.(newSize);
5701
+ }
5597
5702
  this.cdr.markForCheck();
5598
5703
  }
5599
5704
  get visiblePages() {
@@ -5621,21 +5726,19 @@ class MnList {
5621
5726
  }
5622
5727
  // ── Private ──
5623
5728
  applyPagination() {
5624
- if (this.isPaginated) {
5625
- if (this.currentPage > this.totalPages) {
5626
- this.currentPage = this.totalPages;
5627
- }
5729
+ if (this.dataSource.paginationMode === 'client-side-pagination') {
5628
5730
  const start = (this.currentPage - 1) * this.pageSize;
5629
5731
  this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
5630
5732
  }
5631
5733
  else {
5734
+ // Server always provides the correct page/slice — no client-side slicing.
5632
5735
  this.paginatedItems = this.filteredItems;
5633
5736
  }
5634
5737
  }
5635
5738
  applyFilter(searchForItems) {
5636
5739
  let items = this.dataSource.dataRows.value;
5637
- // Global search filter
5638
- if (this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
5740
+ // Skip client-side search filtering when server handles it.
5741
+ if (!this.isServerPaginated && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
5639
5742
  const term = this.searchValue.toLowerCase();
5640
5743
  items = items.filter(row => this.dataSource.isInSearch(row, term));
5641
5744
  }
@@ -5651,17 +5754,33 @@ class MnList {
5651
5754
  this.loadingMoreRows = false;
5652
5755
  this.applyFilter(false);
5653
5756
  }
5757
+ validateDataSource() {
5758
+ const mode = this.dataSource.paginationMode;
5759
+ if (mode === 'paginated') {
5760
+ if (!this.dataSource.onPageChange) {
5761
+ throw new Error(`[MnList] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
5762
+ }
5763
+ if (this.dataSource.totalItems == null) {
5764
+ throw new Error(`[MnList] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
5765
+ }
5766
+ }
5767
+ if (mode === 'load-more' || mode === 'infinite-scroll') {
5768
+ if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
5769
+ throw new Error(`[MnList] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
5770
+ }
5771
+ }
5772
+ }
5654
5773
  emitSelection() {
5655
5774
  const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
5656
5775
  this.dataSource.selectedRows?.next(rows);
5657
5776
  this.selectionChange.emit(rows);
5658
5777
  }
5659
5778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: [], target: i0.ɵɵFactoryTarget.Component });
5660
- 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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"flex-shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\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) {\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Items per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\u2013{{ currentPage * pageSize > filteredItems.length ? filteredItems.length : currentPage * pageSize }} of {{ filteredItems.length }}</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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5779
+ 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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Items per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5661
5780
  }
5662
5781
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
5663
5782
  type: Component,
5664
- args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton], 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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"flex-shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\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) {\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Items per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\u2013{{ currentPage * pageSize > filteredItems.length ? filteredItems.length : currentPage * pageSize }} of {{ filteredItems.length }}</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" }]
5783
+ args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton], 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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Items per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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" }]
5665
5784
  }], propDecorators: { dataSource: [{
5666
5785
  type: Input
5667
5786
  }], selectionChange: [{