mn-angular-lib 0.0.42 → 0.0.44

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,13 +1,14 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, Self, APP_INITIALIZER, SkipSelf, Attribute, Directive } from '@angular/core';
3
- import { BehaviorSubject, firstValueFrom } from 'rxjs';
2
+ import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, Self, APP_INITIALIZER, ElementRef, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewChildren, ViewContainerRef, ViewChild, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
3
+ export { TemplateRef, Type } from '@angular/core';
4
+ import { BehaviorSubject, firstValueFrom, Subject, debounceTime } from 'rxjs';
4
5
  import * as i1 from '@angular/common';
5
- import { CommonModule, NgClass, NgOptimizedImage } from '@angular/common';
6
+ import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
6
7
  import { tv } from 'tailwind-variants';
7
8
  import * as i1$2 from '@angular/forms';
8
- import { Validators } from '@angular/forms';
9
- import JSON5 from 'json5';
9
+ import { Validators, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
10
10
  import * as i1$1 from '@angular/common/http';
11
+ import JSON5 from 'json5';
11
12
 
12
13
  // projects/mn-angular-lib/src/lib/mn-mn-alert/mn-mn-alert.tokens.ts
13
14
  const MN_ALERT_CONFIG = new InjectionToken('MN_ALERT_CONFIG');
@@ -293,13 +294,13 @@ const mnButtonVariants = tv({
293
294
  { variant: 'fill', color: 'warning', class: 'bg-amber-500 text-black hover:bg-amber-600' },
294
295
  { variant: 'fill', color: 'success', class: 'bg-green-600 text-white hover:bg-green-700' },
295
296
  // Outline
296
- { variant: 'outline', color: 'primary', class: 'border-brand-600 text-blue-600 hover:bg-brand-100' },
297
+ { variant: 'outline', color: 'primary', class: 'border-brand-500 text-brand-500 hover:bg-brand-100' },
297
298
  { variant: 'outline', color: 'secondary', class: 'border-gray-600 text-gray-700 hover:bg-gray-100' },
298
299
  { variant: 'outline', color: 'danger', class: 'border-red-600 text-red-600 hover:bg-red-100' },
299
300
  { variant: 'outline', color: 'warning', class: 'border-amber-500 text-amber-600 hover:bg-amber-100' },
300
301
  { variant: 'outline', color: 'success', class: 'border-green-600 text-green-600 hover:bg-green-100' },
301
302
  // Text
302
- { variant: 'text', color: 'primary', class: 'text-brand-600 hover:bg-brand-100' },
303
+ { variant: 'text', color: 'primary', class: 'text-brand-500 hover:bg-brand-100' },
303
304
  { variant: 'text', color: 'secondary', class: 'text-gray-700 hover:bg-gray-100' },
304
305
  { variant: 'text', color: 'danger', class: 'text-red-600 hover:bg-red-100' },
305
306
  { variant: 'text', color: 'warning', class: 'text-amber-600 hover:bg-amber-100' },
@@ -546,6 +547,106 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
546
547
  args: [{ required: true }]
547
548
  }] } });
548
549
 
550
+ class MnLanguageService {
551
+ http;
552
+ _translations = {};
553
+ _locale$ = new BehaviorSubject('en');
554
+ _urlPattern = null;
555
+ /** Observable of the current active locale. */
556
+ locale$ = this._locale$.asObservable();
557
+ constructor(http) {
558
+ this.http = http;
559
+ }
560
+ /** Current active locale. */
561
+ get locale() {
562
+ return this._locale$.value;
563
+ }
564
+ /**
565
+ * Configure the URL pattern used to fetch translation files.
566
+ * Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
567
+ */
568
+ configure(urlPattern) {
569
+ this._urlPattern = urlPattern;
570
+ }
571
+ /**
572
+ * Load translations for a locale from the configured URL pattern.
573
+ * If translations are already loaded for this locale, this is a no-op.
574
+ */
575
+ async loadLocale(locale) {
576
+ if (this._translations[locale])
577
+ return;
578
+ if (!this._urlPattern) {
579
+ console.warn(`[MnLanguage] No URL pattern configured. Call configure() or use provideMnLanguage().`);
580
+ return;
581
+ }
582
+ const url = this._urlPattern.replace('{locale}', locale);
583
+ try {
584
+ const map = await firstValueFrom(this.http.get(url));
585
+ this._translations[locale] = map ?? {};
586
+ }
587
+ catch (err) {
588
+ console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
589
+ this._translations[locale] = {};
590
+ }
591
+ }
592
+ /**
593
+ * Switch the active locale. Loads translations if not yet loaded.
594
+ */
595
+ async setLocale(locale) {
596
+ await this.loadLocale(locale);
597
+ this._locale$.next(locale);
598
+ }
599
+ /**
600
+ * Register translations for a locale directly from code (no HTTP needed).
601
+ */
602
+ registerTranslations(locale, translations) {
603
+ this._translations[locale] = {
604
+ ...(this._translations[locale] ?? {}),
605
+ ...translations,
606
+ };
607
+ }
608
+ /**
609
+ * Translate a key using the current locale, with optional parameter interpolation.
610
+ * Falls back to the key itself if no translation is found.
611
+ *
612
+ * Interpolation replaces `{{paramName}}` with the provided value.
613
+ */
614
+ translate(key, params) {
615
+ const map = this._translations[this.locale] ?? {};
616
+ let value = map[key];
617
+ if (value === undefined) {
618
+ return key;
619
+ }
620
+ if (params) {
621
+ for (const [paramKey, paramValue] of Object.entries(params)) {
622
+ value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), String(paramValue));
623
+ }
624
+ }
625
+ return value;
626
+ }
627
+ /**
628
+ * Shorthand alias for `translate`.
629
+ */
630
+ t(key, params) {
631
+ return this.translate(key, params);
632
+ }
633
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
634
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, providedIn: 'root' });
635
+ }
636
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, decorators: [{
637
+ type: Injectable,
638
+ args: [{ providedIn: 'root' }]
639
+ }], ctorParameters: () => [{ type: i1$1.HttpClient }] });
640
+
641
+ /**
642
+ * Type guard: checks whether a value is a translatable marker object.
643
+ */
644
+ function isTranslatable(value) {
645
+ return (typeof value === 'object' &&
646
+ value !== null &&
647
+ typeof value.$translate === 'string');
648
+ }
649
+
549
650
  function isPlainObject(value) {
550
651
  return (typeof value === 'object' &&
551
652
  value !== null &&
@@ -555,6 +656,7 @@ class MnConfigService {
555
656
  http;
556
657
  _config = null;
557
658
  _debugMode = false;
659
+ lang = inject(MnLanguageService);
558
660
  constructor(http) {
559
661
  this.http = http;
560
662
  }
@@ -585,6 +687,17 @@ class MnConfigService {
585
687
  const defaults = isPlainObject(cfg.defaults) ? cfg.defaults : {};
586
688
  const overrides = isPlainObject(cfg.overrides) ? cfg.overrides : cfg.overrides ?? {};
587
689
  this._config = { defaults, overrides };
690
+ // Bootstrap language service from config if a "language" section is present.
691
+ // This avoids circular dependency: config reads raw language settings and
692
+ // pushes them into the language service (language service never imports config).
693
+ const langCfg = cfg.language;
694
+ if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
695
+ const lc = langCfg;
696
+ this.lang.configure(lc.urlPattern);
697
+ const localesToLoad = lc.preload ?? [lc.defaultLocale];
698
+ await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
699
+ await this.lang.setLocale(lc.defaultLocale);
700
+ }
588
701
  }
589
702
  /**
590
703
  * Resolve a configuration object for a component, optionally scoped to a section path
@@ -614,7 +727,7 @@ class MnConfigService {
614
727
  resolved,
615
728
  });
616
729
  }
617
- return resolved;
730
+ return this.resolveTranslatables(resolved);
618
731
  }
619
732
  /**
620
733
  * Walk the overrides nested object using the provided section path and return the leaf node.
@@ -631,6 +744,25 @@ class MnConfigService {
631
744
  }
632
745
  return node;
633
746
  }
747
+ /**
748
+ * Recursively walk a resolved config object and replace any `{ $translate: "key" }` markers
749
+ * with their translated values from MnLanguageService.
750
+ */
751
+ resolveTranslatables(obj) {
752
+ const out = {};
753
+ for (const [key, value] of Object.entries(obj)) {
754
+ if (isTranslatable(value)) {
755
+ out[key] = this.lang.translate(value.$translate, value.params);
756
+ }
757
+ else if (isPlainObject(value)) {
758
+ out[key] = this.resolveTranslatables(value);
759
+ }
760
+ else {
761
+ out[key] = value;
762
+ }
763
+ }
764
+ return out;
765
+ }
634
766
  /**
635
767
  * Deep merge two plain-object trees. Arrays and non-plain values are replaced by the patch.
636
768
  * Does not mutate inputs; returns a new object.
@@ -752,6 +884,13 @@ class MnInputField {
752
884
  resolveConfig() {
753
885
  const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
754
886
  this.uiConfig = this.configService.resolve('mn-input-field', this.sectionPath, instanceId);
887
+ // Allow props to override uiConfig for label and placeholder
888
+ if (this.props.label) {
889
+ this.uiConfig = { ...this.uiConfig, label: this.props.label };
890
+ }
891
+ if (this.props.placeholder) {
892
+ this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
893
+ }
755
894
  }
756
895
  /**
757
896
  * Gets the appropriate adapter based on the input type.
@@ -1391,80 +1530,2824 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1391
1530
  args: [{ required: true }]
1392
1531
  }] } });
1393
1532
 
1394
- class MnSectionDirective {
1395
- /** Section name contributed by this DOM node to the section path */
1396
- mnSection;
1397
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1398
- 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: [
1399
- {
1400
- provide: MN_SECTION_PATH,
1401
- // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
1402
- // and read the attribute value using Attribute so it's available at provider creation time.
1403
- deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
1404
- useFactory: (parentPath, attr) => {
1405
- const parent = Array.isArray(parentPath) ? parentPath : [];
1406
- const name = (attr ?? '').trim();
1407
- return name ? [...parent, name] : [...parent];
1408
- },
1409
- },
1410
- ], ngImport: i0 });
1411
- }
1412
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
1413
- type: Directive,
1414
- args: [{
1415
- selector: '[mn-section]',
1416
- standalone: true,
1417
- providers: [
1418
- {
1419
- provide: MN_SECTION_PATH,
1420
- // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
1421
- // and read the attribute value using Attribute so it's available at provider creation time.
1422
- deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
1423
- useFactory: (parentPath, attr) => {
1424
- const parent = Array.isArray(parentPath) ? parentPath : [];
1425
- const name = (attr ?? '').trim();
1426
- return name ? [...parent, name] : [...parent];
1427
- },
1428
- },
1429
- ],
1430
- }]
1431
- }], propDecorators: { mnSection: [{
1432
- type: Input,
1433
- args: ['mn-section']
1434
- }] } });
1533
+ const mnCheckboxVariants = tv({
1534
+ base: 'accent-brand-500 cursor-pointer',
1535
+ variants: {
1536
+ size: {
1537
+ sm: 'w-4 h-4',
1538
+ md: 'w-5 h-5',
1539
+ lg: 'w-6 h-6',
1540
+ },
1541
+ borderRadius: {
1542
+ none: 'rounded-none',
1543
+ xs: 'rounded-xs',
1544
+ sm: 'rounded-sm',
1545
+ md: 'rounded-md',
1546
+ lg: 'rounded-lg',
1547
+ },
1548
+ },
1549
+ defaultVariants: {
1550
+ size: 'md',
1551
+ borderRadius: 'sm',
1552
+ },
1553
+ });
1435
1554
 
1436
- class MnInstanceDirective {
1437
- /** Instance id for targeting per-component instance overrides */
1438
- mnInstance;
1439
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1440
- 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: [
1441
- {
1442
- provide: MN_INSTANCE_ID,
1443
- // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
1444
- deps: [new Attribute('mn-instance')],
1445
- useFactory: (attr) => (attr ?? '').trim() || null,
1446
- },
1447
- ], ngImport: i0 });
1555
+ const MN_CHECKBOX_CONFIG = new InjectionToken('MN_CHECKBOX_CONFIG');
1556
+ class MnCheckbox {
1557
+ ngControl;
1558
+ uiConfig = {};
1559
+ props;
1560
+ configService = inject(MnConfigService);
1561
+ sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
1562
+ explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
1563
+ value = false;
1564
+ isDisabled = false;
1565
+ onChange = () => { };
1566
+ onTouched = () => { };
1567
+ builtInErrorMessages = {
1568
+ required: 'This field is required',
1569
+ };
1570
+ constructor(ngControl) {
1571
+ this.ngControl = ngControl;
1572
+ if (this.ngControl)
1573
+ this.ngControl.valueAccessor = this;
1574
+ }
1575
+ ngOnInit() {
1576
+ this.resolveConfig();
1577
+ }
1578
+ resolveConfig() {
1579
+ const instanceId = this.explicitInstanceId || `mn-checkbox-${this.props.id}`;
1580
+ this.uiConfig = this.configService.resolve('mn-checkbox', this.sectionPath, instanceId);
1581
+ if (this.props.label) {
1582
+ this.uiConfig = { ...this.uiConfig, label: this.props.label };
1583
+ }
1584
+ }
1585
+ // ========== ControlValueAccessor Implementation ==========
1586
+ writeValue(val) {
1587
+ this.value = !!val;
1588
+ }
1589
+ registerOnChange(fn) {
1590
+ this.onChange = fn;
1591
+ }
1592
+ registerOnTouched(fn) {
1593
+ this.onTouched = fn;
1594
+ }
1595
+ setDisabledState(isDisabled) {
1596
+ this.isDisabled = isDisabled;
1597
+ }
1598
+ // ========== Event Handlers ==========
1599
+ handleChange(checked) {
1600
+ this.value = checked;
1601
+ this.onChange(checked);
1602
+ }
1603
+ handleBlur() {
1604
+ this.onTouched();
1605
+ }
1606
+ // ========== Error Handling ==========
1607
+ get control() {
1608
+ return this.ngControl?.control ?? null;
1609
+ }
1610
+ get showError() {
1611
+ const c = this.control;
1612
+ return !!c && c.invalid && (c.touched || c.dirty);
1613
+ }
1614
+ pickErrorKey(errors) {
1615
+ if (this.props.errorPriority) {
1616
+ for (const key of this.props.errorPriority) {
1617
+ if (errors[key] !== undefined) {
1618
+ return key;
1619
+ }
1620
+ }
1621
+ }
1622
+ return Object.keys(errors)[0];
1623
+ }
1624
+ isRequired() {
1625
+ if (!this.control)
1626
+ return false;
1627
+ return this.control.hasValidator(Validators.required);
1628
+ }
1629
+ resolveErrorMessageForKey(errorKey, errors) {
1630
+ const errorArgs = errors[errorKey];
1631
+ const customMsg = this.props.errorMessages?.[errorKey];
1632
+ const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
1633
+ const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
1634
+ const fallbackMsg = this.props.defaultErrorMessage;
1635
+ const msgDef = customMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
1636
+ if (typeof msgDef === 'function') {
1637
+ return msgDef(errorArgs, errors);
1638
+ }
1639
+ return msgDef;
1640
+ }
1641
+ get errorMessages() {
1642
+ const errors = this.control?.errors;
1643
+ if (!errors)
1644
+ return [];
1645
+ return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
1646
+ }
1647
+ get errorMessage() {
1648
+ const errors = this.control?.errors;
1649
+ if (!errors)
1650
+ return null;
1651
+ const errorKey = this.pickErrorKey(errors);
1652
+ return this.resolveErrorMessageForKey(errorKey, errors);
1653
+ }
1654
+ // ========== Resolved Properties ==========
1655
+ get resolvedId() {
1656
+ return this.props.id;
1657
+ }
1658
+ get resolvedName() {
1659
+ return this.props?.name ?? null;
1660
+ }
1661
+ get checkboxClasses() {
1662
+ return mnCheckboxVariants({
1663
+ size: this.props.size,
1664
+ borderRadius: this.props.borderRadius,
1665
+ });
1666
+ }
1667
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCheckbox, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
1668
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCheckbox, isStandalone: true, selector: "mn-lib-checkbox", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col\">\n <label class=\"flex flex-row items-center gap-x-2 cursor-pointer\" [attr.for]=\"resolvedId\">\n <input\n type=\"checkbox\"\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [checked]=\"value\"\n [ngClass]=\"checkboxClasses\"\n (change)=\"handleChange($any($event.target).checked)\"\n (blur)=\"handleBlur()\"\n />\n @if (uiConfig.label || props.label) {\n <span class=\"text-sm text-gray-700 select-none\">{{ uiConfig.label || props.label }}</span>\n }\n </label>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
1448
1669
  }
