mn-angular-lib 0.0.43 → 0.0.45

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