mn-angular-lib 1.0.2 → 1.0.3

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);
@@ -1948,140 +2088,6 @@ const mnDatetimeVariants = tv({
1948
2088
  },
1949
2089
  });
1950
2090
 
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],
2052
- },
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 }] });
2084
-
2085
2091
  const MN_DATETIME_CONFIG = new InjectionToken('MN_DATETIME_CONFIG');
2086
2092
  class MnDatetime {
2087
2093
  ngControl;
@@ -3870,11 +3876,11 @@ class MnTable {
3870
3876
  this.selectionChange.emit(rows);
3871
3877
  }
3872
3878
  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 });
3879
+ 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) {\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"] }, { 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
3880
  }
3875
3881
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
3876
3882
  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" }]
3883
+ 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) {\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" }]
3878
3884
  }], propDecorators: { dataSource: [{
3879
3885
  type: Input
3880
3886
  }], sortChange: [{