1449
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
1450
- type: Directive,
1451
- args: [{
1452
- selector: '[mn-instance]',
1453
- standalone: true,
1454
- providers: [
1455
- {
1456
- provide: MN_INSTANCE_ID,
1457
- // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
1458
- deps: [new Attribute('mn-instance')],
1459
- useFactory: (attr) => (attr ?? '').trim() || null,
1460
- },
1461
- ],
1462
- }]
1463
- }], propDecorators: { mnInstance: [{
1670
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCheckbox, decorators: [{
1671
+ type: Component,
1672
+ args: [{ selector: 'mn-lib-checkbox', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col\">\n <label class=\"flex flex-row items-center gap-x-2 cursor-pointer\" [attr.for]=\"resolvedId\">\n <input\n type=\"checkbox\"\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [checked]=\"value\"\n [ngClass]=\"checkboxClasses\"\n (change)=\"handleChange($any($event.target).checked)\"\n (blur)=\"handleBlur()\"\n />\n @if (uiConfig.label || props.label) {\n <span class=\"text-sm text-gray-700 select-none\">{{ uiConfig.label || props.label }}</span>\n }\n </label>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
1673
+ }], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
1674
+ type: Optional
1675
+ }, {
1676
+ type: Self
1677
+ }] }], propDecorators: { props: [{
1464
1678
  type: Input,
1465
- args: ['mn-instance']
1679
+ args: [{ required: true }]
1466
1680
  }] } });
1467
1681
 
1682
+ const mnDatetimeVariants = tv({
1683
+ base: 'bg-white border-1 border-gray-500 placeholder-gray-500 text-sm',
1684
+ variants: {
1685
+ shadow: {
1686
+ true: 'shadow-lg',
1687
+ },
1688
+ size: {
1689
+ sm: 'p-2',
1690
+ md: 'p-3',
1691
+ lg: 'p-4',
1692
+ },
1693
+ borderRadius: {
1694
+ none: 'rounded-none',
1695
+ xs: 'rounded-xs',
1696
+ sm: 'rounded-sm',
1697
+ md: 'rounded-md',
1698
+ lg: 'rounded-lg',
1699
+ xl: 'rounded-xl',
1700
+ two_xl: 'rounded-2xl',
1701
+ three_xl: 'rounded-3xl',
1702
+ four_xl: 'rounded-4xl',
1703
+ },
1704
+ fullWidth: {
1705
+ true: 'w-full',
1706
+ },
1707
+ },
1708
+ defaultVariants: {
1709
+ size: 'md',
1710
+ borderRadius: 'md',
1711
+ },
1712
+ });
1713
+
1714
+ const MN_DATETIME_CONFIG = new InjectionToken('MN_DATETIME_CONFIG');
1715
+ class MnDatetime {
1716
+ ngControl;
1717
+ uiConfig = {};
1718
+ props;
1719
+ configService = inject(MnConfigService);
1720
+ sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
1721
+ explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
1722
+ value = null;
1723
+ isDisabled = false;
1724
+ onChange = () => { };
1725
+ onTouched = () => { };
1726
+ builtInErrorMessages = {
1727
+ required: 'This field is required',
1728
+ mnMin: (args) => `Date/time must be from ${args.min} onwards`,
1729
+ mnMax: (args) => `Date/time must be up to ${args.max}`,
1730
+ };
1731
+ constructor(ngControl) {
1732
+ this.ngControl = ngControl;
1733
+ if (this.ngControl)
1734
+ this.ngControl.valueAccessor = this;
1735
+ }
1736
+ ngOnInit() {
1737
+ this.resolveConfig();
1738
+ }
1739
+ resolveConfig() {
1740
+ const instanceId = this.explicitInstanceId || `mn-datetime-${this.props.id}`;
1741
+ this.uiConfig = this.configService.resolve('mn-datetime', this.sectionPath, instanceId);
1742
+ if (this.props.label) {
1743
+ this.uiConfig = { ...this.uiConfig, label: this.props.label };
1744
+ }
1745
+ if (this.props.placeholder) {
1746
+ this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
1747
+ }
1748
+ }
1749
+ // ========== ControlValueAccessor Implementation ==========
1750
+ writeValue(val) {
1751
+ this.value = val != null ? String(val) : null;
1752
+ }
1753
+ registerOnChange(fn) {
1754
+ this.onChange = fn;
1755
+ }
1756
+ registerOnTouched(fn) {
1757
+ this.onTouched = fn;
1758
+ }
1759
+ setDisabledState(isDisabled) {
1760
+ this.isDisabled = isDisabled;
1761
+ }
1762
+ // ========== Event Handlers ==========
1763
+ handleInput(raw) {
1764
+ this.value = raw;
1765
+ this.onChange(raw);
1766
+ }
1767
+ handleBlur() {
1768
+ this.onTouched();
1769
+ }
1770
+ // ========== Error Handling ==========
1771
+ get control() {
1772
+ return this.ngControl?.control ?? null;
1773
+ }
1774
+ get showError() {
1775
+ const c = this.control;
1776
+ return !!c && c.invalid && (c.touched || c.dirty);
1777
+ }
1778
+ pickErrorKey(errors) {
1779
+ if (this.props.errorPriority) {
1780
+ for (const key of this.props.errorPriority) {
1781
+ if (errors[key] !== undefined) {
1782
+ return key;
1783
+ }
1784
+ }
1785
+ }
1786
+ return Object.keys(errors)[0];
1787
+ }
1788
+ isRequired() {
1789
+ if (!this.control)
1790
+ return false;
1791
+ return this.control.hasValidator(Validators.required);
1792
+ }
1793
+ resolveErrorMessageForKey(errorKey, errors) {
1794
+ const errorArgs = errors[errorKey];
1795
+ const customMsg = this.props.errorMessages?.[errorKey];
1796
+ const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
1797
+ const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
1798
+ const fallbackMsg = this.props.defaultErrorMessage;
1799
+ const msgDef = customMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
1800
+ if (typeof msgDef === 'function') {
1801
+ return msgDef(errorArgs, errors);
1802
+ }
1803
+ return msgDef;
1804
+ }
1805
+ get errorMessages() {
1806
+ const errors = this.control?.errors;
1807
+ if (!errors)
1808
+ return [];
1809
+ return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
1810
+ }
1811
+ get errorMessage() {
1812
+ const errors = this.control?.errors;
1813
+ if (!errors)
1814
+ return null;
1815
+ const errorKey = this.pickErrorKey(errors);
1816
+ return this.resolveErrorMessageForKey(errorKey, errors);
1817
+ }
1818
+ // ========== Resolved Properties ==========
1819
+ get resolvedId() {
1820
+ return this.props.id;
1821
+ }
1822
+ get resolvedName() {
1823
+ return this.props?.name ?? null;
1824
+ }
1825
+ get resolvedMode() {
1826
+ return this.props.mode ?? 'datetime-local';
1827
+ }
1828
+ get inputClasses() {
1829
+ return mnDatetimeVariants({
1830
+ size: this.props.size,
1831
+ borderRadius: this.props.borderRadius,
1832
+ shadow: this.props.shadow,
1833
+ fullWidth: this.props.fullWidth,
1834
+ });
1835
+ }
1836
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDatetime, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
1837
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDatetime, isStandalone: true, selector: "mn-lib-datetime", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
1838
+ }
1839
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDatetime, decorators: [{
1840
+ type: Component,
1841
+ args: [{ selector: 'mn-lib-datetime', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
1842
+ }], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
1843
+ type: Optional
1844
+ }, {
1845
+ type: Self
1846
+ }] }], propDecorators: { props: [{
1847
+ type: Input,
1848
+ args: [{ required: true }]
1849
+ }] } });
1850
+
1851
+ const mnMultiSelectVariants = tv({
1852
+ base: 'bg-white border-1 border-gray-500 text-sm cursor-pointer',
1853
+ variants: {
1854
+ shadow: {
1855
+ true: 'shadow-lg',
1856
+ },
1857
+ size: {
1858
+ sm: 'p-2',
1859
+ md: 'p-3',
1860
+ lg: 'p-4',
1861
+ },
1862
+ borderRadius: {
1863
+ none: 'rounded-none',
1864
+ xs: 'rounded-xs',
1865
+ sm: 'rounded-sm',
1866
+ md: 'rounded-md',
1867
+ lg: 'rounded-lg',
1868
+ xl: 'rounded-xl',
1869
+ two_xl: 'rounded-2xl',
1870
+ three_xl: 'rounded-3xl',
1871
+ four_xl: 'rounded-4xl',
1872
+ },
1873
+ fullWidth: {
1874
+ true: 'w-full',
1875
+ },
1876
+ },
1877
+ defaultVariants: {
1878
+ size: 'md',
1879
+ borderRadius: 'md',
1880
+ },
1881
+ });
1882
+
1883
+ const MN_MULTI_SELECT_CONFIG = new InjectionToken('MN_MULTI_SELECT_CONFIG');
1884
+ class MnMultiSelect {
1885
+ ngControl;
1886
+ uiConfig = {};
1887
+ props;
1888
+ configService = inject(MnConfigService);
1889
+ sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
1890
+ explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
1891
+ elRef = inject(ElementRef);
1892
+ /** Currently selected values */
1893
+ selectedValues = [];
1894
+ isOpen = false;
1895
+ isDisabled = false;
1896
+ searchTerm = '';
1897
+ onChange = () => { };
1898
+ onTouched = () => { };
1899
+ builtInErrorMessages = {
1900
+ required: 'At least one option must be selected',
1901
+ };
1902
+ constructor(ngControl) {
1903
+ this.ngControl = ngControl;
1904
+ if (this.ngControl)
1905
+ this.ngControl.valueAccessor = this;
1906
+ }
1907
+ ngOnInit() {
1908
+ this.resolveConfig();
1909
+ }
1910
+ resolveConfig() {
1911
+ const instanceId = this.explicitInstanceId || `mn-multi-select-${this.props.id}`;
1912
+ this.uiConfig = this.configService.resolve('mn-multi-select', this.sectionPath, instanceId);
1913
+ if (this.props.label) {
1914
+ this.uiConfig = { ...this.uiConfig, label: this.props.label };
1915
+ }
1916
+ if (this.props.placeholder) {
1917
+ this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
1918
+ }
1919
+ }
1920
+ // ========== ControlValueAccessor Implementation ==========
1921
+ writeValue(val) {
1922
+ this.selectedValues = Array.isArray(val) ? val : [];
1923
+ }
1924
+ registerOnChange(fn) {
1925
+ this.onChange = fn;
1926
+ }
1927
+ registerOnTouched(fn) {
1928
+ this.onTouched = fn;
1929
+ }
1930
+ setDisabledState(isDisabled) {
1931
+ this.isDisabled = isDisabled;
1932
+ }
1933
+ // ========== Dropdown Logic ==========
1934
+ toggle() {
1935
+ if (this.isDisabled)
1936
+ return;
1937
+ this.isOpen = !this.isOpen;
1938
+ if (!this.isOpen) {
1939
+ this.searchTerm = '';
1940
+ }
1941
+ }
1942
+ onDocumentClick(event) {
1943
+ if (!this.elRef.nativeElement.contains(event.target)) {
1944
+ if (this.isOpen) {
1945
+ this.isOpen = false;
1946
+ this.searchTerm = '';
1947
+ }
1948
+ }
1949
+ }
1950
+ toggleOption(option) {
1951
+ if (option.disabled)
1952
+ return;
1953
+ const index = this.selectedValues.indexOf(option.value);
1954
+ if (index > -1) {
1955
+ this.selectedValues = this.selectedValues.filter(v => v !== option.value);
1956
+ }
1957
+ else {
1958
+ if (this.props.maxSelections && this.selectedValues.length >= this.props.maxSelections) {
1959
+ return;
1960
+ }
1961
+ this.selectedValues = [...this.selectedValues, option.value];
1962
+ }
1963
+ this.onChange(this.selectedValues);
1964
+ }
1965
+ removeOption(option, event) {
1966
+ event.stopPropagation();
1967
+ this.selectedValues = this.selectedValues.filter(v => v !== option.value);
1968
+ this.onChange(this.selectedValues);
1969
+ }
1970
+ isSelected(option) {
1971
+ return this.selectedValues.includes(option.value);
1972
+ }
1973
+ isMaxReached(option) {
1974
+ if (!this.props.maxSelections)
1975
+ return false;
1976
+ return this.selectedValues.length >= this.props.maxSelections && !this.isSelected(option);
1977
+ }
1978
+ onSearch(term) {
1979
+ this.searchTerm = term;
1980
+ }
1981
+ get filteredOptions() {
1982
+ if (!this.searchTerm)
1983
+ return this.props.options;
1984
+ const lower = this.searchTerm.toLowerCase();
1985
+ return this.props.options.filter(o => o.label.toLowerCase().includes(lower));
1986
+ }
1987
+ get selectedOptions() {
1988
+ return this.props.options.filter(o => this.selectedValues.includes(o.value));
1989
+ }
1990
+ handleBlur() {
1991
+ this.onTouched();
1992
+ }
1993
+ // ========== Error Handling ==========
1994
+ get control() {
1995
+ return this.ngControl?.control ?? null;
1996
+ }
1997
+ get showError() {
1998
+ const c = this.control;
1999
+ return !!c && c.invalid && (c.touched || c.dirty);
2000
+ }
2001
+ pickErrorKey(errors) {
2002
+ if (this.props.errorPriority) {
2003
+ for (const key of this.props.errorPriority) {
2004
+ if (errors[key] !== undefined) {
2005
+ return key;
2006
+ }
2007
+ }
2008
+ }
2009
+ return Object.keys(errors)[0];
2010
+ }
2011
+ isRequired() {
2012
+ if (!this.control)
2013
+ return false;
2014
+ return this.control.hasValidator(Validators.required);
2015
+ }
2016
+ resolveErrorMessageForKey(errorKey, errors) {
2017
+ const errorArgs = errors[errorKey];
2018
+ const customMsg = this.props.errorMessages?.[errorKey];
2019
+ const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
2020
+ const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
2021
+ const fallbackMsg = this.props.defaultErrorMessage;
2022
+ const msgDef = customMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
2023
+ if (typeof msgDef === 'function') {
2024
+ return msgDef(errorArgs, errors);
2025
+ }
2026
+ return msgDef;
2027
+ }
2028
+ get errorMessages() {
2029
+ const errors = this.control?.errors;
2030
+ if (!errors)
2031
+ return [];
2032
+ return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
2033
+ }
2034
+ get errorMessage() {
2035
+ const errors = this.control?.errors;
2036
+ if (!errors)
2037
+ return null;
2038
+ const errorKey = this.pickErrorKey(errors);
2039
+ return this.resolveErrorMessageForKey(errorKey, errors);
2040
+ }
2041
+ // ========== Resolved Properties ==========
2042
+ get resolvedId() {
2043
+ return this.props.id;
2044
+ }
2045
+ get resolvedName() {
2046
+ return this.props?.name ?? null;
2047
+ }
2048
+ get triggerClasses() {
2049
+ return mnMultiSelectVariants({
2050
+ size: this.props.size,
2051
+ borderRadius: this.props.borderRadius,
2052
+ shadow: this.props.shadow,
2053
+ fullWidth: this.props.fullWidth,
2054
+ });
2055
+ }
2056
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
2057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <div class=\"flex flex-row items-center gap-x-1 flex-wrap min-h-[1.5rem]\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-gray-500\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <span class=\"inline-flex items-center gap-x-1 bg-gray-100 text-gray-700 text-xs px-2 py-0.5 rounded-md\">\n {{ opt.label }}\n <button\n type=\"button\"\n class=\"text-gray-400 hover:text-gray-600 cursor-pointer\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n >\u00D7</button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 12 12\">\n <path fill=\"currentColor\" d=\"M6 8L1 3h10z\"/>\n </svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n <div class=\"relative z-50\">\n <div class=\"absolute top-0 left-0 right-0 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto\">\n @if (props.searchable) {\n <div class=\"p-2 border-b border-gray-200\">\n <input\n type=\"text\"\n class=\"w-full p-1.5 text-sm border border-gray-300 rounded-md outline-none focus:border-blue-500\"\n [placeholder]=\"props.searchPlaceholder || 'Search...'\"\n [value]=\"searchTerm\"\n (input)=\"onSearch(($any($event.target)).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n }\n @for (opt of filteredOptions; track opt.value) {\n <div\n class=\"flex items-center gap-x-2 px-3 py-2 text-sm cursor-pointer hover:bg-gray-50\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n >\n <input\n type=\"checkbox\"\n class=\"w-4 h-4 accent-brand-500 pointer-events-none\"\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n tabindex=\"-1\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div class=\"px-3 py-2 text-sm text-gray-400\">No options found</div>\n }\n </div>\n </div>\n }\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
2058
+ }
2059
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
2060
+ type: Component,
2061
+ args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <div class=\"flex flex-row items-center gap-x-1 flex-wrap min-h-[1.5rem]\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-gray-500\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <span class=\"inline-flex items-center gap-x-1 bg-gray-100 text-gray-700 text-xs px-2 py-0.5 rounded-md\">\n {{ opt.label }}\n <button\n type=\"button\"\n class=\"text-gray-400 hover:text-gray-600 cursor-pointer\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n >\u00D7</button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 12 12\">\n <path fill=\"currentColor\" d=\"M6 8L1 3h10z\"/>\n </svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n <div class=\"relative z-50\">\n <div class=\"absolute top-0 left-0 right-0 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto\">\n @if (props.searchable) {\n <div class=\"p-2 border-b border-gray-200\">\n <input\n type=\"text\"\n class=\"w-full p-1.5 text-sm border border-gray-300 rounded-md outline-none focus:border-blue-500\"\n [placeholder]=\"props.searchPlaceholder || 'Search...'\"\n [value]=\"searchTerm\"\n (input)=\"onSearch(($any($event.target)).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n }\n @for (opt of filteredOptions; track opt.value) {\n <div\n class=\"flex items-center gap-x-2 px-3 py-2 text-sm cursor-pointer hover:bg-gray-50\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n >\n <input\n type=\"checkbox\"\n class=\"w-4 h-4 accent-brand-500 pointer-events-none\"\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n tabindex=\"-1\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div class=\"px-3 py-2 text-sm text-gray-400\">No options found</div>\n }\n </div>\n </div>\n }\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
2062
+ }], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
2063
+ type: Optional
2064
+ }, {
2065
+ type: Self
2066
+ }] }], propDecorators: { props: [{
2067
+ type: Input,
2068
+ args: [{ required: true }]
2069
+ }], onDocumentClick: [{
2070
+ type: HostListener,
2071
+ args: ['document:click', ['$event']]
2072
+ }] } });
2073
+
2074
+ // =========================
2075
+ // Enums / Value Types
2076
+ // =========================
2077
+ var ModalKind;
2078
+ (function (ModalKind) {
2079
+ ModalKind["WIZARD"] = "wizard";
2080
+ ModalKind["FORM"] = "form";
2081
+ ModalKind["CONFIRMATION"] = "confirmation";
2082
+ ModalKind["CUSTOM"] = "custom";
2083
+ })(ModalKind || (ModalKind = {}));
2084
+ var ModalSize;
2085
+ (function (ModalSize) {
2086
+ ModalSize["SM"] = "sm";
2087
+ ModalSize["MD"] = "md";
2088
+ ModalSize["LG"] = "lg";
2089
+ ModalSize["XL"] = "xl";
2090
+ ModalSize["FULL"] = "full";
2091
+ })(ModalSize || (ModalSize = {}));
2092
+ var CloseMode;
2093
+ (function (CloseMode) {
2094
+ CloseMode["ALLOWED"] = "allowed";
2095
+ CloseMode["GUARDED"] = "guarded";
2096
+ CloseMode["DISABLED"] = "disabled";
2097
+ })(CloseMode || (CloseMode = {}));
2098
+ var BackdropMode;
2099
+ (function (BackdropMode) {
2100
+ BackdropMode["HIDE"] = "hide";
2101
+ BackdropMode["STATIC"] = "static";
2102
+ BackdropMode["CLOSABLE"] = "closable";
2103
+ })(BackdropMode || (BackdropMode = {}));
2104
+ var KeyboardMode;
2105
+ (function (KeyboardMode) {
2106
+ KeyboardMode["ENABLED"] = "enabled";
2107
+ KeyboardMode["DISABLED"] = "disabled";
2108
+ })(KeyboardMode || (KeyboardMode = {}));
2109
+ var ModalIntent;
2110
+ (function (ModalIntent) {
2111
+ ModalIntent["NEUTRAL"] = "neutral";
2112
+ ModalIntent["INFO"] = "info";
2113
+ ModalIntent["SUCCESS"] = "success";
2114
+ ModalIntent["WARNING"] = "warning";
2115
+ ModalIntent["DANGER"] = "danger";
2116
+ })(ModalIntent || (ModalIntent = {}));
2117
+ var WizardFlowMode;
2118
+ (function (WizardFlowMode) {
2119
+ WizardFlowMode["LINEAR"] = "linear";
2120
+ WizardFlowMode["FREE"] = "free";
2121
+ })(WizardFlowMode || (WizardFlowMode = {}));
2122
+ var FormLayoutMode;
2123
+ (function (FormLayoutMode) {
2124
+ FormLayoutMode["SINGLE_COLUMN"] = "single-column";
2125
+ FormLayoutMode["TWO_COLUMN"] = "two-column";
2126
+ FormLayoutMode["INLINE"] = "inline";
2127
+ })(FormLayoutMode || (FormLayoutMode = {}));
2128
+ var SubmitMode;
2129
+ (function (SubmitMode) {
2130
+ SubmitMode["ONCE"] = "once";
2131
+ SubmitMode["RETRYABLE"] = "retryable";
2132
+ })(SubmitMode || (SubmitMode = {}));
2133
+ var ConfirmationTone;
2134
+ (function (ConfirmationTone) {
2135
+ ConfirmationTone["DEFAULT"] = "default";
2136
+ ConfirmationTone["WARNING"] = "warning";
2137
+ ConfirmationTone["DANGER"] = "danger";
2138
+ })(ConfirmationTone || (ConfirmationTone = {}));
2139
+ var FieldKind;
2140
+ (function (FieldKind) {
2141
+ FieldKind["TEXT"] = "text";
2142
+ FieldKind["NUMBER"] = "number";
2143
+ FieldKind["SELECT"] = "select";
2144
+ FieldKind["CHECKBOX"] = "checkbox";
2145
+ FieldKind["DATE"] = "date";
2146
+ FieldKind["TEXTAREA"] = "textarea";
2147
+ FieldKind["DATETIME"] = "datetime";
2148
+ FieldKind["MULTI_SELECT"] = "multi-select";
2149
+ FieldKind["MULTI_SELECT_TABLE"] = "multi-select-table";
2150
+ FieldKind["SINGLE_SELECT_TABLE"] = "single-select-table";
2151
+ FieldKind["PASSWORD"] = "password";
2152
+ FieldKind["FILE"] = "file";
2153
+ FieldKind["COLOR"] = "color";
2154
+ FieldKind["RATING"] = "rating";
2155
+ FieldKind["SLIDER"] = "slider";
2156
+ FieldKind["CUSTOM"] = "custom";
2157
+ })(FieldKind || (FieldKind = {}));
2158
+ var FieldAppearance;
2159
+ (function (FieldAppearance) {
2160
+ FieldAppearance["OUTLINE"] = "outline";
2161
+ FieldAppearance["FILLED"] = "filled";
2162
+ FieldAppearance["GHOST"] = "ghost";
2163
+ })(FieldAppearance || (FieldAppearance = {}));
2164
+ var SelectionMode;
2165
+ (function (SelectionMode) {
2166
+ SelectionMode["SINGLE"] = "single";
2167
+ SelectionMode["MULTIPLE"] = "multiple";
2168
+ })(SelectionMode || (SelectionMode = {}));
2169
+ var OptionState;
2170
+ (function (OptionState) {
2171
+ OptionState["ENABLED"] = "enabled";
2172
+ OptionState["DISABLED"] = "disabled";
2173
+ })(OptionState || (OptionState = {}));
2174
+ var ActionStyle;
2175
+ (function (ActionStyle) {
2176
+ ActionStyle["PRIMARY"] = "primary";
2177
+ ActionStyle["SECONDARY"] = "secondary";
2178
+ ActionStyle["DANGER"] = "danger";
2179
+ ActionStyle["GHOST"] = "ghost";
2180
+ })(ActionStyle || (ActionStyle = {}));
2181
+ var StepState;
2182
+ (function (StepState) {
2183
+ StepState["PENDING"] = "pending";
2184
+ StepState["ACTIVE"] = "active";
2185
+ StepState["COMPLETE"] = "complete";
2186
+ StepState["DISABLED"] = "disabled";
2187
+ StepState["HIDDEN"] = "hidden";
2188
+ })(StepState || (StepState = {}));
2189
+ var NavigationDirection;
2190
+ (function (NavigationDirection) {
2191
+ NavigationDirection["FORWARD"] = "forward";
2192
+ NavigationDirection["BACKWARD"] = "backward";
2193
+ NavigationDirection["DIRECT"] = "direct";
2194
+ })(NavigationDirection || (NavigationDirection = {}));
2195
+ var ModalCloseReason;
2196
+ (function (ModalCloseReason) {
2197
+ ModalCloseReason["COMPLETED"] = "completed";
2198
+ ModalCloseReason["CANCELLED"] = "cancelled";
2199
+ ModalCloseReason["DISMISSED"] = "dismissed";
2200
+ ModalCloseReason["BACKDROP"] = "backdrop";
2201
+ ModalCloseReason["ESCAPE"] = "escape";
2202
+ ModalCloseReason["PROGRAMMATIC"] = "programmatic";
2203
+ ModalCloseReason["GUARD_REJECTED"] = "guard_rejected";
2204
+ })(ModalCloseReason || (ModalCloseReason = {}));
2205
+ var ValidationStatus;
2206
+ (function (ValidationStatus) {
2207
+ ValidationStatus["VALID"] = "valid";
2208
+ ValidationStatus["INVALID"] = "invalid";
2209
+ ValidationStatus["PENDING"] = "pending";
2210
+ })(ValidationStatus || (ValidationStatus = {}));
2211
+ var ValidationCode;
2212
+ (function (ValidationCode) {
2213
+ ValidationCode["REQUIRED"] = "required";
2214
+ ValidationCode["MIN"] = "min";
2215
+ ValidationCode["MAX"] = "max";
2216
+ ValidationCode["PATTERN"] = "pattern";
2217
+ ValidationCode["CUSTOM"] = "custom";
2218
+ })(ValidationCode || (ValidationCode = {}));
2219
+
2220
+ class StepBuilder {
2221
+ config;
2222
+ currentRow = [];
2223
+ currentRowColumns = 1;
2224
+ constructor(id, title) {
2225
+ this.config = {
2226
+ id,
2227
+ title,
2228
+ fields: [],
2229
+ state: StepState.PENDING,
2230
+ };
2231
+ }
2232
+ body(body) {
2233
+ this.config.body = body;
2234
+ return this;
2235
+ }
2236
+ state(state) {
2237
+ this.config.state = state;
2238
+ return this;
2239
+ }
2240
+ guard(guard) {
2241
+ this.config.guard = guard;
2242
+ return this;
2243
+ }
2244
+ validators(validators) {
2245
+ this.config.validators = validators;
2246
+ return this;
2247
+ }
2248
+ /**
2249
+ * Add a field to this step. This is the single API for all field types.
2250
+ *
2251
+ * @example
2252
+ * s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
2253
+ * s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
2254
+ */
2255
+ field(field) {
2256
+ this.flushCurrentRow();
2257
+ this.config.fields = this.config.fields || [];
2258
+ this.config.fields.push(field);
2259
+ if (!this.config.rows) {
2260
+ this.config.rows = [];
2261
+ }
2262
+ this.config.rows.push({
2263
+ columns: 1,
2264
+ fields: [{ field, span: 1 }],
2265
+ });
2266
+ return this;
2267
+ }
2268
+ /**
2269
+ * Start a new row with the specified number of columns.
2270
+ * All subsequent `addToRow()` calls will add fields to this row.
2271
+ */
2272
+ row(columns = 2) {
2273
+ this.flushCurrentRow();
2274
+ this.currentRowColumns = columns;
2275
+ return this;
2276
+ }
2277
+ /**
2278
+ * Add a field to the current row started by `row()`.
2279
+ */
2280
+ addToRow(field, span = 1) {
2281
+ this.config.fields = this.config.fields || [];
2282
+ this.config.fields.push(field);
2283
+ this.currentRow.push({ field, span });
2284
+ return this;
2285
+ }
2286
+ flushCurrentRow() {
2287
+ if (this.currentRow.length > 0) {
2288
+ if (!this.config.rows) {
2289
+ this.config.rows = [];
2290
+ }
2291
+ this.config.rows.push({
2292
+ columns: this.currentRowColumns,
2293
+ fields: [...this.currentRow],
2294
+ });
2295
+ this.currentRow = [];
2296
+ this.currentRowColumns = 1;
2297
+ }
2298
+ }
2299
+ /**
2300
+ * Add a field group with a section header.
2301
+ */
2302
+ fieldGroup(group) {
2303
+ this.flushCurrentRow();
2304
+ if (!this.config.fieldGroups) {
2305
+ this.config.fieldGroups = [];
2306
+ }
2307
+ this.config.fields = this.config.fields || [];
2308
+ group.fields.forEach(f => this.config.fields.push(f));
2309
+ if (!group.rows) {
2310
+ group.rows = group.fields.map(f => ({
2311
+ columns: 1,
2312
+ fields: [{ field: f, span: 1 }],
2313
+ }));
2314
+ }
2315
+ this.config.fieldGroups.push(group);
2316
+ return this;
2317
+ }
2318
+ /**
2319
+ * Add form-level validators for cross-field validation within this step.
2320
+ */
2321
+ formValidators(validators) {
2322
+ this.config.formValidators = validators;
2323
+ return this;
2324
+ }
2325
+ /**
2326
+ * Add Angular FormGroup-level validators for this step.
2327
+ */
2328
+ groupValidators(validators) {
2329
+ this.config.groupValidators = validators;
2330
+ return this;
2331
+ }
2332
+ /**
2333
+ * Set initial values for fields in this step.
2334
+ */
2335
+ initialValue(value) {
2336
+ this.config.initialValue = value;
2337
+ return this;
2338
+ }
2339
+ /**
2340
+ * Set a visibility condition for this step based on aggregated wizard data.
2341
+ */
2342
+ visible(condition) {
2343
+ this.config.visible = condition;
2344
+ return this;
2345
+ }
2346
+ build() {
2347
+ this.flushCurrentRow();
2348
+ return this.config;
2349
+ }
2350
+ }
2351
+
2352
+ class BaseModalBuilder {
2353
+ config;
2354
+ constructor(initialConfig) {
2355
+ this.config = initialConfig;
2356
+ }
2357
+ title(title) {
2358
+ this.config.title = title;
2359
+ return this;
2360
+ }
2361
+ subtitle(subtitle) {
2362
+ this.config.subtitle = subtitle;
2363
+ return this;
2364
+ }
2365
+ description(description) {
2366
+ this.config.description = description;
2367
+ return this;
2368
+ }
2369
+ closeGuard(guard) {
2370
+ this.config.closeGuard = guard;
2371
+ return this;
2372
+ }
2373
+ size(size) {
2374
+ this.config.size = size;
2375
+ return this;
2376
+ }
2377
+ closeMode(mode) {
2378
+ this.config.closeMode = mode;
2379
+ return this;
2380
+ }
2381
+ backdrop(mode) {
2382
+ this.config.backdrop = mode;
2383
+ return this;
2384
+ }
2385
+ keyboard(mode) {
2386
+ this.config.keyboard = mode;
2387
+ return this;
2388
+ }
2389
+ intent(intent) {
2390
+ this.config.intent = intent;
2391
+ return this;
2392
+ }
2393
+ footerActions(actions) {
2394
+ this.config.footerActions = actions;
2395
+ return this;
2396
+ }
2397
+ polling(config) {
2398
+ this.config.polling = config;
2399
+ return this;
2400
+ }
2401
+ onCancel(handler) {
2402
+ this.config.onCancel = handler;
2403
+ return this;
2404
+ }
2405
+ i18n(labels) {
2406
+ this.config.i18n = labels;
2407
+ return this;
2408
+ }
2409
+ build() {
2410
+ return Object.freeze({ ...this.config });
2411
+ }
2412
+ }
2413
+
2414
+ class WizardModalBuilder extends BaseModalBuilder {
2415
+ constructor() {
2416
+ super({
2417
+ kind: ModalKind.WIZARD,
2418
+ steps: [],
2419
+ });
2420
+ }
2421
+ step(step) {
2422
+ if (typeof step === 'function') {
2423
+ const builder = new StepBuilder('', '');
2424
+ step(builder);
2425
+ const builtStep = builder.build();
2426
+ this.config.steps.push(builtStep);
2427
+ }
2428
+ else {
2429
+ this.config.steps.push(step);
2430
+ }
2431
+ return this;
2432
+ }
2433
+ addStep(title, buildFn, id) {
2434
+ const stepId = id || `step-${this.config.steps.length}`;
2435
+ const builder = new StepBuilder(stepId, title);
2436
+ buildFn(builder);
2437
+ this.config.steps.push(builder.build());
2438
+ return this;
2439
+ }
2440
+ startAt(stepId) {
2441
+ this.config.startStepId = stepId;
2442
+ return this;
2443
+ }
2444
+ flow(mode) {
2445
+ this.config.flow = mode;
2446
+ return this;
2447
+ }
2448
+ onStepChange(handler) {
2449
+ this.config.onStepChange = handler;
2450
+ return this;
2451
+ }
2452
+ onComplete(handler) {
2453
+ this.config.onComplete = handler;
2454
+ return this;
2455
+ }
2456
+ onBeforeComplete(validators) {
2457
+ this.config.onBeforeComplete = validators;
2458
+ return this;
2459
+ }
2460
+ }
2461
+
2462
+ class FormModalBuilder extends BaseModalBuilder {
2463
+ currentRow = [];
2464
+ currentRowColumns = 1;
2465
+ constructor() {
2466
+ super({
2467
+ kind: ModalKind.FORM,
2468
+ fields: [],
2469
+ rows: [],
2470
+ });
2471
+ }
2472
+ /**
2473
+ * Add a field as a full-width row (single column).
2474
+ * This is the simple API — each field gets its own row.
2475
+ */
2476
+ field(field) {
2477
+ this.flushCurrentRow();
2478
+ this.config.fields.push(field);
2479
+ this.config.rows.push({
2480
+ columns: 1,
2481
+ fields: [{ field, span: 1 }],
2482
+ });
2483
+ return this;
2484
+ }
2485
+ /**
2486
+ * Start a new row with the specified number of columns.
2487
+ * All subsequent `addToRow()` calls will add fields to this row
2488
+ * until the next `row()` or `field()` call.
2489
+ *
2490
+ * @example
2491
+ * .row(2)
2492
+ * .addToRow({ kind: FieldKind.TEXT, key: 'firstName', label: 'First Name' })
2493
+ * .addToRow({ kind: FieldKind.TEXT, key: 'lastName', label: 'Last Name' })
2494
+ * .row(3)
2495
+ * .addToRow({ kind: FieldKind.TEXT, key: 'city', label: 'City' }, 2)
2496
+ * .addToRow({ kind: FieldKind.TEXT, key: 'zip', label: 'ZIP' })
2497
+ */
2498
+ row(columns = 2) {
2499
+ this.flushCurrentRow();
2500
+ this.currentRowColumns = columns;
2501
+ return this;
2502
+ }
2503
+ /**
2504
+ * Add a field to the current row started by `row()`.
2505
+ * @param field - The field configuration
2506
+ * @param span - How many columns this field should span (default: 1)
2507
+ */
2508
+ addToRow(field, span = 1) {
2509
+ this.config.fields.push(field);
2510
+ this.currentRow.push({ field, span });
2511
+ return this;
2512
+ }
2513
+ flushCurrentRow() {
2514
+ if (this.currentRow.length > 0) {
2515
+ this.config.rows.push({
2516
+ columns: this.currentRowColumns,
2517
+ fields: [...this.currentRow],
2518
+ });
2519
+ this.currentRow = [];
2520
+ this.currentRowColumns = 1;
2521
+ }
2522
+ }
2523
+ layout(mode) {
2524
+ this.config.layout = mode;
2525
+ return this;
2526
+ }
2527
+ initialValue(value) {
2528
+ this.config.initialValue = value;
2529
+ return this;
2530
+ }
2531
+ submitMode(mode) {
2532
+ this.config.submitMode = mode;
2533
+ return this;
2534
+ }
2535
+ onComplete(handler) {
2536
+ this.config.onComplete = handler;
2537
+ return this;
2538
+ }
2539
+ /**
2540
+ * Add form-level validators for cross-field validation.
2541
+ * These receive the entire form value and return an error map or null.
2542
+ */
2543
+ formValidators(validators) {
2544
+ this.config.formValidators = validators;
2545
+ return this;
2546
+ }
2547
+ /**
2548
+ * Add Angular FormGroup-level validators.
2549
+ * These are standard Angular ValidatorFn applied to the FormGroup itself.
2550
+ */
2551
+ groupValidators(validators) {
2552
+ this.config.groupValidators = validators;
2553
+ return this;
2554
+ }
2555
+ /**
2556
+ * Add a field group with a section header.
2557
+ * Groups visually separate fields with a title and optional description.
2558
+ */
2559
+ fieldGroup(group) {
2560
+ this.flushCurrentRow();
2561
+ if (!this.config.fieldGroups) {
2562
+ this.config.fieldGroups = [];
2563
+ }
2564
+ // Also add group fields to the flat fields array for form control creation
2565
+ group.fields.forEach(f => this.config.fields.push(f));
2566
+ // Build rows for the group if not provided
2567
+ if (!group.rows) {
2568
+ group.rows = group.fields.map(f => ({
2569
+ columns: 1,
2570
+ fields: [{ field: f, span: 1 }],
2571
+ }));
2572
+ }
2573
+ this.config.fieldGroups.push(group);
2574
+ return this;
2575
+ }
2576
+ build() {
2577
+ this.flushCurrentRow();
2578
+ return super.build();
2579
+ }
2580
+ }
2581
+
2582
+ class ConfirmationModalBuilder extends BaseModalBuilder {
2583
+ constructor() {
2584
+ super({
2585
+ kind: ModalKind.CONFIRMATION,
2586
+ message: '',
2587
+ });
2588
+ }
2589
+ message(text) {
2590
+ this.config.message = text;
2591
+ return this;
2592
+ }
2593
+ tone(tone) {
2594
+ this.config.tone = tone;
2595
+ return this;
2596
+ }
2597
+ confirmAction(action) {
2598
+ this.config.confirm = action;
2599
+ return this;
2600
+ }
2601
+ cancelAction(action) {
2602
+ this.config.cancel = action;
2603
+ return this;
2604
+ }
2605
+ }
2606
+
2607
+ class CustomModalBuilder extends BaseModalBuilder {
2608
+ constructor() {
2609
+ super({
2610
+ kind: ModalKind.CUSTOM,
2611
+ });
2612
+ }
2613
+ component(component) {
2614
+ this.config.component = component;
2615
+ return this;
2616
+ }
2617
+ template(template) {
2618
+ this.config.template = template;
2619
+ return this;
2620
+ }
2621
+ inputs(inputs) {
2622
+ this.config.inputs = inputs;
2623
+ return this;
2624
+ }
2625
+ onComplete(handler) {
2626
+ this.config.onComplete = handler;
2627
+ return this;
2628
+ }
2629
+ }
2630
+
2631
+ class ModalBuilder {
2632
+ static wizard() {
2633
+ return new WizardModalBuilder();
2634
+ }
2635
+ static form() {
2636
+ return new FormModalBuilder();
2637
+ }
2638
+ static confirmation() {
2639
+ return new ConfirmationModalBuilder();
2640
+ }
2641
+ static custom() {
2642
+ return new CustomModalBuilder();
2643
+ }
2644
+ }
2645
+
2646
+ class MnModalRef {
2647
+ componentRef;
2648
+ config;
2649
+ closeSubject = new Subject();
2650
+ afterClosed$ = this.closeSubject.asObservable();
2651
+ constructor(componentRef, config) {
2652
+ this.componentRef = componentRef;
2653
+ this.config = config;
2654
+ }
2655
+ close(result) {
2656
+ const event = {
2657
+ reason: ModalCloseReason.COMPLETED,
2658
+ result,
2659
+ };
2660
+ this.animateAndDestroy(event);
2661
+ }
2662
+ dismiss(reason) {
2663
+ const event = {
2664
+ reason,
2665
+ };
2666
+ this.animateAndDestroy(event);
2667
+ }
2668
+ async animateAndDestroy(event) {
2669
+ const shell = this.componentRef.instance;
2670
+ if (shell && typeof shell.startClosing === 'function') {
2671
+ await shell.startClosing();
2672
+ }
2673
+ this.closeSubject.next(event);
2674
+ this.closeSubject.complete();
2675
+ this.destroy();
2676
+ }
2677
+ update(config) {
2678
+ Object.assign(this.config, config);
2679
+ // Trigger change detection on the shell component
2680
+ this.componentRef.changeDetectorRef.detectChanges();
2681
+ }
2682
+ destroy() {
2683
+ this.componentRef.destroy();
2684
+ }
2685
+ }
2686
+
2687
+ class MnCustomFieldHostDirective {
2688
+ vcr;
2689
+ component;
2690
+ inputs;
2691
+ componentRef;
2692
+ onChange = () => { };
2693
+ onTouched = () => { };
2694
+ constructor(vcr) {
2695
+ this.vcr = vcr;
2696
+ }
2697
+ ngOnInit() {
2698
+ if (!this.component)
2699
+ return;
2700
+ this.vcr.clear();
2701
+ this.componentRef = this.vcr.createComponent(this.component);
2702
+ // Pass inputs to the component
2703
+ if (this.inputs) {
2704
+ Object.entries(this.inputs).forEach(([key, value]) => {
2705
+ this.componentRef.instance[key] = value;
2706
+ });
2707
+ }
2708
+ // Wire up value accessor if the component supports it
2709
+ const instance = this.componentRef.instance;
2710
+ if (typeof instance.registerOnChange === 'function') {
2711
+ instance.registerOnChange((val) => this.onChange(val));
2712
+ }
2713
+ if (typeof instance.registerOnTouched === 'function') {
2714
+ instance.registerOnTouched(() => this.onTouched());
2715
+ }
2716
+ }
2717
+ ngOnDestroy() {
2718
+ this.componentRef?.destroy();
2719
+ }
2720
+ writeValue(val) {
2721
+ const instance = this.componentRef?.instance;
2722
+ if (instance && typeof instance.writeValue === 'function') {
2723
+ instance.writeValue(val);
2724
+ }
2725
+ }
2726
+ registerOnChange(fn) {
2727
+ this.onChange = fn;
2728
+ }
2729
+ registerOnTouched(fn) {
2730
+ this.onTouched = fn;
2731
+ }
2732
+ setDisabledState(isDisabled) {
2733
+ const instance = this.componentRef?.instance;
2734
+ if (instance && typeof instance.setDisabledState === 'function') {
2735
+ instance.setDisabledState(isDisabled);
2736
+ }
2737
+ }
2738
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomFieldHostDirective, deps: [{ token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive });
2739
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnCustomFieldHostDirective, isStandalone: true, selector: "[mnCustomFieldHost]", inputs: { component: "component", inputs: "inputs" }, providers: [
2740
+ {
2741
+ provide: NG_VALUE_ACCESSOR,
2742
+ useExisting: forwardRef(() => MnCustomFieldHostDirective),
2743
+ multi: true,
2744
+ },
2745
+ ], ngImport: i0 });
2746
+ }
2747
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomFieldHostDirective, decorators: [{
2748
+ type: Directive,
2749
+ args: [{
2750
+ selector: '[mnCustomFieldHost]',
2751
+ standalone: true,
2752
+ providers: [
2753
+ {
2754
+ provide: NG_VALUE_ACCESSOR,
2755
+ useExisting: forwardRef(() => MnCustomFieldHostDirective),
2756
+ multi: true,
2757
+ },
2758
+ ],
2759
+ }]
2760
+ }], ctorParameters: () => [{ type: i0.ViewContainerRef }], propDecorators: { component: [{
2761
+ type: Input
2762
+ }], inputs: [{
2763
+ type: Input
2764
+ }] } });
2765
+
2766
+ // ── Column Sort Type ──
2767
+ var ColumnSortType;
2768
+ (function (ColumnSortType) {
2769
+ ColumnSortType["ALPHABETICAL"] = "ALPHABETICAL";
2770
+ ColumnSortType["NUMERICAL"] = "NUMERICAL";
2771
+ ColumnSortType["DATE"] = "DATE";
2772
+ ColumnSortType["NONE"] = "NONE";
2773
+ })(ColumnSortType || (ColumnSortType = {}));
2774
+
2775
+ class MnTable {
2776
+ dataSource;
2777
+ sortChange = new EventEmitter();
2778
+ selectionChange = new EventEmitter();
2779
+ rowClick = new EventEmitter();
2780
+ filteredItems = [];
2781
+ searchValue = '';
2782
+ loadingMoreRows = false;
2783
+ currentSort = null;
2784
+ selectedIds = new Set();
2785
+ dataSubscription;
2786
+ searchSubject = new Subject();
2787
+ searchSubscription;
2788
+ ngOnInit() {
2789
+ this.currentSort = this.dataSource.defaultSort ?? null;
2790
+ this.filteredItems = this.dataSource.dataRows.value;
2791
+ this.dataSubscription = this.dataSource.dataRows.subscribe(() => {
2792
+ this.applyFilterAndSort(false);
2793
+ });
2794
+ this.searchSubscription = this.searchSubject
2795
+ .pipe(debounceTime(300))
2796
+ .subscribe(value => {
2797
+ this.searchValue = value;
2798
+ this.applyFilterAndSort(true);
2799
+ });
2800
+ }
2801
+ ngOnDestroy() {
2802
+ this.dataSubscription?.unsubscribe();
2803
+ this.searchSubscription?.unsubscribe();
2804
+ }
2805
+ // ── Search ──
2806
+ onSearch(searchString) {
2807
+ this.searchSubject.next(searchString);
2808
+ }
2809
+ // ── Sorting ──
2810
+ sort(column) {
2811
+ if (!column.sortType || column.sortType === ColumnSortType.NONE)
2812
+ return;
2813
+ if (this.currentSort?.columnKey === column.key) {
2814
+ this.currentSort = this.currentSort.direction === 'asc'
2815
+ ? { columnKey: column.key, direction: 'desc' }
2816
+ : null;
2817
+ }
2818
+ else {
2819
+ this.currentSort = { columnKey: column.key, direction: 'asc' };
2820
+ }
2821
+ this.sortChange.emit(this.currentSort);
2822
+ this.applyFilterAndSort(false);
2823
+ }
2824
+ getSortIcon(column) {
2825
+ if (!this.currentSort || this.currentSort.columnKey !== column.key)
2826
+ return '';
2827
+ return this.currentSort.direction === 'asc' ? '▲' : '▼';
2828
+ }
2829
+ isSortable(column) {
2830
+ return !!column.sortType && column.sortType !== ColumnSortType.NONE;
2831
+ }
2832
+ // ── Selection ──
2833
+ isSelected(row) {
2834
+ return this.selectedIds.has(this.dataSource.getID(row));
2835
+ }
2836
+ toggleRow(row) {
2837
+ const id = this.dataSource.getID(row);
2838
+ const mode = this.dataSource.selectionMode ?? 'none';
2839
+ if (mode === 'single') {
2840
+ this.selectedIds.clear();
2841
+ this.selectedIds.add(id);
2842
+ }
2843
+ else if (mode === 'multi') {
2844
+ if (this.selectedIds.has(id)) {
2845
+ this.selectedIds.delete(id);
2846
+ }
2847
+ else {
2848
+ this.selectedIds.add(id);
2849
+ }
2850
+ }
2851
+ this.emitSelection();
2852
+ }
2853
+ toggleAll() {
2854
+ if (this.selectedIds.size === this.filteredItems.length) {
2855
+ this.selectedIds.clear();
2856
+ }
2857
+ else {
2858
+ this.filteredItems.forEach(row => this.selectedIds.add(this.dataSource.getID(row)));
2859
+ }
2860
+ this.emitSelection();
2861
+ }
2862
+ get allSelected() {
2863
+ return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
2864
+ }
2865
+ get hasSelection() {
2866
+ return (this.dataSource.selectionMode ?? 'none') !== 'none';
2867
+ }
2868
+ get isMultiSelect() {
2869
+ return this.dataSource.selectionMode === 'multi';
2870
+ }
2871
+ // ── Row interaction ──
2872
+ onRowClick(row) {
2873
+ this.dataSource.onRowClick?.(row);
2874
+ this.rowClick.emit(row);
2875
+ }
2876
+ // ── Pagination ──
2877
+ loadMoreRows() {
2878
+ if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
2879
+ return;
2880
+ this.loadingMoreRows = true;
2881
+ const promise = (this.searchValue.length > 0 && this.dataSource.searchForAdditionalItems)
2882
+ ? this.dataSource.searchForAdditionalItems(this.searchValue)
2883
+ : this.dataSource.loadAdditionalRows();
2884
+ promise
2885
+ .then(rows => this.processLoadedRows(rows))
2886
+ .catch(() => this.loadingMoreRows = false);
2887
+ }
2888
+ get showLoadMore() {
2889
+ const mode = this.dataSource.paginationMode ?? 'load-more';
2890
+ const strategy = this.dataSource.paginationStrategy;
2891
+ const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
2892
+ return mode === 'load-more' && hasMore;
2893
+ }
2894
+ // ── Template helpers ──
2895
+ isTemplateRef(value) {
2896
+ return value instanceof TemplateRef;
2897
+ }
2898
+ getCellValue(column, row) {
2899
+ if (typeof column.cell === 'function')
2900
+ return column.cell(row);
2901
+ return '';
2902
+ }
2903
+ trackByID = (_index, row) => {
2904
+ return this.dataSource.getID(row);
2905
+ };
2906
+ trackByKey = (_index, column) => {
2907
+ return column.key;
2908
+ };
2909
+ // ── Table CSS classes ──
2910
+ get tableClasses() {
2911
+ return 'w-full border-collapse overflow-y-hidden';
2912
+ }
2913
+ get hasRowActions() {
2914
+ return !!this.dataSource.rowActions && this.dataSource.rowActions.length > 0;
2915
+ }
2916
+ getVisibleActions(row) {
2917
+ return (this.dataSource.rowActions ?? []).filter(a => !a.isVisible || a.isVisible(row));
2918
+ }
2919
+ get totalColumnCount() {
2920
+ let count = this.dataSource.columns.length;
2921
+ if (this.hasSelection)
2922
+ count++;
2923
+ if (this.hasRowActions)
2924
+ count++;
2925
+ return count;
2926
+ }
2927
+ // ── Skeleton rows for loading ──
2928
+ get skeletonRows() {
2929
+ return Array.from({ length: 5 });
2930
+ }
2931
+ // ── Private ──
2932
+ applyFilterAndSort(searchForItems) {
2933
+ let items = this.dataSource.dataRows.value;
2934
+ if (this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
2935
+ const term = this.searchValue.toLowerCase();
2936
+ items = items.filter(row => this.dataSource.isInSearch(row, term));
2937
+ }
2938
+ items = this.applySorting(items);
2939
+ this.filteredItems = items;
2940
+ if (searchForItems) {
2941
+ this.loadMoreRows();
2942
+ }
2943
+ }
2944
+ applySorting(items) {
2945
+ if (!this.currentSort)
2946
+ return items;
2947
+ const column = this.dataSource.columns.find(c => c.key === this.currentSort.columnKey);
2948
+ if (!column || !column.sortType || column.sortType === ColumnSortType.NONE)
2949
+ return items;
2950
+ const getValue = column.getRawValueToSort ?? ((row) => {
2951
+ if (typeof column.cell === 'function')
2952
+ return column.cell(row);
2953
+ return '';
2954
+ });
2955
+ const dir = this.currentSort.direction === 'asc' ? 1 : -1;
2956
+ return [...items].sort((a, b) => {
2957
+ const va = getValue(a);
2958
+ const vb = getValue(b);
2959
+ if (va == null && vb == null)
2960
+ return 0;
2961
+ if (va == null)
2962
+ return 1;
2963
+ if (vb == null)
2964
+ return -1;
2965
+ switch (column.sortType) {
2966
+ case ColumnSortType.ALPHABETICAL:
2967
+ return String(va).localeCompare(String(vb)) * dir;
2968
+ case ColumnSortType.NUMERICAL:
2969
+ return (Number(va) - Number(vb)) * dir;
2970
+ case ColumnSortType.DATE:
2971
+ return (new Date(va).getTime() - new Date(vb).getTime()) * dir;
2972
+ default:
2973
+ return 0;
2974
+ }
2975
+ });
2976
+ }
2977
+ processLoadedRows(rows) {
2978
+ const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
2979
+ this.dataSource.dataRows.next(merged);
2980
+ this.loadingMoreRows = false;
2981
+ this.applyFilterAndSort(false);
2982
+ }
2983
+ emitSelection() {
2984
+ const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
2985
+ this.dataSource.selectedRows?.next(rows);
2986
+ this.selectionChange.emit(rows);
2987
+ }
2988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
2989
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.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 <div *ngIf=\"dataSource.canSearch\">\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-gray-300 bg-gray-100 px-3 py-1.5 text-sm 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 table\"\n />\n </div>\n <ng-container *ngIf=\"dataSource.toolbarTemplate\" [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\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>\n <!-- Selection checkbox column header -->\n <th *ngIf=\"hasSelection\" class=\"w-10 text-center text-sm bg-gray-50 px-2 py-2\">\n <label *ngIf=\"isMultiSelect\">\n <input\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n </th>\n\n <!-- Data columns -->\n <th\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\n class=\"text-sm px-4 py-2\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-gray-100]=\"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 [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\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 <ng-container *ngIf=\"isTemplateRef(column.header); else stringHeader\">\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n </ng-container>\n <ng-template #stringHeader>\n <span>{{ column.header }}</span>\n </ng-template>\n <span *ngIf=\"isSortable(column)\" class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n </span>\n </th>\n\n <!-- Actions column header -->\n <th *ngIf=\"hasRowActions\" class=\"text-sm px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Loading state -->\n <ng-container *ngIf=\"dataSource.isDataLoading; else content\">\n <tr *ngFor=\"let _ of skeletonRows\" class=\"animate-pulse\">\n <td *ngIf=\"hasSelection\" class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-gray-200\"></div></td>\n <td *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\" class=\"px-4 py-3\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n >\n <div class=\"h-4 w-3/4 rounded bg-gray-200\"></div>\n </td>\n <td *ngIf=\"hasRowActions\" class=\"px-4 py-3\"><div class=\"h-4 w-16 rounded bg-gray-200\"></div></td>\n </tr>\n </ng-container>\n\n <!-- Content -->\n <ng-template #content>\n <!-- Empty state -->\n <tr *ngIf=\"filteredItems.length === 0\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n <ng-container *ngIf=\"dataSource.emptyTemplate; else defaultEmpty\">\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"flex flex-col items-center gap-2 text-gray-400\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data rows -->\n <tr\n *ngFor=\"let row of filteredItems; trackBy: trackByID; let odd = odd; let last = last\"\n class=\"bg-white transition-colors duration-150\"\n [class.bg-brand-50]=\"isSelected(row)\"\n [class.bg-gray-50]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-gray-100]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-gray-200]=\"!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 <td *ngIf=\"hasSelection\" class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n *ngIf=\"isMultiSelect; else radioTpl\"\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n <ng-template #radioTpl>\n <input\n type=\"radio\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n name=\"mn-table-single-select\"\n />\n </ng-template>\n </label>\n </td>\n\n <!-- Data cells -->\n <td\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\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 [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n [style.width]=\"column.width ?? null\"\n >\n <ng-container *ngIf=\"isTemplateRef(column.cell); else stringCell\">\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n </ng-container>\n <ng-template #stringCell>{{ getCellValue(column, row) }}</ng-template>\n </td>\n\n <!-- Row actions -->\n <td *ngIf=\"hasRowActions\" class=\"text-right px-4 py-2\">\n <div class=\"inline-flex items-center justify-end gap-2\">\n <button\n *ngFor=\"let action of getVisibleActions(row)\"\n class=\"px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n [ngClass]=\"action.cssClass ?? ''\"\n [disabled]=\"action.isDisabled ? action.isDisabled(row) : false\"\n (click)=\"$event.stopPropagation(); action.onClick(row)\"\n [attr.aria-label]=\"action.label\"\n >\n <span *ngIf=\"action.icon\" [innerHTML]=\"action.icon\"></span>\n <span *ngIf=\"action.label\">{{ action.label }}</span>\n </button>\n </div>\n </td>\n </tr>\n </ng-template>\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n<div *ngIf=\"showLoadMore\" class=\"flex justify-center py-4\">\n <button\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 <span *ngIf=\"loadingMoreRows\" class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n Load more\n </button>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
2990
+ }
2991
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
2992
+ type: Component,
2993
+ args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgForOf, NgIf, NgTemplateOutlet], template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n <div *ngIf=\"dataSource.canSearch\">\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-gray-300 bg-gray-100 px-3 py-1.5 text-sm 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 table\"\n />\n </div>\n <ng-container *ngIf=\"dataSource.toolbarTemplate\" [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\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>\n <!-- Selection checkbox column header -->\n <th *ngIf=\"hasSelection\" class=\"w-10 text-center text-sm bg-gray-50 px-2 py-2\">\n <label *ngIf=\"isMultiSelect\">\n <input\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n </th>\n\n <!-- Data columns -->\n <th\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\n class=\"text-sm px-4 py-2\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-gray-100]=\"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 [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\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 <ng-container *ngIf=\"isTemplateRef(column.header); else stringHeader\">\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n </ng-container>\n <ng-template #stringHeader>\n <span>{{ column.header }}</span>\n </ng-template>\n <span *ngIf=\"isSortable(column)\" class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n </span>\n </th>\n\n <!-- Actions column header -->\n <th *ngIf=\"hasRowActions\" class=\"text-sm px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Loading state -->\n <ng-container *ngIf=\"dataSource.isDataLoading; else content\">\n <tr *ngFor=\"let _ of skeletonRows\" class=\"animate-pulse\">\n <td *ngIf=\"hasSelection\" class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-gray-200\"></div></td>\n <td *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\" class=\"px-4 py-3\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n >\n <div class=\"h-4 w-3/4 rounded bg-gray-200\"></div>\n </td>\n <td *ngIf=\"hasRowActions\" class=\"px-4 py-3\"><div class=\"h-4 w-16 rounded bg-gray-200\"></div></td>\n </tr>\n </ng-container>\n\n <!-- Content -->\n <ng-template #content>\n <!-- Empty state -->\n <tr *ngIf=\"filteredItems.length === 0\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n <ng-container *ngIf=\"dataSource.emptyTemplate; else defaultEmpty\">\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"flex flex-col items-center gap-2 text-gray-400\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data rows -->\n <tr\n *ngFor=\"let row of filteredItems; trackBy: trackByID; let odd = odd; let last = last\"\n class=\"bg-white transition-colors duration-150\"\n [class.bg-brand-50]=\"isSelected(row)\"\n [class.bg-gray-50]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-gray-100]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-gray-200]=\"!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 <td *ngIf=\"hasSelection\" class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n *ngIf=\"isMultiSelect; else radioTpl\"\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n <ng-template #radioTpl>\n <input\n type=\"radio\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n name=\"mn-table-single-select\"\n />\n </ng-template>\n </label>\n </td>\n\n <!-- Data cells -->\n <td\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\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 [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n [style.width]=\"column.width ?? null\"\n >\n <ng-container *ngIf=\"isTemplateRef(column.cell); else stringCell\">\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n </ng-container>\n <ng-template #stringCell>{{ getCellValue(column, row) }}</ng-template>\n </td>\n\n <!-- Row actions -->\n <td *ngIf=\"hasRowActions\" class=\"text-right px-4 py-2\">\n <div class=\"inline-flex items-center justify-end gap-2\">\n <button\n *ngFor=\"let action of getVisibleActions(row)\"\n class=\"px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n [ngClass]=\"action.cssClass ?? ''\"\n [disabled]=\"action.isDisabled ? action.isDisabled(row) : false\"\n (click)=\"$event.stopPropagation(); action.onClick(row)\"\n [attr.aria-label]=\"action.label\"\n >\n <span *ngIf=\"action.icon\" [innerHTML]=\"action.icon\"></span>\n <span *ngIf=\"action.label\">{{ action.label }}</span>\n </button>\n </div>\n </td>\n </tr>\n </ng-template>\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n<div *ngIf=\"showLoadMore\" class=\"flex justify-center py-4\">\n <button\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 <span *ngIf=\"loadingMoreRows\" class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n Load more\n </button>\n</div>\n" }]
2994
+ }], propDecorators: { dataSource: [{
2995
+ type: Input
2996
+ }], sortChange: [{
2997
+ type: Output
2998
+ }], selectionChange: [{
2999
+ type: Output
3000
+ }], rowClick: [{
3001
+ type: Output
3002
+ }] } });
3003
+
3004
+ class MnFormBodyComponent {
3005
+ fb;
3006
+ config;
3007
+ modalRef;
3008
+ hideFooter = false;
3009
+ form;
3010
+ rows = [];
3011
+ fieldGroups = [];
3012
+ isSubmitting = false;
3013
+ FieldKind = FieldKind;
3014
+ ModalCloseReason = ModalCloseReason;
3015
+ /** Cross-field validation errors: { fieldKey: errorMessage } */
3016
+ formErrors = {};
3017
+ /** Track which fields are currently visible (for conditional fields) */
3018
+ fieldVisibility = {};
3019
+ /** Track loading state per field for async data sources */
3020
+ fieldLoading = {};
3021
+ /** Dynamic options loaded from data sources */
3022
+ fieldOptions = {};
3023
+ valueChangesSubscription;
3024
+ constructor(fb) {
3025
+ this.fb = fb;
3026
+ }
3027
+ asField(field) {
3028
+ return field;
3029
+ }
3030
+ asKey(key) {
3031
+ return key;
3032
+ }
3033
+ asAny(val) {
3034
+ return val;
3035
+ }
3036
+ hasRequiredValidator(field) {
3037
+ const validators = field.validators;
3038
+ if (!validators)
3039
+ return false;
3040
+ // Check if Validators.required is in the array by testing a dummy control
3041
+ const control = this.fb.control(null, validators);
3042
+ const errors = control.errors;
3043
+ return errors != null && 'required' in errors;
3044
+ }
3045
+ /** Store table data sources keyed by field key for template access */
3046
+ tableDataSources = {};
3047
+ /** Resolved i18n labels with defaults */
3048
+ get labels() {
3049
+ const i = this.config;
3050
+ const i18n = i.i18n || {};
3051
+ return {
3052
+ submit: i18n.submit || 'Submit',
3053
+ cancel: i18n.cancel || 'Cancel',
3054
+ submitting: i18n.submitting || 'Submitting...',
3055
+ selectPlaceholder: i18n.selectPlaceholder || 'Select...',
3056
+ loading: i18n.loading || 'Loading...',
3057
+ fileUploadPrompt: i18n.fileUploadPrompt || 'Click or drag files here',
3058
+ };
3059
+ }
3060
+ ngOnInit() {
3061
+ this.initializeForm();
3062
+ this.buildRows();
3063
+ this.initializeVisibility();
3064
+ this.initializeDataSources();
3065
+ this.initializeTableFields();
3066
+ this.subscribeToValueChanges();
3067
+ }
3068
+ ngOnDestroy() {
3069
+ this.valueChangesSubscription?.unsubscribe();
3070
+ }
3071
+ initializeForm() {
3072
+ const formControls = {};
3073
+ this.config.fields.forEach(field => {
3074
+ const fieldConfig = field;
3075
+ let initialValue = this.config.initialValue?.[field.key] ?? null;
3076
+ // Handle checkbox default values
3077
+ if (field.kind === FieldKind.CHECKBOX && initialValue === null) {
3078
+ initialValue = (fieldConfig.defaultValue ?? false);
3079
+ }
3080
+ const validators = fieldConfig.validators || [];
3081
+ const asyncValidators = fieldConfig.asyncValidators || [];
3082
+ formControls[field.key] = [initialValue, validators, asyncValidators];
3083
+ });
3084
+ this.form = this.fb.group(formControls);
3085
+ // Apply Angular FormGroup-level validators
3086
+ if (this.config.groupValidators && this.config.groupValidators.length > 0) {
3087
+ this.form.setValidators(this.config.groupValidators);
3088
+ this.form.updateValueAndValidity();
3089
+ }
3090
+ // Apply disabled/readOnly state to controls
3091
+ this.config.fields.forEach(field => {
3092
+ const fieldAny = field;
3093
+ const control = this.form.get(field.key);
3094
+ if (control && (fieldAny.disabled || fieldAny.readOnly)) {
3095
+ control.disable();
3096
+ }
3097
+ });
3098
+ }
3099
+ isFieldReadOnly(field) {
3100
+ return field.readOnly === true;
3101
+ }
3102
+ isFieldDisabled(field) {
3103
+ return field.disabled === true;
3104
+ }
3105
+ /** Track which field groups are currently visible */
3106
+ groupVisibility = {};
3107
+ buildRows() {
3108
+ if (this.config.rows && this.config.rows.length > 0) {
3109
+ this.rows = this.config.rows;
3110
+ }
3111
+ else if (!this.config.fieldGroups || this.config.fieldGroups.length === 0) {
3112
+ // Fallback: each field gets its own full-width row
3113
+ this.rows = this.config.fields.map(field => ({
3114
+ columns: 1,
3115
+ fields: [{ field, span: 1 }],
3116
+ }));
3117
+ }
3118
+ // Build field groups
3119
+ this.fieldGroups = this.config.fieldGroups || [];
3120
+ // Initialize group visibility
3121
+ this.initializeGroupVisibility();
3122
+ }
3123
+ initializeGroupVisibility() {
3124
+ this.fieldGroups.forEach((group, index) => {
3125
+ const key = group.title || `group-${index}`;
3126
+ const isVisible = group.visible ? group.visible(this.form.value) : true;
3127
+ this.groupVisibility[key] = isVisible;
3128
+ // If group is hidden, clear validators on its fields
3129
+ if (!isVisible) {
3130
+ group.fields.forEach(field => {
3131
+ const control = this.form.get(field.key);
3132
+ if (control) {
3133
+ control.clearValidators();
3134
+ control.updateValueAndValidity({ emitEvent: false });
3135
+ }
3136
+ });
3137
+ }
3138
+ });
3139
+ }
3140
+ updateGroupVisibility() {
3141
+ const formValue = this.form.value;
3142
+ this.fieldGroups.forEach((group, index) => {
3143
+ const key = group.title || `group-${index}`;
3144
+ const wasVisible = this.groupVisibility[key];
3145
+ const isVisible = group.visible ? group.visible(formValue) : true;
3146
+ this.groupVisibility[key] = isVisible;
3147
+ if (!isVisible && wasVisible) {
3148
+ // Group became hidden — clear validators on its fields
3149
+ group.fields.forEach(field => {
3150
+ const control = this.form.get(field.key);
3151
+ if (control) {
3152
+ control.clearValidators();
3153
+ control.updateValueAndValidity({ emitEvent: false });
3154
+ }
3155
+ });
3156
+ }
3157
+ else if (isVisible && !wasVisible) {
3158
+ // Group became visible — restore validators
3159
+ group.fields.forEach(field => {
3160
+ const fieldAny = field;
3161
+ const validators = fieldAny.validators || [];
3162
+ const control = this.form.get(field.key);
3163
+ if (control) {
3164
+ control.setValidators(validators);
3165
+ control.updateValueAndValidity({ emitEvent: false });
3166
+ }
3167
+ });
3168
+ }
3169
+ });
3170
+ }
3171
+ isGroupVisible(group) {
3172
+ const key = group.title || `group-${this.fieldGroups.indexOf(group)}`;
3173
+ return this.groupVisibility[key] !== false;
3174
+ }
3175
+ // =========================
3176
+ // Feature 1: Conditional/Dynamic Fields
3177
+ // =========================
3178
+ initializeVisibility() {
3179
+ this.config.fields.forEach(field => {
3180
+ const key = field.key;
3181
+ const fieldAny = field;
3182
+ const isVisible = fieldAny.visible ? fieldAny.visible(this.form.value) : true;
3183
+ this.fieldVisibility[key] = isVisible;
3184
+ // If initially hidden, clear validators so they don't block submit
3185
+ if (!isVisible) {
3186
+ const control = this.form.get(key);
3187
+ if (control) {
3188
+ control.clearValidators();
3189
+ control.updateValueAndValidity({ emitEvent: false });
3190
+ }
3191
+ }
3192
+ });
3193
+ }
3194
+ updateVisibility() {
3195
+ const formValue = this.form.value;
3196
+ this.config.fields.forEach(field => {
3197
+ const key = field.key;
3198
+ const fieldAny = field;
3199
+ const wasVisible = this.fieldVisibility[key];
3200
+ const isVisible = fieldAny.visible ? fieldAny.visible(formValue) : true;
3201
+ this.fieldVisibility[key] = isVisible;
3202
+ // When a field becomes hidden, clear its validators so it doesn't block submit
3203
+ const control = this.form.get(key);
3204
+ if (control) {
3205
+ if (!isVisible && wasVisible) {
3206
+ control.clearValidators();
3207
+ control.updateValueAndValidity({ emitEvent: false });
3208
+ }
3209
+ else if (isVisible && !wasVisible) {
3210
+ // Restore validators
3211
+ const validators = fieldAny.validators || [];
3212
+ control.setValidators(validators);
3213
+ control.updateValueAndValidity({ emitEvent: false });
3214
+ }
3215
+ }
3216
+ });
3217
+ }
3218
+ isFieldVisible(field) {
3219
+ return this.fieldVisibility[field.key] !== false;
3220
+ }
3221
+ // =========================
3222
+ // Feature 2: Cross-Field Validation
3223
+ // =========================
3224
+ runFormValidators() {
3225
+ this.formErrors = {};
3226
+ if (!this.config.formValidators)
3227
+ return;
3228
+ const formValue = this.form.value;
3229
+ for (const validator of this.config.formValidators) {
3230
+ const errors = validator(formValue);
3231
+ if (errors) {
3232
+ Object.assign(this.formErrors, errors);
3233
+ }
3234
+ }
3235
+ }
3236
+ getFieldError(key) {
3237
+ return this.formErrors[key] || null;
3238
+ }
3239
+ get hasFormErrors() {
3240
+ return Object.keys(this.formErrors).length > 0;
3241
+ }
3242
+ // =========================
3243
+ // Feature 3: Async Data Sources
3244
+ // =========================
3245
+ initializeDataSources() {
3246
+ this.config.fields.forEach(field => {
3247
+ const fieldAny = field;
3248
+ if (fieldAny.dataSource) {
3249
+ this.loadFieldOptions(field.key, fieldAny.dataSource, this.form.value);
3250
+ }
3251
+ });
3252
+ }
3253
+ async loadFieldOptions(key, dataSource, formValue) {
3254
+ this.fieldLoading[key] = true;
3255
+ try {
3256
+ const options = await dataSource.load(formValue);
3257
+ this.fieldOptions[key] = options;
3258
+ }
3259
+ catch (error) {
3260
+ console.error(`Failed to load options for field '${key}':`, error);
3261
+ this.fieldOptions[key] = [];
3262
+ }
3263
+ finally {
3264
+ this.fieldLoading[key] = false;
3265
+ }
3266
+ }
3267
+ /** Get options for a field — uses dataSource options if available, otherwise static options */
3268
+ getFieldOptions(field) {
3269
+ const key = field.key;
3270
+ if (this.fieldOptions[key] !== undefined) {
3271
+ return this.fieldOptions[key];
3272
+ }
3273
+ return field.options || [];
3274
+ }
3275
+ isFieldLoading(key) {
3276
+ return this.fieldLoading[key] === true;
3277
+ }
3278
+ // =========================
3279
+ // Feature: Multi-Select Table Fields
3280
+ // =========================
3281
+ initializeTableFields() {
3282
+ this.config.fields.forEach(field => {
3283
+ if (field.kind === FieldKind.MULTI_SELECT_TABLE) {
3284
+ const tableField = field;
3285
+ const ds = tableField.tableDataSource;
3286
+ // Force multi selection mode
3287
+ ds.selectionMode = 'multi';
3288
+ this.tableDataSources[field.key] = ds;
3289
+ // Initialize form control with empty array if null
3290
+ const control = this.form.get(field.key);
3291
+ if (control && control.value === null) {
3292
+ control.setValue([], { emitEvent: false });
3293
+ }
3294
+ }
3295
+ else if (field.kind === FieldKind.SINGLE_SELECT_TABLE) {
3296
+ const tableField = field;
3297
+ const ds = tableField.tableDataSource;
3298
+ // Force single selection mode
3299
+ ds.selectionMode = 'single';
3300
+ this.tableDataSources[field.key] = ds;
3301
+ }
3302
+ });
3303
+ }
3304
+ onTableSelectionChange(field, selectedRows) {
3305
+ if (field.kind === FieldKind.SINGLE_SELECT_TABLE) {
3306
+ const tableField = field;
3307
+ const getVal = tableField.getRowValue || tableField.tableDataSource.getID;
3308
+ const value = selectedRows.length > 0 ? getVal(selectedRows[0]) : null;
3309
+ const control = this.form.get(field.key);
3310
+ if (control) {
3311
+ control.setValue(value);
3312
+ control.markAsTouched();
3313
+ }
3314
+ }
3315
+ else {
3316
+ const tableField = field;
3317
+ const getVal = tableField.getRowValue || tableField.tableDataSource.getID;
3318
+ const values = selectedRows.map(row => getVal(row));
3319
+ const control = this.form.get(field.key);
3320
+ if (control) {
3321
+ control.setValue(values);
3322
+ control.markAsTouched();
3323
+ }
3324
+ }
3325
+ }
3326
+ // =========================
3327
+ // Feature: Rating Fields
3328
+ // =========================
3329
+ getRatingRange(field) {
3330
+ const max = field.max || 5;
3331
+ return Array.from({ length: max }, (_, i) => i + 1);
3332
+ }
3333
+ setRating(field, value) {
3334
+ const control = this.form.get(field.key);
3335
+ if (control) {
3336
+ control.setValue(value);
3337
+ control.markAsTouched();
3338
+ }
3339
+ }
3340
+ getRatingValue(field) {
3341
+ return this.form.get(field.key)?.value || 0;
3342
+ }
3343
+ // =========================
3344
+ // Feature: Slider Fields
3345
+ // =========================
3346
+ onSliderChange(field, event) {
3347
+ const input = event.target;
3348
+ const value = parseFloat(input.value);
3349
+ const control = this.form.get(field.key);
3350
+ if (control) {
3351
+ control.setValue(value);
3352
+ control.markAsTouched();
3353
+ }
3354
+ }
3355
+ getSliderValue(field) {
3356
+ const f = field;
3357
+ return this.form.get(field.key)?.value ?? f.min ?? 0;
3358
+ }
3359
+ // =========================
3360
+ // Feature: Color Fields
3361
+ // =========================
3362
+ onColorChange(field, event) {
3363
+ const input = event.target;
3364
+ const control = this.form.get(field.key);
3365
+ if (control) {
3366
+ control.setValue(input.value);
3367
+ control.markAsTouched();
3368
+ }
3369
+ }
3370
+ setColorFromSwatch(field, color) {
3371
+ const control = this.form.get(field.key);
3372
+ if (control) {
3373
+ control.setValue(color);
3374
+ control.markAsTouched();
3375
+ }
3376
+ }
3377
+ getColorValue(field) {
3378
+ return this.form.get(field.key)?.value || '#000000';
3379
+ }
3380
+ // =========================
3381
+ // Value Changes Subscription
3382
+ // =========================
3383
+ subscribeToValueChanges() {
3384
+ // Initialize previousFormValue with current form state to avoid false "changed" on first emission
3385
+ this.previousFormValue = { ...this.form.value };
3386
+ this.valueChangesSubscription = this.form.valueChanges.subscribe(formValue => {
3387
+ // Update conditional visibility
3388
+ this.updateVisibility();
3389
+ // Update group visibility
3390
+ this.updateGroupVisibility();
3391
+ // Run cross-field validators
3392
+ this.runFormValidators();
3393
+ // Reload data sources that depend on changed fields
3394
+ this.reloadDependentDataSources(formValue);
3395
+ });
3396
+ }
3397
+ previousFormValue = {};
3398
+ reloadDependentDataSources(formValue) {
3399
+ this.config.fields.forEach(field => {
3400
+ const fieldAny = field;
3401
+ const dataSource = fieldAny.dataSource;
3402
+ if (!dataSource?.dependsOn)
3403
+ return;
3404
+ // Check if any dependency changed
3405
+ const changed = dataSource.dependsOn.some((depKey) => {
3406
+ return formValue[depKey] !== this.previousFormValue[depKey];
3407
+ });
3408
+ if (changed) {
3409
+ this.loadFieldOptions(field.key, dataSource, formValue);
3410
+ }
3411
+ });
3412
+ this.previousFormValue = { ...formValue };
3413
+ }
3414
+ getGridColumns(row) {
3415
+ const cols = row.columns || 1;
3416
+ return `repeat(${cols}, 1fr)`;
3417
+ }
3418
+ getGridSpan(rowField) {
3419
+ const span = rowField.span || 1;
3420
+ return span > 1 ? `span ${span}` : '';
3421
+ }
3422
+ // =========================
3423
+ // Feature: File Upload Fields
3424
+ // =========================
3425
+ /** Store selected files keyed by field key */
3426
+ fileSelections = {};
3427
+ onFileChange(field, event) {
3428
+ const input = event.target;
3429
+ const fileField = field;
3430
+ const files = Array.from(input.files || []);
3431
+ const key = field.key;
3432
+ // Validate file size
3433
+ if (fileField.maxSize) {
3434
+ const oversized = files.filter(f => f.size > fileField.maxSize);
3435
+ if (oversized.length > 0) {
3436
+ // Remove oversized files
3437
+ const valid = files.filter(f => f.size <= fileField.maxSize);
3438
+ this.fileSelections[key] = fileField.multiple
3439
+ ? [...(this.fileSelections[key] || []), ...valid]
3440
+ : valid.slice(0, 1);
3441
+ }
3442
+ else {
3443
+ this.fileSelections[key] = fileField.multiple
3444
+ ? [...(this.fileSelections[key] || []), ...files]
3445
+ : files.slice(0, 1);
3446
+ }
3447
+ }
3448
+ else {
3449
+ this.fileSelections[key] = fileField.multiple
3450
+ ? [...(this.fileSelections[key] || []), ...files]
3451
+ : files.slice(0, 1);
3452
+ }
3453
+ // Enforce maxFiles
3454
+ if (fileField.maxFiles && this.fileSelections[key].length > fileField.maxFiles) {
3455
+ this.fileSelections[key] = this.fileSelections[key].slice(0, fileField.maxFiles);
3456
+ }
3457
+ const control = this.form.get(key);
3458
+ if (control) {
3459
+ control.setValue(this.fileSelections[key]);
3460
+ control.markAsTouched();
3461
+ }
3462
+ // Reset input so same file can be re-selected
3463
+ input.value = '';
3464
+ }
3465
+ removeFile(key, index) {
3466
+ this.fileSelections[key] = (this.fileSelections[key] || []).filter((_, i) => i !== index);
3467
+ const control = this.form.get(key);
3468
+ if (control) {
3469
+ control.setValue(this.fileSelections[key].length > 0 ? this.fileSelections[key] : null);
3470
+ }
3471
+ }
3472
+ getSelectedFiles(key) {
3473
+ return this.fileSelections[key] || [];
3474
+ }
3475
+ formatFileSize(bytes) {
3476
+ if (bytes < 1024)
3477
+ return bytes + ' B';
3478
+ if (bytes < 1024 * 1024)
3479
+ return (bytes / 1024).toFixed(1) + ' KB';
3480
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
3481
+ }
3482
+ getFileError(field) {
3483
+ const control = this.form.get(field.key);
3484
+ if (!control?.errors)
3485
+ return 'This field is required';
3486
+ if (control.errors['required'])
3487
+ return 'Please select a file';
3488
+ return 'Invalid file';
3489
+ }
3490
+ async submit() {
3491
+ // Run cross-field validators before submit
3492
+ this.runFormValidators();
3493
+ if (this.form.invalid || this.isSubmitting || this.hasFormErrors) {
3494
+ this.form.markAllAsTouched();
3495
+ return;
3496
+ }
3497
+ if (this.config.submitMode === SubmitMode.ONCE) {
3498
+ this.isSubmitting = true;
3499
+ }
3500
+ const formValue = this.form.value;
3501
+ try {
3502
+ if (this.config.onComplete) {
3503
+ await this.config.onComplete.handle(formValue);
3504
+ }
3505
+ this.modalRef.close(formValue);
3506
+ }
3507
+ catch (error) {
3508
+ // Always allow retry on error, regardless of submit mode
3509
+ this.isSubmitting = false;
3510
+ console.error('Form submission error:', error);
3511
+ }
3512
+ }
3513
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, deps: [{ token: i1$2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component });
3514
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter" }, ngImport: i0, template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-xl text-base text-gray-900 bg-white transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key))\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label\n })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step\n })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical'\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections\n })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n &#9733;\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">&times;</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n\n <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"fieldGroups.length === 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }] });
3515
+ }
3516
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
3517
+ type: Component,
3518
+ args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable], template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-xl text-base text-gray-900 bg-white transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key))\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label\n })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step\n })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical'\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections\n })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n &#9733;\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">&times;</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n\n <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"fieldGroups.length === 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"] }]
3519
+ }], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
3520
+ type: Input
3521
+ }], modalRef: [{
3522
+ type: Input
3523
+ }], hideFooter: [{
3524
+ type: Input
3525
+ }] } });
3526
+
3527
+ class MnWizardBodyComponent {
3528
+ cdr;
3529
+ config;
3530
+ modalRef;
3531
+ formBodies;
3532
+ currentStepId;
3533
+ visitedStepIds = [];
3534
+ isCurrentStepValid = true;
3535
+ isCompleting = false;
3536
+ wizardErrors = {};
3537
+ /** Resolved i18n labels with defaults */
3538
+ get labels() {
3539
+ const i18n = this.config.i18n || {};
3540
+ return {
3541
+ next: i18n.next || 'Next',
3542
+ back: i18n.back || 'Back',
3543
+ close: i18n.close || 'Close',
3544
+ complete: i18n.complete || 'Complete',
3545
+ completing: i18n.completing || 'Completing...',
3546
+ };
3547
+ }
3548
+ /** Pre-built form configs keyed by step id — only for steps that have fields */
3549
+ stepFormConfigs = {};
3550
+ statusSubscription;
3551
+ formBodiesSubscription;
3552
+ constructor(cdr) {
3553
+ this.cdr = cdr;
3554
+ }
3555
+ ngOnInit() {
3556
+ // Pre-build form configs for all form-driven steps
3557
+ for (const step of this.config.steps) {
3558
+ if (step.fields && step.fields.length > 0) {
3559
+ this.stepFormConfigs[step.id] = {
3560
+ kind: ModalKind.FORM,
3561
+ fields: step.fields,
3562
+ rows: step.rows,
3563
+ fieldGroups: step.fieldGroups,
3564
+ formValidators: step.formValidators,
3565
+ groupValidators: step.groupValidators,
3566
+ initialValue: step.initialValue,
3567
+ };
3568
+ }
3569
+ }
3570
+ this.currentStepId = this.config.startStepId || this.config.steps[0]?.id;
3571
+ if (this.currentStepId) {
3572
+ this.visitedStepIds.push(this.currentStepId);
3573
+ }
3574
+ }
3575
+ ngAfterViewInit() {
3576
+ // Subscribe to form bodies list changes to track validity
3577
+ this.formBodiesSubscription = this.formBodies.changes.subscribe(() => {
3578
+ this.trackCurrentStepValidity();
3579
+ });
3580
+ // Initial validity check
3581
+ this.trackCurrentStepValidity();
3582
+ }
3583
+ ngOnDestroy() {
3584
+ this.statusSubscription?.unsubscribe();
3585
+ this.formBodiesSubscription?.unsubscribe();
3586
+ }
3587
+ asAny(val) {
3588
+ return val;
3589
+ }
3590
+ isTextBody(step) {
3591
+ return typeof step.body === 'string' || typeof step.body === 'number';
3592
+ }
3593
+ /** Get visible steps (filtered by visibility condition) */
3594
+ get visibleSteps() {
3595
+ return this.config.steps.filter(s => this.isStepVisible(s));
3596
+ }
3597
+ isStepVisible(step) {
3598
+ if (!step.visible)
3599
+ return true;
3600
+ return step.visible(this.getAggregatedData());
3601
+ }
3602
+ get currentStep() {
3603
+ return this.config.steps.find(s => s.id === this.currentStepId);
3604
+ }
3605
+ get currentVisibleIndex() {
3606
+ return this.visibleSteps.findIndex(s => s.id === this.currentStepId);
3607
+ }
3608
+ get currentStepIndex() {
3609
+ return this.config.steps.findIndex(s => s.id === this.currentStepId);
3610
+ }
3611
+ get canGoBack() {
3612
+ return this.currentVisibleIndex > 0;
3613
+ }
3614
+ get canGoNext() {
3615
+ return this.currentVisibleIndex < this.visibleSteps.length - 1;
3616
+ }
3617
+ get isLastStep() {
3618
+ return this.currentVisibleIndex === this.visibleSteps.length - 1;
3619
+ }
3620
+ get isFreeFlow() {
3621
+ return this.config.flow === WizardFlowMode.FREE;
3622
+ }
3623
+ canNavigateToStep(step) {
3624
+ if (!this.isFreeFlow)
3625
+ return false;
3626
+ return this.isStepVisible(step);
3627
+ }
3628
+ async goToStep(step) {
3629
+ if (!this.canNavigateToStep(step))
3630
+ return;
3631
+ if (step.id === this.currentStepId)
3632
+ return;
3633
+ const previousStepId = this.currentStepId;
3634
+ this.currentStepId = step.id;
3635
+ if (!this.visitedStepIds.includes(this.currentStepId)) {
3636
+ this.visitedStepIds.push(this.currentStepId);
3637
+ }
3638
+ await this.notifyStepChange(previousStepId, NavigationDirection.DIRECT);
3639
+ this.trackCurrentStepValidity();
3640
+ }
3641
+ /** Find the MnFormBodyComponent for the current step */
3642
+ getCurrentFormBody() {
3643
+ if (!this.formBodies)
3644
+ return undefined;
3645
+ const stepIndex = this.currentStepIndex;
3646
+ // formBodies only contains entries for steps that have stepFormConfigs
3647
+ // We need to find which form body index corresponds to the current step
3648
+ const formStepIds = this.config.steps
3649
+ .filter(s => this.stepFormConfigs[s.id])
3650
+ .map(s => s.id);
3651
+ const formIndex = formStepIds.indexOf(this.currentStepId);
3652
+ if (formIndex === -1)
3653
+ return undefined;
3654
+ return this.formBodies.toArray()[formIndex];
3655
+ }
3656
+ trackCurrentStepValidity() {
3657
+ // Unsubscribe from previous
3658
+ this.statusSubscription?.unsubscribe();
3659
+ this.statusSubscription = undefined;
3660
+ const formBody = this.getCurrentFormBody();
3661
+ if (formBody && formBody.form) {
3662
+ this.isCurrentStepValid = formBody.form.valid;
3663
+ this.statusSubscription = formBody.form.statusChanges.subscribe(() => {
3664
+ this.isCurrentStepValid = formBody.form.valid;
3665
+ this.cdr.detectChanges();
3666
+ });
3667
+ }
3668
+ else {
3669
+ this.isCurrentStepValid = true;
3670
+ }
3671
+ this.cdr.detectChanges();
3672
+ }
3673
+ async next() {
3674
+ if (!this.canGoNext)
3675
+ return;
3676
+ const currentStep = this.currentStep;
3677
+ // Validate embedded form if present
3678
+ const formBody = this.getCurrentFormBody();
3679
+ if (formBody && formBody.form.invalid) {
3680
+ formBody.form.markAllAsTouched();
3681
+ return;
3682
+ }
3683
+ if (currentStep?.guard?.canExit) {
3684
+ const canExit = await currentStep.guard.canExit();
3685
+ if (!canExit)
3686
+ return;
3687
+ }
3688
+ if (currentStep?.validators) {
3689
+ for (const validator of currentStep.validators) {
3690
+ const result = await validator.validate();
3691
+ if (result.status === 'invalid')
3692
+ return;
3693
+ }
3694
+ }
3695
+ // Find next visible step
3696
+ const nextStep = this.visibleSteps[this.currentVisibleIndex + 1];
3697
+ if (!nextStep)
3698
+ return;
3699
+ if (nextStep.guard?.canEnter) {
3700
+ const canEnter = await nextStep.guard.canEnter();
3701
+ if (!canEnter)
3702
+ return;
3703
+ }
3704
+ const previousStepId = this.currentStepId;
3705
+ this.currentStepId = nextStep.id;
3706
+ if (!this.visitedStepIds.includes(this.currentStepId)) {
3707
+ this.visitedStepIds.push(this.currentStepId);
3708
+ }
3709
+ await this.notifyStepChange(previousStepId, NavigationDirection.FORWARD);
3710
+ this.trackCurrentStepValidity();
3711
+ }
3712
+ async back() {
3713
+ if (!this.canGoBack) {
3714
+ // Call onCancel handler if configured
3715
+ if (this.config.onCancel) {
3716
+ await this.config.onCancel(ModalCloseReason.CANCELLED);
3717
+ }
3718
+ this.modalRef.dismiss(ModalCloseReason.CANCELLED);
3719
+ return;
3720
+ }
3721
+ const prevStep = this.visibleSteps[this.currentVisibleIndex - 1];
3722
+ const previousStepId = this.currentStepId;
3723
+ this.currentStepId = prevStep.id;
3724
+ await this.notifyStepChange(previousStepId, NavigationDirection.BACKWARD);
3725
+ this.trackCurrentStepValidity();
3726
+ }
3727
+ async complete() {
3728
+ if (this.isCompleting)
3729
+ return;
3730
+ const currentStep = this.currentStep;
3731
+ // Validate embedded form if present
3732
+ const formBody = this.getCurrentFormBody();
3733
+ if (formBody && formBody.form.invalid) {
3734
+ formBody.form.markAllAsTouched();
3735
+ return;
3736
+ }
3737
+ if (currentStep?.validators) {
3738
+ for (const validator of currentStep.validators) {
3739
+ const result = await validator.validate();
3740
+ if (result.status === 'invalid')
3741
+ return;
3742
+ }
3743
+ }
3744
+ this.isCompleting = true;
3745
+ this.wizardErrors = {};
3746
+ this.cdr.detectChanges();
3747
+ try {
3748
+ const aggregatedData = this.getAggregatedData();
3749
+ // Run cross-step validators (onBeforeComplete)
3750
+ if (this.config.onBeforeComplete) {
3751
+ for (const validator of this.config.onBeforeComplete) {
3752
+ const errors = await validator(aggregatedData);
3753
+ if (errors) {
3754
+ this.wizardErrors = { ...this.wizardErrors, ...errors };
3755
+ }
3756
+ }
3757
+ if (Object.keys(this.wizardErrors).length > 0) {
3758
+ this.isCompleting = false;
3759
+ this.cdr.detectChanges();
3760
+ return;
3761
+ }
3762
+ }
3763
+ const result = {
3764
+ status: ModalCloseReason.COMPLETED,
3765
+ visitedStepIds: this.visitedStepIds,
3766
+ payload: aggregatedData,
3767
+ };
3768
+ if (this.config.onComplete) {
3769
+ await this.config.onComplete.handle(result);
3770
+ }
3771
+ this.modalRef.close(result);
3772
+ }
3773
+ catch (error) {
3774
+ this.isCompleting = false;
3775
+ this.cdr.detectChanges();
3776
+ console.error('Wizard completion error:', error);
3777
+ }
3778
+ }
3779
+ /** Collect form data from all form-driven steps, namespaced by step ID */
3780
+ getAggregatedData() {
3781
+ const aggregated = {};
3782
+ if (!this.formBodies)
3783
+ return aggregated;
3784
+ const formStepIds = this.config.steps
3785
+ .filter(s => this.stepFormConfigs[s.id])
3786
+ .map(s => s.id);
3787
+ this.formBodies.toArray().forEach((fb, index) => {
3788
+ if (fb.form && formStepIds[index]) {
3789
+ aggregated[formStepIds[index]] = { ...fb.form.value };
3790
+ }
3791
+ });
3792
+ return aggregated;
3793
+ }
3794
+ async notifyStepChange(previousStepId, direction) {
3795
+ if (this.config.onStepChange) {
3796
+ await this.config.onStepChange.handle({
3797
+ previousStepId,
3798
+ currentStepId: this.currentStepId,
3799
+ direction,
3800
+ });
3801
+ }
3802
+ }
3803
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3804
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\">\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ canGoBack ? labels.back : labels.close }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ isCompleting ? labels.completing : labels.complete }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
3805
+ }
3806
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
3807
+ type: Component,
3808
+ args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent], template: "<div class=\"flex flex-col gap-6\">\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ canGoBack ? labels.back : labels.close }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ isCompleting ? labels.completing : labels.complete }}\n </button>\n </div>\n</div>\n" }]
3809
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
3810
+ type: Input
3811
+ }], modalRef: [{
3812
+ type: Input
3813
+ }], formBodies: [{
3814
+ type: ViewChildren,
3815
+ args: [MnFormBodyComponent]
3816
+ }] } });
3817
+
3818
+ class MnConfirmationBodyComponent {
3819
+ config;
3820
+ modalRef;
3821
+ async confirm() {
3822
+ const result = true;
3823
+ if (this.config.confirm?.handler) {
3824
+ await this.config.confirm.handler.handle(result);
3825
+ }
3826
+ this.modalRef.close(result);
3827
+ }
3828
+ cancel() {
3829
+ const reason = this.config.cancel?.reason || ModalCloseReason.CANCELLED;
3830
+ this.modalRef.dismiss(reason);
3831
+ }
3832
+ get confirmLabel() {
3833
+ return this.config.confirm?.label || 'Confirm';
3834
+ }
3835
+ get cancelLabel() {
3836
+ return this.config.cancel?.label || 'Cancel';
3837
+ }
3838
+ get confirmStyle() {
3839
+ return this.config.confirm?.style || ActionStyle.PRIMARY;
3840
+ }
3841
+ get cancelStyle() {
3842
+ return this.config.cancel?.style || ActionStyle.SECONDARY;
3843
+ }
3844
+ get toneClass() {
3845
+ switch (this.config.tone) {
3846
+ case ConfirmationTone.WARNING:
3847
+ return 'tone-warning';
3848
+ case ConfirmationTone.DANGER:
3849
+ return 'tone-danger';
3850
+ default:
3851
+ return 'tone-default';
3852
+ }
3853
+ }
3854
+ getButtonColor(style) {
3855
+ switch (style) {
3856
+ case ActionStyle.PRIMARY:
3857
+ return 'primary';
3858
+ case ActionStyle.DANGER:
3859
+ return 'danger';
3860
+ case ActionStyle.GHOST:
3861
+ return 'secondary';
3862
+ default:
3863
+ return 'secondary';
3864
+ }
3865
+ }
3866
+ getButtonVariant(style) {
3867
+ switch (style) {
3868
+ case ActionStyle.PRIMARY:
3869
+ case ActionStyle.DANGER:
3870
+ return 'fill';
3871
+ case ActionStyle.GHOST:
3872
+ return 'text';
3873
+ default:
3874
+ return 'outline';
3875
+ }
3876
+ }
3877
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3878
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnConfirmationBodyComponent, isStandalone: true, selector: "mn-confirmation-body", inputs: { config: "config", modalRef: "modalRef" }, ngImport: i0, template: "<div class=\"flex flex-col items-center gap-6 text-center\" [ngClass]=\"toneClass\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle)\n }\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }] });
3879
+ }
3880
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
3881
+ type: Component,
3882
+ args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton], template: "<div class=\"flex flex-col items-center gap-6 text-center\" [ngClass]=\"toneClass\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle)\n }\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
3883
+ }], propDecorators: { config: [{
3884
+ type: Input
3885
+ }], modalRef: [{
3886
+ type: Input
3887
+ }] } });
3888
+
3889
+ class MnCustomBodyHostComponent {
3890
+ config;
3891
+ modalRef;
3892
+ container;
3893
+ componentRef;
3894
+ ngOnInit() {
3895
+ setTimeout(() => this.loadContent(), 0);
3896
+ }
3897
+ loadContent() {
3898
+ if (!this.container)
3899
+ return;
3900
+ this.container.clear();
3901
+ if (this.config.component) {
3902
+ this.attachComponent(this.config.component);
3903
+ }
3904
+ else if (this.config.template) {
3905
+ this.attachTemplate(this.config.template);
3906
+ }
3907
+ }
3908
+ attachComponent(component) {
3909
+ this.componentRef = this.container.createComponent(component);
3910
+ // Pass inputs to the component
3911
+ if (this.config.inputs) {
3912
+ Object.entries(this.config.inputs).forEach(([key, value]) => {
3913
+ this.componentRef.instance[key] = value;
3914
+ });
3915
+ }
3916
+ // Pass modalRef if the component has a modalRef property
3917
+ const instance = this.componentRef.instance;
3918
+ if (instance && 'modalRef' in instance) {
3919
+ instance.modalRef = this.modalRef;
3920
+ }
3921
+ }
3922
+ attachTemplate(template) {
3923
+ this.container.createEmbeddedView(template, {
3924
+ $implicit: this.modalRef,
3925
+ modalRef: this.modalRef,
3926
+ });
3927
+ }
3928
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3929
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnCustomBodyHostComponent, isStandalone: true, selector: "mn-custom-body-host", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: '<ng-container #container></ng-container>', isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
3930
+ }
3931
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, decorators: [{
3932
+ type: Component,
3933
+ args: [{
3934
+ selector: 'mn-custom-body-host',
3935
+ standalone: true,
3936
+ imports: [CommonModule],
3937
+ template: '<ng-container #container></ng-container>',
3938
+ }]
3939
+ }], propDecorators: { config: [{
3940
+ type: Input
3941
+ }], modalRef: [{
3942
+ type: Input
3943
+ }], container: [{
3944
+ type: ViewChild,
3945
+ args: ['container', { read: ViewContainerRef }]
3946
+ }] } });
3947
+
3948
+ class MnModalShellComponent {
3949
+ el;
3950
+ config;
3951
+ modalRef;
3952
+ isClosing = false;
3953
+ ModalKind = ModalKind;
3954
+ previouslyFocusedElement = null;
3955
+ focusTrapListener = null;
3956
+ pollingTimer = null;
3957
+ pollAttempts = 0;
3958
+ constructor(el) {
3959
+ this.el = el;
3960
+ }
3961
+ ngOnInit() {
3962
+ this.startPollingIfConfigured();
3963
+ }
3964
+ ngAfterViewInit() {
3965
+ this.previouslyFocusedElement = document.activeElement;
3966
+ this.setupFocusTrap();
3967
+ // Focus the modal container
3968
+ const container = this.el.nativeElement.querySelector('.modal-container');
3969
+ if (container) {
3970
+ container.focus();
3971
+ }
3972
+ }
3973
+ ngOnDestroy() {
3974
+ this.removeFocusTrap();
3975
+ this.stopPolling();
3976
+ // Restore focus to previously focused element
3977
+ if (this.previouslyFocusedElement && typeof this.previouslyFocusedElement.focus === 'function') {
3978
+ this.previouslyFocusedElement.focus();
3979
+ }
3980
+ }
3981
+ setupFocusTrap() {
3982
+ this.focusTrapListener = (e) => {
3983
+ if (e.key !== 'Tab')
3984
+ return;
3985
+ const focusable = this.el.nativeElement.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
3986
+ if (focusable.length === 0)
3987
+ return;
3988
+ const first = focusable[0];
3989
+ const last = focusable[focusable.length - 1];
3990
+ if (e.shiftKey) {
3991
+ if (document.activeElement === first) {
3992
+ e.preventDefault();
3993
+ last.focus();
3994
+ }
3995
+ }
3996
+ else {
3997
+ if (document.activeElement === last) {
3998
+ e.preventDefault();
3999
+ first.focus();
4000
+ }
4001
+ }
4002
+ };
4003
+ this.el.nativeElement.addEventListener('keydown', this.focusTrapListener);
4004
+ }
4005
+ removeFocusTrap() {
4006
+ if (this.focusTrapListener) {
4007
+ this.el.nativeElement.removeEventListener('keydown', this.focusTrapListener);
4008
+ this.focusTrapListener = null;
4009
+ }
4010
+ }
4011
+ asWizard(config) {
4012
+ return config;
4013
+ }
4014
+ asForm(config) {
4015
+ return config;
4016
+ }
4017
+ asConfirmation(config) {
4018
+ return config;
4019
+ }
4020
+ asCustom(config) {
4021
+ return config;
4022
+ }
4023
+ asAny(val) {
4024
+ return val;
4025
+ }
4026
+ get hostClasses() {
4027
+ const size = this.config.size || ModalSize.MD;
4028
+ const closing = this.isClosing ? ' closing' : '';
4029
+ return `modal-shell modal-${size}${closing}`;
4030
+ }
4031
+ startClosing() {
4032
+ this.isClosing = true;
4033
+ return new Promise(resolve => setTimeout(resolve, 150));
4034
+ }
4035
+ onEscapeKey(event) {
4036
+ if (this.config.keyboard === KeyboardMode.ENABLED) {
4037
+ this.handleClose(ModalCloseReason.ESCAPE);
4038
+ if (event && event.preventDefault) {
4039
+ event.preventDefault();
4040
+ }
4041
+ }
4042
+ }
4043
+ onBackdropClick() {
4044
+ if (this.config.backdrop === BackdropMode.CLOSABLE) {
4045
+ this.handleClose(ModalCloseReason.BACKDROP);
4046
+ }
4047
+ }
4048
+ onCloseButtonClick() {
4049
+ this.handleClose(ModalCloseReason.DISMISSED);
4050
+ }
4051
+ async handleClose(reason) {
4052
+ if (this.config.closeMode === CloseMode.DISABLED) {
4053
+ return;
4054
+ }
4055
+ if (this.config.closeMode === CloseMode.GUARDED) {
4056
+ if (this.config.closeGuard) {
4057
+ const allowed = await this.config.closeGuard();
4058
+ if (!allowed)
4059
+ return;
4060
+ }
4061
+ }
4062
+ this.modalRef.dismiss(reason);
4063
+ }
4064
+ get showBackdrop() {
4065
+ return this.config.backdrop !== BackdropMode.HIDE;
4066
+ }
4067
+ get containerSizeClass() {
4068
+ switch (this.config.size || ModalSize.MD) {
4069
+ case ModalSize.SM: return 'w-96';
4070
+ case ModalSize.MD: return 'w-[32rem]';
4071
+ case ModalSize.LG: return 'w-[48rem]';
4072
+ case ModalSize.XL: return 'w-[64rem]';
4073
+ case ModalSize.FULL: return 'w-[95vw] h-[95vh] max-h-[95vh]';
4074
+ default: return 'w-[32rem]';
4075
+ }
4076
+ }
4077
+ get showCloseButton() {
4078
+ return this.config.closeMode !== CloseMode.DISABLED;
4079
+ }
4080
+ // =========================
4081
+ // Footer Actions
4082
+ // =========================
4083
+ get hasCustomFooterActions() {
4084
+ return !!this.config.footerActions && this.config.footerActions.length > 0;
4085
+ }
4086
+ get leftFooterActions() {
4087
+ return (this.config.footerActions || []).filter(a => a.position === 'left');
4088
+ }
4089
+ get rightFooterActions() {
4090
+ return (this.config.footerActions || []).filter(a => a.position !== 'left');
4091
+ }
4092
+ async onFooterAction(action) {
4093
+ if (action.disabled)
4094
+ return;
4095
+ if (action.handler) {
4096
+ await action.handler(this.modalRef);
4097
+ }
4098
+ if (action.closesModal) {
4099
+ if (action.closeReason === ModalCloseReason.COMPLETED) {
4100
+ this.modalRef.close();
4101
+ }
4102
+ else {
4103
+ this.modalRef.dismiss(action.closeReason || ModalCloseReason.DISMISSED);
4104
+ }
4105
+ }
4106
+ }
4107
+ getActionButtonColor(style) {
4108
+ switch (style) {
4109
+ case ActionStyle.PRIMARY: return 'primary';
4110
+ case ActionStyle.DANGER: return 'danger';
4111
+ case ActionStyle.GHOST: return 'secondary';
4112
+ default: return 'secondary';
4113
+ }
4114
+ }
4115
+ getActionButtonVariant(style) {
4116
+ switch (style) {
4117
+ case ActionStyle.PRIMARY:
4118
+ case ActionStyle.DANGER: return 'fill';
4119
+ case ActionStyle.GHOST: return 'text';
4120
+ default: return 'outline';
4121
+ }
4122
+ }
4123
+ // =========================
4124
+ // Polling
4125
+ // =========================
4126
+ startPollingIfConfigured() {
4127
+ const polling = this.config.polling;
4128
+ if (!polling)
4129
+ return;
4130
+ if (polling.autoStart === false)
4131
+ return;
4132
+ this.startPolling();
4133
+ }
4134
+ startPolling() {
4135
+ const polling = this.config.polling;
4136
+ if (!polling)
4137
+ return;
4138
+ this.pollingTimer = setInterval(async () => {
4139
+ this.pollAttempts++;
4140
+ try {
4141
+ const shouldStop = await polling.onPoll(this.modalRef);
4142
+ if (shouldStop === true) {
4143
+ this.stopPolling();
4144
+ }
4145
+ }
4146
+ catch (e) {
4147
+ console.error('Polling error:', e);
4148
+ }
4149
+ if (polling.maxAttempts && this.pollAttempts >= polling.maxAttempts) {
4150
+ this.stopPolling();
4151
+ }
4152
+ }, polling.interval);
4153
+ }
4154
+ stopPolling() {
4155
+ if (this.pollingTimer) {
4156
+ clearInterval(this.pollingTimer);
4157
+ this.pollingTimer = null;
4158
+ }
4159
+ }
4160
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
4161
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col animate-[slideIn_0.2s_ease-in-out]\"\n [ngClass]=\"containerSizeClass\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n <div class=\"flex items-center justify-between p-6 border-b border-gray-200\">\n <div class=\"flex flex-col gap-0.5\">\n <h2 class=\"m-0 text-xl font-semibold text-gray-900\" *ngIf=\"config.title\" id=\"mn-modal-title\">{{ config.title }}</h2>\n <p class=\"m-0 text-sm text-gray-500 font-normal\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n </div>\n <button\n *ngIf=\"showCloseButton\"\n class=\"bg-transparent border-none text-2xl cursor-pointer text-gray-500 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-100 hover:text-gray-900\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n </div>\n <p class=\"m-0 px-6 text-sm text-gray-500 leading-relaxed\" *ngIf=\"config.description\" id=\"mn-modal-description\">{{ config.description }}</p>\n\n <div class=\"flex-1 overflow-y-auto p-6\">\n <mn-wizard-body\n *ngIf=\"config.kind === ModalKind.WIZARD\"\n [config]=\"asWizard(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-wizard-body>\n\n <mn-form-body\n *ngIf=\"config.kind === ModalKind.FORM\"\n [config]=\"asForm(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-form-body>\n\n <mn-confirmation-body\n *ngIf=\"config.kind === ModalKind.CONFIRMATION\"\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-confirmation-body>\n\n <mn-custom-body-host\n *ngIf=\"config.kind === ModalKind.CUSTOM\"\n [config]=\"asCustom(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n </div>\n\n <!-- Custom Footer Actions -->\n <div *ngIf=\"hasCustomFooterActions\" class=\"flex gap-3 p-6 border-t border-gray-200\">\n <ng-container *ngFor=\"let action of leftFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n\n <div class=\"flex-1\"></div>\n\n <ng-container *ngFor=\"let action of rightFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing) .modal-container{animation:slideOut .15s ease-in-out forwards}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }] });
4162
+ }
4163
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
4164
+ type: Component,
4165
+ args: [{ selector: 'mn-modal-shell', standalone: true, imports: [
4166
+ CommonModule,
4167
+ MnWizardBodyComponent,
4168
+ MnFormBodyComponent,
4169
+ MnConfirmationBodyComponent,
4170
+ MnCustomBodyHostComponent,
4171
+ MnButton,
4172
+ ], template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col animate-[slideIn_0.2s_ease-in-out]\"\n [ngClass]=\"containerSizeClass\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n <div class=\"flex items-center justify-between p-6 border-b border-gray-200\">\n <div class=\"flex flex-col gap-0.5\">\n <h2 class=\"m-0 text-xl font-semibold text-gray-900\" *ngIf=\"config.title\" id=\"mn-modal-title\">{{ config.title }}</h2>\n <p class=\"m-0 text-sm text-gray-500 font-normal\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n </div>\n <button\n *ngIf=\"showCloseButton\"\n class=\"bg-transparent border-none text-2xl cursor-pointer text-gray-500 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-100 hover:text-gray-900\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n </div>\n <p class=\"m-0 px-6 text-sm text-gray-500 leading-relaxed\" *ngIf=\"config.description\" id=\"mn-modal-description\">{{ config.description }}</p>\n\n <div class=\"flex-1 overflow-y-auto p-6\">\n <mn-wizard-body\n *ngIf=\"config.kind === ModalKind.WIZARD\"\n [config]=\"asWizard(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-wizard-body>\n\n <mn-form-body\n *ngIf=\"config.kind === ModalKind.FORM\"\n [config]=\"asForm(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-form-body>\n\n <mn-confirmation-body\n *ngIf=\"config.kind === ModalKind.CONFIRMATION\"\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-confirmation-body>\n\n <mn-custom-body-host\n *ngIf=\"config.kind === ModalKind.CUSTOM\"\n [config]=\"asCustom(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n </div>\n\n <!-- Custom Footer Actions -->\n <div *ngIf=\"hasCustomFooterActions\" class=\"flex gap-3 p-6 border-t border-gray-200\">\n <ng-container *ngFor=\"let action of leftFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n\n <div class=\"flex-1\"></div>\n\n <ng-container *ngFor=\"let action of rightFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing) .modal-container{animation:slideOut .15s ease-in-out forwards}\n"] }]
4173
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
4174
+ type: Input
4175
+ }], modalRef: [{
4176
+ type: Input
4177
+ }], hostClasses: [{
4178
+ type: HostBinding,
4179
+ args: ['class']
4180
+ }], onEscapeKey: [{
4181
+ type: HostListener,
4182
+ args: ['document:keydown.escape', ['$event']]
4183
+ }] } });
4184
+
4185
+ class MnModalService {
4186
+ appRef = inject(ApplicationRef);
4187
+ injector = inject(EnvironmentInjector);
4188
+ open(config) {
4189
+ // Create the modal shell component
4190
+ const componentRef = createComponent(MnModalShellComponent, {
4191
+ environmentInjector: this.injector,
4192
+ });
4193
+ // Set the config on the component
4194
+ componentRef.instance.config = config;
4195
+ // Create modal ref
4196
+ const modalRef = new MnModalRef(componentRef, config);
4197
+ componentRef.instance.modalRef = modalRef;
4198
+ // Attach to application
4199
+ this.appRef.attachView(componentRef.hostView);
4200
+ const domElem = componentRef.location.nativeElement;
4201
+ document.body.appendChild(domElem);
4202
+ // Clean up on close
4203
+ modalRef.afterClosed$.subscribe(() => {
4204
+ this.appRef.detachView(componentRef.hostView);
4205
+ domElem.remove();
4206
+ });
4207
+ return modalRef;
4208
+ }
4209
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4210
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, providedIn: 'root' });
4211
+ }
4212
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, decorators: [{
4213
+ type: Injectable,
4214
+ args: [{
4215
+ providedIn: 'root',
4216
+ }]
4217
+ }] });
4218
+
4219
+ // Types
4220
+
4221
+ class MnSectionDirective {
4222
+ /** Section name contributed by this DOM node to the section path */
4223
+ mnSection;
4224
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4225
+ 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: [
4226
+ {
4227
+ provide: MN_SECTION_PATH,
4228
+ // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
4229
+ // and read the attribute value using Attribute so it's available at provider creation time.
4230
+ deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
4231
+ useFactory: (parentPath, attr) => {
4232
+ const parent = Array.isArray(parentPath) ? parentPath : [];
4233
+ const name = (attr ?? '').trim();
4234
+ return name ? [...parent, name] : [...parent];
4235
+ },
4236
+ },
4237
+ ], ngImport: i0 });
4238
+ }
4239
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
4240
+ type: Directive,
4241
+ args: [{
4242
+ selector: '[mn-section]',
4243
+ standalone: true,
4244
+ providers: [
4245
+ {
4246
+ provide: MN_SECTION_PATH,
4247
+ // Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
4248
+ // and read the attribute value using Attribute so it's available at provider creation time.
4249
+ deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
4250
+ useFactory: (parentPath, attr) => {
4251
+ const parent = Array.isArray(parentPath) ? parentPath : [];
4252
+ const name = (attr ?? '').trim();
4253
+ return name ? [...parent, name] : [...parent];
4254
+ },
4255
+ },
4256
+ ],
4257
+ }]
4258
+ }], propDecorators: { mnSection: [{
4259
+ type: Input,
4260
+ args: ['mn-section']
4261
+ }] } });
4262
+
4263
+ class MnInstanceDirective {
4264
+ /** Instance id for targeting per-component instance overrides */
4265
+ mnInstance;
4266
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4267
+ 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: [
4268
+ {
4269
+ provide: MN_INSTANCE_ID,
4270
+ // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
4271
+ deps: [new Attribute('mn-instance')],
4272
+ useFactory: (attr) => (attr ?? '').trim() || null,
4273
+ },
4274
+ ], ngImport: i0 });
4275
+ }
4276
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
4277
+ type: Directive,
4278
+ args: [{
4279
+ selector: '[mn-instance]',
4280
+ standalone: true,
4281
+ providers: [
4282
+ {
4283
+ provide: MN_INSTANCE_ID,
4284
+ // Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
4285
+ deps: [new Attribute('mn-instance')],
4286
+ useFactory: (attr) => (attr ?? '').trim() || null,
4287
+ },
4288
+ ],
4289
+ }]
4290
+ }], propDecorators: { mnInstance: [{
4291
+ type: Input,
4292
+ args: ['mn-instance']
4293
+ }] } });
4294
+
4295
+ /**
4296
+ * Provides an APP_INITIALIZER that configures the MnLanguageService and
4297
+ * preloads the requested locales during application bootstrap.
4298
+ *
4299
+ * Usage in app.config.ts:
4300
+ * ...provideMnLanguage({
4301
+ * urlPattern: 'assets/i18n/{locale}.json',
4302
+ * defaultLocale: 'en',
4303
+ * preload: ['en', 'nl'],
4304
+ * })
4305
+ */
4306
+ function provideMnLanguage(config) {
4307
+ return [
4308
+ {
4309
+ provide: APP_INITIALIZER,
4310
+ multi: true,
4311
+ useFactory: (svc) => async () => {
4312
+ svc.configure(config.urlPattern);
4313
+ const localesToLoad = config.preload ?? [config.defaultLocale];
4314
+ await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
4315
+ await svc.setLocale(config.defaultLocale);
4316
+ },
4317
+ deps: [MnLanguageService],
4318
+ },
4319
+ ];
4320
+ }
4321
+
4322
+ /**
4323
+ * Pipe that translates a key via MnLanguageService.
4324
+ *
4325
+ * Usage in templates:
4326
+ * {{ 'form.email.label' | mnTranslate }}
4327
+ * {{ 'greeting' | mnTranslate:{ name: 'World' } }}
4328
+ *
4329
+ * Note: This pipe is impure so it re-evaluates when the locale changes.
4330
+ */
4331
+ class MnTranslatePipe {
4332
+ lang;
4333
+ constructor(lang) {
4334
+ this.lang = lang;
4335
+ }
4336
+ transform(key, params) {
4337
+ return this.lang.translate(key, params);
4338
+ }
4339
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [{ token: MnLanguageService }], target: i0.ɵɵFactoryTarget.Pipe });
4340
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
4341
+ }
4342
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
4343
+ type: Pipe,
4344
+ args: [{
4345
+ name: 'mnTranslate',
4346
+ standalone: true,
4347
+ pure: false,
4348
+ }]
4349
+ }], ctorParameters: () => [{ type: MnLanguageService }] });
4350
+
1468
4351
  /*
1469
4352
  * Public API Surface of mn-lib
1470
4353
  */
@@ -1473,5 +4356,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1473
4356
  * Generated bundle index. Do not edit.
1474
4357
  */
1475
4358
 
1476
- export { DEFAULT_MN_ALERT_CONFIG, MN_ALERT_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnConfigService, MnDualHorizontalImage, MnInformationCard, MnInputField, MnInstanceDirective, MnSectionDirective, MnTestComponent, MnTextarea, Test, dateTimeAdapter, defaultTextAdapter, mnAlertVariants, mnButtonVariants, mnInformationCardVariants, mnInputFieldVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig };
4359
+ export { ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CustomModalBuilder, DEFAULT_MN_ALERT_CONFIG, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig, provideMnLanguage };
1477
4360
  //# sourceMappingURL=mn-angular-lib.mjs.map