mn-angular-lib 0.0.51 → 0.0.53

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,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, DestroyRef, Self, APP_INITIALIZER, ElementRef, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewChildren, ViewContainerRef, ViewChild, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
2
+ import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, signal, ElementRef, DestroyRef, Self, APP_INITIALIZER, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewContainerRef, ViewChild, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
3
3
  export { TemplateRef, Type } from '@angular/core';
4
- import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, map, catchError, of } from 'rxjs';
4
+ import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
7
7
  import { tv } from 'tailwind-variants';
8
8
  import * as i1$2 from '@angular/forms';
9
- import { Validators, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
9
+ import { Validators, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
10
10
  import * as i1$1 from '@angular/common/http';
11
11
  import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
12
12
  import JSON5 from 'json5';
@@ -401,12 +401,65 @@ const emptyToNull = (raw) => (raw === '' ? null : raw);
401
401
  * - Values are stored as strings in the FormControl
402
402
  * - No special DOM attributes
403
403
  * - No additional validation (relies on Angular's built-in validators)
404
+ * - Supports simple masking (0 for digit, A for alpha, * for any)
404
405
  */
405
406
  const defaultTextAdapter = {
406
407
  parse: (raw) => emptyToNull(raw),
407
408
  format: (val) => (val == null ? '' : String(val)),
408
409
  attrs: () => ({}),
409
410
  validate: () => null,
411
+ applyMask: (value, mask) => {
412
+ if (!mask || !value)
413
+ return value;
414
+ let result = '';
415
+ let maskIndex = 0;
416
+ let dataIndex = 0;
417
+ // Remove non-alphanumeric if we want to re-mask from clean data
418
+ // But usually we just want to restrict input.
419
+ // A simple implementation:
420
+ while (maskIndex < mask.length && dataIndex < value.length) {
421
+ const maskChar = mask[maskIndex];
422
+ const dataChar = value[dataIndex];
423
+ if (maskChar === '0') {
424
+ if (/\d/.test(dataChar)) {
425
+ result += dataChar;
426
+ dataIndex++;
427
+ maskIndex++;
428
+ }
429
+ else {
430
+ dataIndex++; // skip invalid
431
+ }
432
+ }
433
+ else if (maskChar === 'A') {
434
+ if (/[a-zA-Z]/.test(dataChar)) {
435
+ result += dataChar;
436
+ dataIndex++;
437
+ maskIndex++;
438
+ }
439
+ else {
440
+ dataIndex++; // skip invalid
441
+ }
442
+ }
443
+ else if (maskChar === '*') {
444
+ result += dataChar;
445
+ dataIndex++;
446
+ maskIndex++;
447
+ }
448
+ else {
449
+ result += maskChar;
450
+ if (dataChar === maskChar) {
451
+ dataIndex++;
452
+ }
453
+ maskIndex++;
454
+ }
455
+ }
456
+ // Auto-append static characters if next in mask
457
+ while (maskIndex < mask.length && !/[0A*]/.test(mask[maskIndex])) {
458
+ result += mask[maskIndex];
459
+ maskIndex++;
460
+ }
461
+ return result;
462
+ }
410
463
  };
411
464
  /**
412
465
  * Adapter for date and time input types.
@@ -667,11 +720,19 @@ function isPlainObject(value) {
667
720
  class MnConfigService {
668
721
  http;
669
722
  _config = null;
723
+ _settings = {};
670
724
  _debugMode = false;
725
+ /** Reactive version counter — incremented on every config load. */
726
+ _configVersion = signal(0, ...(ngDevMode ? [{ debugName: "_configVersion" }] : []));
727
+ configVersion = this._configVersion.asReadonly();
671
728
  lang = inject(MnLanguageService);
672
729
  constructor(http) {
673
730
  this.http = http;
674
731
  }
732
+ /** General settings from the config file (version, name, etc.). */
733
+ get settings() {
734
+ return this._settings;
735
+ }
675
736
  /**
676
737
  * Load the configuration JSON from the provided URL and cache it in memory.
677
738
  * Consumers should typically call this via the APP_INITIALIZER helper.
@@ -698,7 +759,9 @@ class MnConfigService {
698
759
  const cfg = (isPlainObject(json) ? json : {});
699
760
  const defaults = isPlainObject(cfg.defaults) ? cfg.defaults : {};
700
761
  const overrides = isPlainObject(cfg.overrides) ? cfg.overrides : cfg.overrides ?? {};
701
- this._config = { defaults, overrides };
762
+ const settings = isPlainObject(cfg.settings) ? cfg.settings : {};
763
+ this._config = { settings, defaults, overrides };
764
+ this._settings = settings;
702
765
  // Bootstrap language service from config if a "language" section is present.
703
766
  // This avoids circular dependency: config reads raw language settings and
704
767
  // pushes them into the language service (language service never imports config).
@@ -711,6 +774,31 @@ class MnConfigService {
711
774
  await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
712
775
  await this.lang.setLocale(effectiveLocale);
713
776
  }
777
+ this._configVersion.update(v => v + 1);
778
+ }
779
+ /**
780
+ * Load configuration from a pre-parsed object (no HTTP fetch).
781
+ * Used for live preview scenarios where config is pushed via postMessage.
782
+ * Optionally re-bootstraps the language service if a `language` section is present.
783
+ */
784
+ async loadFromObject(config, bootstrapLanguage = false) {
785
+ const defaults = isPlainObject(config['defaults']) ? config['defaults'] : {};
786
+ const overrides = isPlainObject(config['overrides']) ? config['overrides'] : {};
787
+ const settings = isPlainObject(config['settings']) ? config['settings'] : {};
788
+ this._config = { settings, defaults, overrides };
789
+ this._settings = settings;
790
+ if (bootstrapLanguage) {
791
+ const langCfg = config['language'];
792
+ if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
793
+ const lc = langCfg;
794
+ this.lang.configure(lc.urlPattern);
795
+ const effectiveLocale = this.lang.resolveLocaleForDomain(lc.domainLocaleMap, lc.defaultLocale);
796
+ const localesToLoad = lc.preload ?? [effectiveLocale];
797
+ await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
798
+ await this.lang.setLocale(effectiveLocale);
799
+ }
800
+ }
801
+ this._configVersion.update(v => v + 1);
714
802
  }
715
803
  /**
716
804
  * Resolve a configuration object for a component, optionally scoped to a section path
@@ -865,6 +953,7 @@ class MnInputField {
865
953
  ngControl;
866
954
  /** Resolved UI configuration for the input field */
867
955
  uiConfig = {};
956
+ el = inject(ElementRef);
868
957
  /** Configuration properties for the input field */
869
958
  props;
870
959
  configService = inject(MnConfigService);
@@ -910,6 +999,17 @@ class MnInputField {
910
999
  this.resolveConfig();
911
1000
  });
912
1001
  this.destroyRef.onDestroy(() => sub.unsubscribe());
1002
+ if (this.props.autoFocus) {
1003
+ setTimeout(() => this.focus(), 0);
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Focuses the input element.
1008
+ */
1009
+ focus() {
1010
+ const input = this.el.nativeElement.querySelector('input');
1011
+ if (input)
1012
+ input.focus();
913
1013
  }
914
1014
  resolveConfig() {
915
1015
  const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
@@ -971,8 +1071,13 @@ class MnInputField {
971
1071
  * @param raw - Raw string value from the input element
972
1072
  */
973
1073
  handleInput(raw) {
974
- this.value = raw;
975
- this.onChange(this.adapter.parse(raw));
1074
+ let finalValue = raw;
1075
+ // Apply mask if available
1076
+ if (this.props.mask && typeof this.adapter.applyMask === 'function') {
1077
+ finalValue = this.adapter.applyMask(raw, this.props.mask);
1078
+ }
1079
+ this.value = finalValue;
1080
+ this.onChange(this.adapter.parse(finalValue));
976
1081
  }
977
1082
  /**
978
1083
  * Handles blur events from the input element.
@@ -1127,11 +1232,11 @@ class MnInputField {
1127
1232
  });
1128
1233
  }
1129
1234
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
1130
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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 }\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"] }] });
1235
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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 }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
1131
1236
  }
1132
1237
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, decorators: [{
1133
1238
  type: Component,
1134
- args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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 }\n</div>\n" }]
1239
+ args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage, FormsModule], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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 }\n</div>\n" }]
1135
1240
  }], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
1136
1241
  type: Optional
1137
1242
  }, {
@@ -1374,6 +1479,7 @@ class MnTextarea {
1374
1479
  ngControl;
1375
1480
  /** Resolved UI configuration for the textarea */
1376
1481
  uiConfig = {};
1482
+ el = inject(ElementRef);
1377
1483
  /** Configuration properties for the textarea */
1378
1484
  props;
1379
1485
  configService = inject(MnConfigService);
@@ -1416,6 +1522,17 @@ class MnTextarea {
1416
1522
  this.resolveConfig();
1417
1523
  });
1418
1524
  this.destroyRef.onDestroy(() => sub.unsubscribe());
1525
+ if (this.props.autoFocus) {
1526
+ setTimeout(() => this.focus(), 0);
1527
+ }
1528
+ }
1529
+ /**
1530
+ * Focuses the textarea element.
1531
+ */
1532
+ focus() {
1533
+ const textarea = this.el.nativeElement.querySelector('textarea');
1534
+ if (textarea)
1535
+ textarea.focus();
1419
1536
  }
1420
1537
  resolveConfig() {
1421
1538
  const instanceId = this.explicitInstanceId || `mn-textarea-${this.props.id}`;
@@ -1581,11 +1698,11 @@ class MnTextarea {
1581
1698
  });
1582
1699
  }
1583
1700
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
1584
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTextarea, isStandalone: true, selector: "mn-lib-textarea", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\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 <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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"] }] });
1701
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTextarea, isStandalone: true, selector: "mn-lib-textarea", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\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 <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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"] }] });
1585
1702
  }
1586
1703
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, decorators: [{
1587
1704
  type: Component,
1588
- args: [{ selector: 'mn-lib-textarea', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\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 <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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" }]
1705
+ args: [{ selector: 'mn-lib-textarea', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\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 <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\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" }]
1589
1706
  }], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
1590
1707
  type: Optional
1591
1708
  }, {
@@ -2307,53 +2424,41 @@ var ValidationCode;
2307
2424
  ValidationCode["CUSTOM"] = "custom";
2308
2425
  })(ValidationCode || (ValidationCode = {}));
2309
2426
 
2310
- class StepBuilder {
2427
+ /**
2428
+ * A builder class that provides form layout capabilities (fields, rows, groups).
2429
+ * This can be used as a delegate to avoid code duplication between FormModalBuilder and StepBuilder.
2430
+ */
2431
+ class FormLayoutBuilder {
2311
2432
  config;
2433
+ parent;
2312
2434
  currentRow = [];
2313
2435
  currentRowColumns = 1;
2314
- constructor(id, title) {
2315
- this.config = {
2316
- id,
2317
- title,
2318
- fields: [],
2319
- state: StepState.PENDING,
2320
- };
2436
+ constructor(config, parent) {
2437
+ this.config = config;
2438
+ this.parent = parent;
2439
+ this.config.fields = this.config.fields || [];
2440
+ this.config.rows = this.config.rows || [];
2321
2441
  }
2442
+ /**
2443
+ * Add a custom body/content to the form/step.
2444
+ */
2322
2445
  body(body) {
2323
2446
  this.config.body = body;
2324
- return this;
2325
- }
2326
- state(state) {
2327
- this.config.state = state;
2328
- return this;
2329
- }
2330
- guard(guard) {
2331
- this.config.guard = guard;
2332
- return this;
2333
- }
2334
- validators(validators) {
2335
- this.config.validators = validators;
2336
- return this;
2447
+ return this.parent;
2337
2448
  }
2338
2449
  /**
2339
- * Add a field to this step. This is the single API for all field types.
2340
- *
2341
- * @example
2342
- * s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
2343
- * s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
2450
+ * Add a field as a full-width row (single column).
2344
2451
  */
2345
2452
  field(field) {
2346
2453
  this.flushCurrentRow();
2347
2454
  this.config.fields = this.config.fields || [];
2348
2455
  this.config.fields.push(field);
2349
- if (!this.config.rows) {
2350
- this.config.rows = [];
2351
- }
2456
+ this.config.rows = this.config.rows || [];
2352
2457
  this.config.rows.push({
2353
2458
  columns: 1,
2354
2459
  fields: [{ field, span: 1 }],
2355
2460
  });
2356
- return this;
2461
+ return this.parent;
2357
2462
  }
2358
2463
  /**
2359
2464
  * Start a new row with the specified number of columns.
@@ -2362,70 +2467,293 @@ class StepBuilder {
2362
2467
  row(columns = 2) {
2363
2468
  this.flushCurrentRow();
2364
2469
  this.currentRowColumns = columns;
2365
- return this;
2470
+ return this.parent;
2366
2471
  }
2367
2472
  /**
2368
2473
  * Add a field to the current row started by `row()`.
2474
+ * @param field - The field configuration
2475
+ * @param span - How many columns this field should span (default: 1)
2369
2476
  */
2370
2477
  addToRow(field, span = 1) {
2371
2478
  this.config.fields = this.config.fields || [];
2372
2479
  this.config.fields.push(field);
2373
2480
  this.currentRow.push({ field, span });
2374
- return this;
2481
+ return this.parent;
2375
2482
  }
2376
- flushCurrentRow() {
2377
- if (this.currentRow.length > 0) {
2378
- if (!this.config.rows) {
2379
- this.config.rows = [];
2380
- }
2483
+ /**
2484
+ * Declarative way to add a row.
2485
+ * @example
2486
+ * .addRow(2, row => {
2487
+ * row.add({ kind: FieldKind.TEXT, key: 'first', ... });
2488
+ * row.add({ kind: FieldKind.TEXT, key: 'last', ... });
2489
+ * })
2490
+ */
2491
+ addRow(columns, buildFn) {
2492
+ this.flushCurrentRow();
2493
+ const rowFields = [];
2494
+ buildFn({
2495
+ add: (field, span = 1) => {
2496
+ this.config.fields = this.config.fields || [];
2497
+ this.config.fields.push(field);
2498
+ rowFields.push({ field, span });
2499
+ },
2500
+ });
2501
+ if (rowFields.length > 0) {
2502
+ this.config.rows = this.config.rows || [];
2381
2503
  this.config.rows.push({
2382
- columns: this.currentRowColumns,
2383
- fields: [...this.currentRow],
2504
+ columns,
2505
+ fields: rowFields,
2384
2506
  });
2385
- this.currentRow = [];
2386
- this.currentRowColumns = 1;
2387
2507
  }
2508
+ return this.parent;
2388
2509
  }
2389
- /**
2390
- * Add a field group with a section header.
2391
- */
2392
- fieldGroup(group) {
2510
+ fieldGroup(arg1, arg2, arg3) {
2393
2511
  this.flushCurrentRow();
2512
+ if (typeof arg1 !== 'string') {
2513
+ const group = arg1;
2514
+ this.processFieldGroup(group);
2515
+ return this.parent;
2516
+ }
2517
+ const title = arg1;
2518
+ let description;
2519
+ let buildFn;
2520
+ if (typeof arg2 === 'string') {
2521
+ description = arg2;
2522
+ buildFn = arg3;
2523
+ }
2524
+ else {
2525
+ buildFn = arg2;
2526
+ }
2527
+ const groupConfig = {
2528
+ fields: [],
2529
+ rows: [],
2530
+ };
2531
+ const groupBuilder = new FormLayoutBuilder(groupConfig, {});
2532
+ // Overwrite the groupBuilder's parent to itself for proper chaining within the group
2533
+ groupBuilder.parent = groupBuilder;
2534
+ buildFn(groupBuilder);
2535
+ groupBuilder.flushCurrentRow();
2536
+ const group = {
2537
+ title,
2538
+ description,
2539
+ fields: groupConfig.fields || [],
2540
+ rows: groupConfig.rows || [],
2541
+ };
2542
+ this.processFieldGroup(group);
2543
+ return this.parent;
2544
+ }
2545
+ processFieldGroup(group) {
2394
2546
  if (!this.config.fieldGroups) {
2395
2547
  this.config.fieldGroups = [];
2396
2548
  }
2549
+ // Also add group fields to the flat fields array for form control creation
2397
2550
  this.config.fields = this.config.fields || [];
2398
- group.fields.forEach(f => this.config.fields.push(f));
2399
- if (!group.rows) {
2400
- group.rows = group.fields.map(f => ({
2551
+ const fields = this.config.fields;
2552
+ group.fields.forEach((f) => fields.push(f));
2553
+ // Build rows for the group if not provided
2554
+ if (!group.rows || group.rows.length === 0) {
2555
+ group.rows = group.fields.map((f) => ({
2401
2556
  columns: 1,
2402
2557
  fields: [{ field: f, span: 1 }],
2403
2558
  }));
2404
2559
  }
2405
2560
  this.config.fieldGroups.push(group);
2406
- return this;
2407
2561
  }
2408
2562
  /**
2409
- * Add form-level validators for cross-field validation within this step.
2563
+ * Add form-level validators for cross-field validation.
2410
2564
  */
2411
2565
  formValidators(validators) {
2412
2566
  this.config.formValidators = validators;
2413
- return this;
2567
+ return this.parent;
2414
2568
  }
2415
2569
  /**
2416
- * Add Angular FormGroup-level validators for this step.
2570
+ * Add Angular FormGroup-level validators.
2417
2571
  */
2418
2572
  groupValidators(validators) {
2419
2573
  this.config.groupValidators = validators;
2420
- return this;
2574
+ return this.parent;
2421
2575
  }
2422
2576
  /**
2423
- * Set initial values for fields in this step.
2577
+ * Set initial value for fields.
2424
2578
  */
2425
2579
  initialValue(value) {
2426
2580
  this.config.initialValue = value;
2581
+ return this.parent;
2582
+ }
2583
+ /**
2584
+ * Set the field to be focused when the form initializes.
2585
+ */
2586
+ focus(key) {
2587
+ // Clear autoFocus from other fields first to ensure only one is focused
2588
+ this.config.fields?.forEach(f => {
2589
+ f.autoFocus = false;
2590
+ });
2591
+ this.config.fieldGroups?.forEach(g => {
2592
+ g.fields.forEach(f => {
2593
+ f.autoFocus = false;
2594
+ });
2595
+ });
2596
+ const field = this.config.fields?.find(f => f.key === key);
2597
+ if (field) {
2598
+ field.autoFocus = true;
2599
+ }
2600
+ return this.parent;
2601
+ }
2602
+ /**
2603
+ * Wraps a field with a fluent API for validation.
2604
+ */
2605
+ fieldWithValidators(field) {
2606
+ const fieldAny = field;
2607
+ fieldAny.validators = fieldAny.validators || [];
2608
+ this.field(field);
2609
+ return new FieldValidatorBuilder(field, this.parent);
2610
+ }
2611
+ /**
2612
+ * Flushes any pending fields in the current row to the configuration.
2613
+ */
2614
+ flushCurrentRow() {
2615
+ if (this.currentRow.length > 0) {
2616
+ this.config.rows.push({
2617
+ columns: this.currentRowColumns,
2618
+ fields: [...this.currentRow],
2619
+ });
2620
+ this.currentRow = [];
2621
+ this.currentRowColumns = 1;
2622
+ }
2623
+ }
2624
+ }
2625
+ /**
2626
+ * A builder for adding validation rules to a field fluently.
2627
+ */
2628
+ class FieldValidatorBuilder {
2629
+ field;
2630
+ parent;
2631
+ constructor(field, parent) {
2632
+ this.field = field;
2633
+ this.parent = parent;
2634
+ this.field.validators = this.field.validators || [];
2635
+ }
2636
+ required(message) {
2637
+ this.field.validators.push(Validators.required);
2638
+ return this;
2639
+ }
2640
+ minLength(length) {
2641
+ this.field.validators.push(Validators.minLength(length));
2642
+ return this;
2643
+ }
2644
+ maxLength(length) {
2645
+ this.field.validators.push(Validators.maxLength(length));
2646
+ return this;
2647
+ }
2648
+ pattern(pattern) {
2649
+ this.field.validators.push(Validators.pattern(pattern));
2650
+ return this;
2651
+ }
2652
+ email() {
2653
+ this.field.validators.push(Validators.email);
2654
+ return this;
2655
+ }
2656
+ min(value) {
2657
+ this.field.validators.push(Validators.min(value));
2658
+ return this;
2659
+ }
2660
+ max(value) {
2661
+ this.field.validators.push(Validators.max(value));
2662
+ return this;
2663
+ }
2664
+ /**
2665
+ * Add a custom validator.
2666
+ */
2667
+ custom(validator) {
2668
+ this.field.validators.push(validator);
2669
+ return this;
2670
+ }
2671
+ /**
2672
+ * Return to the parent builder.
2673
+ */
2674
+ done() {
2675
+ return this.parent;
2676
+ }
2677
+ }
2678
+
2679
+ class StepBuilder {
2680
+ config;
2681
+ layoutBuilder;
2682
+ constructor(id, title) {
2683
+ this.config = {
2684
+ id,
2685
+ title,
2686
+ fields: [],
2687
+ state: StepState.PENDING,
2688
+ };
2689
+ this.layoutBuilder = new FormLayoutBuilder(this.config, this);
2690
+ }
2691
+ body(body) {
2692
+ this.config.body = body;
2693
+ return this;
2694
+ }
2695
+ state(state) {
2696
+ this.config.state = state;
2697
+ return this;
2698
+ }
2699
+ guard(guard) {
2700
+ this.config.guard = guard;
2701
+ return this;
2702
+ }
2703
+ validators(validators) {
2704
+ this.config.validators = validators;
2427
2705
  return this;
2428
2706
  }
2707
+ /**
2708
+ * Add a field to this step. This is the single API for all field types.
2709
+ *
2710
+ * @example
2711
+ * s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
2712
+ * s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
2713
+ */
2714
+ field(field) {
2715
+ return this.layoutBuilder.field(field);
2716
+ }
2717
+ /**
2718
+ * Start a new row with the specified number of columns.
2719
+ * All subsequent `addToRow()` calls will add fields to this row.
2720
+ */
2721
+ row(columns = 2) {
2722
+ return this.layoutBuilder.row(columns);
2723
+ }
2724
+ /**
2725
+ * Add a field to the current row started by `row()`.
2726
+ */
2727
+ addToRow(field, span = 1) {
2728
+ return this.layoutBuilder.addToRow(field, span);
2729
+ }
2730
+ /**
2731
+ * Declarative way to add a row.
2732
+ */
2733
+ addRow(columns, buildFn) {
2734
+ return this.layoutBuilder.addRow(columns, buildFn);
2735
+ }
2736
+ fieldGroup(arg1, arg2, arg3) {
2737
+ return this.layoutBuilder.fieldGroup(arg1, arg2, arg3);
2738
+ }
2739
+ /**
2740
+ * Add form-level validators for cross-field validation within this step.
2741
+ */
2742
+ formValidators(validators) {
2743
+ return this.layoutBuilder.formValidators(validators);
2744
+ }
2745
+ /**
2746
+ * Add Angular FormGroup-level validators for this step.
2747
+ */
2748
+ groupValidators(validators) {
2749
+ return this.layoutBuilder.groupValidators(validators);
2750
+ }
2751
+ /**
2752
+ * Set initial values for fields in this step.
2753
+ */
2754
+ initialValue(value) {
2755
+ return this.layoutBuilder.initialValue(value);
2756
+ }
2429
2757
  /**
2430
2758
  * Set a visibility condition for this step based on aggregated wizard data.
2431
2759
  */
@@ -2433,16 +2761,40 @@ class StepBuilder {
2433
2761
  this.config.visible = condition;
2434
2762
  return this;
2435
2763
  }
2764
+ /**
2765
+ * Set a custom label for the 'Next' button on this step.
2766
+ */
2767
+ nextLabel(label) {
2768
+ this.config.nextLabel = label;
2769
+ return this;
2770
+ }
2771
+ /**
2772
+ * Set a custom label for the 'Back' button on this step.
2773
+ */
2774
+ backLabel(label) {
2775
+ this.config.backLabel = label;
2776
+ return this;
2777
+ }
2778
+ /**
2779
+ * Hide the 'Back' button on this step.
2780
+ */
2781
+ hideBack(hide = true) {
2782
+ this.config.hideBack = hide;
2783
+ return this;
2784
+ }
2436
2785
  build() {
2437
- this.flushCurrentRow();
2786
+ this.layoutBuilder.flushCurrentRow();
2438
2787
  return this.config;
2439
2788
  }
2440
2789
  }
2441
2790
 
2442
2791
  class BaseModalBuilder {
2443
2792
  config;
2793
+ layoutBuilder;
2444
2794
  constructor(initialConfig) {
2445
2795
  this.config = initialConfig;
2796
+ // Base layout builder, we pass this.config directly if it supports it
2797
+ this.layoutBuilder = new FormLayoutBuilder(this.config, this);
2446
2798
  }
2447
2799
  title(title) {
2448
2800
  this.config.title = title;
@@ -2480,6 +2832,14 @@ class BaseModalBuilder {
2480
2832
  this.config.intent = intent;
2481
2833
  return this;
2482
2834
  }
2835
+ readOnly(readOnly = true) {
2836
+ this.config.readOnly = readOnly;
2837
+ return this;
2838
+ }
2839
+ disabled(disabled = true) {
2840
+ this.config.disabled = disabled;
2841
+ return this;
2842
+ }
2483
2843
  footerActions(actions) {
2484
2844
  this.config.footerActions = actions;
2485
2845
  return this;
@@ -2496,25 +2856,107 @@ class BaseModalBuilder {
2496
2856
  this.config.i18n = labels;
2497
2857
  return this;
2498
2858
  }
2499
- build() {
2500
- return Object.freeze({ ...this.config });
2859
+ component(component) {
2860
+ this.config.component = component;
2861
+ return this;
2501
2862
  }
2502
- }
2503
-
2504
- class WizardModalBuilder extends BaseModalBuilder {
2505
- constructor() {
2506
- super({
2507
- kind: ModalKind.WIZARD,
2508
- steps: [],
2509
- });
2863
+ template(template) {
2864
+ this.config.template = template;
2865
+ return this;
2510
2866
  }
2511
- step(step) {
2512
- if (typeof step === 'function') {
2513
- const builder = new StepBuilder('', '');
2514
- step(builder);
2515
- const builtStep = builder.build();
2516
- this.config.steps.push(builtStep);
2517
- }
2867
+ inputs(inputs) {
2868
+ this.config.inputs = inputs;
2869
+ return this;
2870
+ }
2871
+ animation(animation) {
2872
+ this.config.animation = animation;
2873
+ return this;
2874
+ }
2875
+ /**
2876
+ * Add a custom body/content to the modal.
2877
+ */
2878
+ body(body) {
2879
+ return this.layoutBuilder.body(body);
2880
+ }
2881
+ /**
2882
+ * Add a field.
2883
+ */
2884
+ field(field) {
2885
+ return this.layoutBuilder.field(field);
2886
+ }
2887
+ /**
2888
+ * Add a field with a fluent validation builder.
2889
+ */
2890
+ fieldWithValidators(field) {
2891
+ return this.layoutBuilder.fieldWithValidators(field);
2892
+ }
2893
+ /**
2894
+ * Start a new row.
2895
+ */
2896
+ row(columns = 2) {
2897
+ return this.layoutBuilder.row(columns);
2898
+ }
2899
+ /**
2900
+ * Add a field to the current row.
2901
+ */
2902
+ addToRow(field, span = 1) {
2903
+ return this.layoutBuilder.addToRow(field, span);
2904
+ }
2905
+ /**
2906
+ * Declarative way to add a row.
2907
+ */
2908
+ addRow(columns, buildFn) {
2909
+ return this.layoutBuilder.addRow(columns, buildFn);
2910
+ }
2911
+ fieldGroup(arg1, arg2, arg3) {
2912
+ return this.layoutBuilder.fieldGroup(arg1, arg2, arg3);
2913
+ }
2914
+ build() {
2915
+ this.layoutBuilder.flushCurrentRow();
2916
+ return Object.freeze({ ...this.config });
2917
+ }
2918
+ }
2919
+
2920
+ class WizardModalBuilder extends BaseModalBuilder {
2921
+ constructor() {
2922
+ super({
2923
+ kind: ModalKind.WIZARD,
2924
+ steps: [],
2925
+ });
2926
+ }
2927
+ /**
2928
+ * Set initial values for the entire wizard.
2929
+ * Note: This will be merged with individual step initial values.
2930
+ */
2931
+ initialValue(value) {
2932
+ this.config.initialValue = value;
2933
+ return this;
2934
+ }
2935
+ body(body) {
2936
+ return super.body(body);
2937
+ }
2938
+ field(field) {
2939
+ return super.field(field);
2940
+ }
2941
+ row(columns = 2) {
2942
+ return super.row(columns);
2943
+ }
2944
+ addToRow(field, span = 1) {
2945
+ return super.addToRow(field, span);
2946
+ }
2947
+ addRow(columns, buildFn) {
2948
+ return super.addRow(columns, buildFn);
2949
+ }
2950
+ fieldGroup(arg1, arg2, arg3) {
2951
+ return super.fieldGroup(arg1, arg2, arg3);
2952
+ }
2953
+ step(step) {
2954
+ if (typeof step === 'function') {
2955
+ const builder = new StepBuilder('', '');
2956
+ step(builder);
2957
+ const builtStep = builder.build();
2958
+ this.config.steps.push(builtStep);
2959
+ }
2518
2960
  else {
2519
2961
  this.config.steps.push(step);
2520
2962
  }
@@ -2524,7 +2966,8 @@ class WizardModalBuilder extends BaseModalBuilder {
2524
2966
  const stepId = id || `step-${this.config.steps.length}`;
2525
2967
  const builder = new StepBuilder(stepId, title);
2526
2968
  buildFn(builder);
2527
- this.config.steps.push(builder.build());
2969
+ const builtStep = builder.build();
2970
+ this.config.steps.push(builtStep);
2528
2971
  return this;
2529
2972
  }
2530
2973
  startAt(stepId) {
@@ -2550,8 +2993,6 @@ class WizardModalBuilder extends BaseModalBuilder {
2550
2993
  }
2551
2994
 
2552
2995
  class FormModalBuilder extends BaseModalBuilder {
2553
- currentRow = [];
2554
- currentRowColumns = 1;
2555
2996
  constructor() {
2556
2997
  super({
2557
2998
  kind: ModalKind.FORM,
@@ -2559,64 +3000,27 @@ class FormModalBuilder extends BaseModalBuilder {
2559
3000
  rows: [],
2560
3001
  });
2561
3002
  }
2562
- /**
2563
- * Add a field as a full-width row (single column).
2564
- * This is the simple API — each field gets its own row.
2565
- */
3003
+ body(body) {
3004
+ return super.body(body);
3005
+ }
2566
3006
  field(field) {
2567
- this.flushCurrentRow();
2568
- this.config.fields.push(field);
2569
- this.config.rows.push({
2570
- columns: 1,
2571
- fields: [{ field, span: 1 }],
2572
- });
2573
- return this;
3007
+ return super.field(field);
2574
3008
  }
2575
- /**
2576
- * Start a new row with the specified number of columns.
2577
- * All subsequent `addToRow()` calls will add fields to this row
2578
- * until the next `row()` or `field()` call.
2579
- *
2580
- * @example
2581
- * .row(2)
2582
- * .addToRow({ kind: FieldKind.TEXT, key: 'firstName', label: 'First Name' })
2583
- * .addToRow({ kind: FieldKind.TEXT, key: 'lastName', label: 'Last Name' })
2584
- * .row(3)
2585
- * .addToRow({ kind: FieldKind.TEXT, key: 'city', label: 'City' }, 2)
2586
- * .addToRow({ kind: FieldKind.TEXT, key: 'zip', label: 'ZIP' })
2587
- */
2588
3009
  row(columns = 2) {
2589
- this.flushCurrentRow();
2590
- this.currentRowColumns = columns;
2591
- return this;
3010
+ return super.row(columns);
2592
3011
  }
2593
- /**
2594
- * Add a field to the current row started by `row()`.
2595
- * @param field - The field configuration
2596
- * @param span - How many columns this field should span (default: 1)
2597
- */
2598
3012
  addToRow(field, span = 1) {
2599
- this.config.fields.push(field);
2600
- this.currentRow.push({ field, span });
2601
- return this;
3013
+ return super.addToRow(field, span);
2602
3014
  }
2603
- flushCurrentRow() {
2604
- if (this.currentRow.length > 0) {
2605
- this.config.rows.push({
2606
- columns: this.currentRowColumns,
2607
- fields: [...this.currentRow],
2608
- });
2609
- this.currentRow = [];
2610
- this.currentRowColumns = 1;
2611
- }
3015
+ addRow(columns, buildFn) {
3016
+ return super.addRow(columns, buildFn);
2612
3017
  }
2613
3018
  layout(mode) {
2614
3019
  this.config.layout = mode;
2615
3020
  return this;
2616
3021
  }
2617
3022
  initialValue(value) {
2618
- this.config.initialValue = value;
2619
- return this;
3023
+ return this.layoutBuilder.initialValue(value);
2620
3024
  }
2621
3025
  submitMode(mode) {
2622
3026
  this.config.submitMode = mode;
@@ -2626,45 +3030,16 @@ class FormModalBuilder extends BaseModalBuilder {
2626
3030
  this.config.onComplete = handler;
2627
3031
  return this;
2628
3032
  }
2629
- /**
2630
- * Add form-level validators for cross-field validation.
2631
- * These receive the entire form value and return an error map or null.
2632
- */
2633
3033
  formValidators(validators) {
2634
- this.config.formValidators = validators;
2635
- return this;
3034
+ return this.layoutBuilder.formValidators(validators);
2636
3035
  }
2637
- /**
2638
- * Add Angular FormGroup-level validators.
2639
- * These are standard Angular ValidatorFn applied to the FormGroup itself.
2640
- */
2641
3036
  groupValidators(validators) {
2642
- this.config.groupValidators = validators;
2643
- return this;
3037
+ return this.layoutBuilder.groupValidators(validators);
2644
3038
  }
2645
- /**
2646
- * Add a field group with a section header.
2647
- * Groups visually separate fields with a title and optional description.
2648
- */
2649
- fieldGroup(group) {
2650
- this.flushCurrentRow();
2651
- if (!this.config.fieldGroups) {
2652
- this.config.fieldGroups = [];
2653
- }
2654
- // Also add group fields to the flat fields array for form control creation
2655
- group.fields.forEach(f => this.config.fields.push(f));
2656
- // Build rows for the group if not provided
2657
- if (!group.rows) {
2658
- group.rows = group.fields.map(f => ({
2659
- columns: 1,
2660
- fields: [{ field: f, span: 1 }],
2661
- }));
2662
- }
2663
- this.config.fieldGroups.push(group);
2664
- return this;
3039
+ fieldGroup(arg1, arg2, arg3) {
3040
+ return super.fieldGroup(arg1, arg2, arg3);
2665
3041
  }
2666
3042
  build() {
2667
- this.flushCurrentRow();
2668
3043
  return super.build();
2669
3044
  }
2670
3045
  }
@@ -2674,6 +3049,8 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
2674
3049
  super({
2675
3050
  kind: ModalKind.CONFIRMATION,
2676
3051
  message: '',
3052
+ fields: [],
3053
+ rows: [],
2677
3054
  });
2678
3055
  }
2679
3056
  message(text) {
@@ -2692,6 +3069,36 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
2692
3069
  this.config.cancel = action;
2693
3070
  return this;
2694
3071
  }
3072
+ body(body) {
3073
+ return super.body(body);
3074
+ }
3075
+ field(field) {
3076
+ return super.field(field);
3077
+ }
3078
+ row(columns = 2) {
3079
+ return super.row(columns);
3080
+ }
3081
+ addToRow(field, span = 1) {
3082
+ return super.addToRow(field, span);
3083
+ }
3084
+ addRow(columns, buildFn) {
3085
+ return super.addRow(columns, buildFn);
3086
+ }
3087
+ fieldGroup(arg1, arg2, arg3) {
3088
+ return super.fieldGroup(arg1, arg2, arg3);
3089
+ }
3090
+ initialValue(value) {
3091
+ return this.layoutBuilder.initialValue(value);
3092
+ }
3093
+ formValidators(validators) {
3094
+ return this.layoutBuilder.formValidators(validators);
3095
+ }
3096
+ groupValidators(validators) {
3097
+ return this.layoutBuilder.groupValidators(validators);
3098
+ }
3099
+ build() {
3100
+ return super.build();
3101
+ }
2695
3102
  }
2696
3103
 
2697
3104
  class CustomModalBuilder extends BaseModalBuilder {
@@ -2700,18 +3107,6 @@ class CustomModalBuilder extends BaseModalBuilder {
2700
3107
  kind: ModalKind.CUSTOM,
2701
3108
  });
2702
3109
  }
2703
- component(component) {
2704
- this.config.component = component;
2705
- return this;
2706
- }
2707
- template(template) {
2708
- this.config.template = template;
2709
- return this;
2710
- }
2711
- inputs(inputs) {
2712
- this.config.inputs = inputs;
2713
- return this;
2714
- }
2715
3110
  onComplete(handler) {
2716
3111
  this.config.onComplete = handler;
2717
3112
  return this;
@@ -2769,6 +3164,9 @@ class MnModalRef {
2769
3164
  // Trigger change detection on the shell component
2770
3165
  this.componentRef.changeDetectorRef.detectChanges();
2771
3166
  }
3167
+ get component() {
3168
+ return this.componentRef.instance;
3169
+ }
2772
3170
  destroy() {
2773
3171
  this.componentRef.destroy();
2774
3172
  }
@@ -3091,11 +3489,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3091
3489
  type: Output
3092
3490
  }] } });
3093
3491
 
3492
+ class MnCustomBodyHostComponent {
3493
+ config;
3494
+ modalRef;
3495
+ container;
3496
+ componentRef;
3497
+ ngOnInit() {
3498
+ setTimeout(() => this.loadContent(), 0);
3499
+ }
3500
+ loadContent() {
3501
+ if (!this.container)
3502
+ return;
3503
+ this.container.clear();
3504
+ if (this.config.component) {
3505
+ this.attachComponent(this.config.component);
3506
+ }
3507
+ else if (this.config.template) {
3508
+ this.attachTemplate(this.config.template);
3509
+ }
3510
+ }
3511
+ attachComponent(component) {
3512
+ this.componentRef = this.container.createComponent(component);
3513
+ // Pass inputs to the component
3514
+ if (this.config.inputs) {
3515
+ Object.entries(this.config.inputs).forEach(([key, value]) => {
3516
+ this.componentRef.instance[key] = value;
3517
+ });
3518
+ }
3519
+ // Pass modalRef if the component has a modalRef property
3520
+ const instance = this.componentRef.instance;
3521
+ if (instance && 'modalRef' in instance) {
3522
+ instance.modalRef = this.modalRef;
3523
+ }
3524
+ }
3525
+ attachTemplate(template) {
3526
+ this.container.createEmbeddedView(template, {
3527
+ $implicit: this.modalRef,
3528
+ modalRef: this.modalRef,
3529
+ });
3530
+ }
3531
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3532
+ 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 }] });
3533
+ }
3534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, decorators: [{
3535
+ type: Component,
3536
+ args: [{
3537
+ selector: 'mn-custom-body-host',
3538
+ standalone: true,
3539
+ imports: [CommonModule],
3540
+ template: '<ng-container #container></ng-container>',
3541
+ }]
3542
+ }], propDecorators: { config: [{
3543
+ type: Input
3544
+ }], modalRef: [{
3545
+ type: Input
3546
+ }], container: [{
3547
+ type: ViewChild,
3548
+ args: ['container', { read: ViewContainerRef }]
3549
+ }] } });
3550
+
3094
3551
  class MnFormBodyComponent {
3095
3552
  fb;
3096
3553
  config;
3097
3554
  modalRef;
3098
3555
  hideFooter = false;
3556
+ hideCustomBody = false;
3557
+ formStatusChange = new EventEmitter();
3558
+ inputFields;
3559
+ textareas;
3099
3560
  form;
3100
3561
  rows = [];
3101
3562
  fieldGroups = [];
@@ -3154,6 +3615,44 @@ class MnFormBodyComponent {
3154
3615
  this.initializeDataSources();
3155
3616
  this.initializeTableFields();
3156
3617
  this.subscribeToValueChanges();
3618
+ if (this.config.disabled || this.config.readOnly) {
3619
+ this.form.disable();
3620
+ }
3621
+ // Emit initial status
3622
+ setTimeout(() => {
3623
+ this.formStatusChange.emit(this.form.status);
3624
+ });
3625
+ }
3626
+ ngAfterViewInit() {
3627
+ // Small delay to ensure children are fully rendered and MnModalShell hasn't just stolen focus
3628
+ setTimeout(() => {
3629
+ this.applyAutoFocus();
3630
+ }, 100);
3631
+ }
3632
+ applyAutoFocus() {
3633
+ const autoFocusField = this.config.fields.find(f => f.autoFocus);
3634
+ if (!autoFocusField)
3635
+ return;
3636
+ const key = autoFocusField.key;
3637
+ // Small delay to ensure browser is ready to focus
3638
+ setTimeout(() => {
3639
+ // Try finding in MnInputField components
3640
+ const inputField = this.inputFields?.find(f => f.props.id === key);
3641
+ if (inputField) {
3642
+ inputField.focus();
3643
+ return;
3644
+ }
3645
+ // Try finding in MnTextarea components
3646
+ const textarea = this.textareas?.find(f => f.props?.id === key);
3647
+ if (textarea && typeof textarea.focus === 'function') {
3648
+ textarea.focus();
3649
+ return;
3650
+ }
3651
+ // Fallback to native element if possible
3652
+ const el = document.getElementById(key);
3653
+ if (el)
3654
+ el.focus();
3655
+ }, 50);
3157
3656
  }
3158
3657
  ngOnDestroy() {
3159
3658
  this.valueChangesSubscription?.unsubscribe();
@@ -3169,7 +3668,15 @@ class MnFormBodyComponent {
3169
3668
  }
3170
3669
  const validators = fieldConfig.validators || [];
3171
3670
  const asyncValidators = fieldConfig.asyncValidators || [];
3172
- formControls[field.key] = [initialValue, validators, asyncValidators];
3671
+ const updateOn = fieldConfig.updateOn || 'change';
3672
+ formControls[field.key] = [
3673
+ initialValue,
3674
+ {
3675
+ validators,
3676
+ asyncValidators,
3677
+ updateOn
3678
+ }
3679
+ ];
3173
3680
  });
3174
3681
  this.form = this.fb.group(formControls);
3175
3682
  // Apply Angular FormGroup-level validators
@@ -3187,10 +3694,10 @@ class MnFormBodyComponent {
3187
3694
  });
3188
3695
  }
3189
3696
  isFieldReadOnly(field) {
3190
- return field.readOnly === true;
3697
+ return this.config.readOnly === true || field.readOnly === true;
3191
3698
  }
3192
3699
  isFieldDisabled(field) {
3193
- return field.disabled === true;
3700
+ return this.config.disabled === true || field.disabled === true;
3194
3701
  }
3195
3702
  /** Track which field groups are currently visible */
3196
3703
  groupVisibility = {};
@@ -3198,12 +3705,25 @@ class MnFormBodyComponent {
3198
3705
  if (this.config.rows && this.config.rows.length > 0) {
3199
3706
  this.rows = this.config.rows;
3200
3707
  }
3201
- else if (!this.config.fieldGroups || this.config.fieldGroups.length === 0) {
3202
- // Fallback: each field gets its own full-width row
3203
- this.rows = this.config.fields.map(field => ({
3204
- columns: 1,
3205
- fields: [{ field, span: 1 }],
3206
- }));
3708
+ else {
3709
+ // Create rows for fields that are not in any row or group
3710
+ const fieldsInGroups = new Set();
3711
+ (this.config.fieldGroups || []).forEach(g => {
3712
+ (g.rows || []).forEach(r => r.fields.forEach(f => fieldsInGroups.add(f.field.key)));
3713
+ // Also check if group has flat fields list (compatibility)
3714
+ if (g.fields) {
3715
+ g.fields.forEach((f) => fieldsInGroups.add(f.key));
3716
+ }
3717
+ });
3718
+ const fieldsInRows = new Set();
3719
+ (this.config.rows || []).forEach(r => r.fields.forEach(f => fieldsInRows.add(f.field.key)));
3720
+ const standaloneFields = this.config.fields.filter(f => !fieldsInGroups.has(f.key) && !fieldsInRows.has(f.key));
3721
+ if (standaloneFields.length > 0) {
3722
+ this.rows = standaloneFields.map(field => ({
3723
+ columns: 1,
3724
+ fields: [{ field, span: 1 }],
3725
+ }));
3726
+ }
3207
3727
  }
3208
3728
  // Build field groups
3209
3729
  this.fieldGroups = this.config.fieldGroups || [];
@@ -3482,6 +4002,8 @@ class MnFormBodyComponent {
3482
4002
  this.runFormValidators();
3483
4003
  // Reload data sources that depend on changed fields
3484
4004
  this.reloadDependentDataSources(formValue);
4005
+ // Emit status change
4006
+ this.formStatusChange.emit(this.form.status);
3485
4007
  });
3486
4008
  }
3487
4009
  previousFormValue = {};
@@ -3601,17 +4123,27 @@ class MnFormBodyComponent {
3601
4123
  }
3602
4124
  }
3603
4125
  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 });
3604
- 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"] }] });
4126
+ 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", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "<mn-custom-body-host\n *ngIf=\"(config.component || config.template) && !hideCustomBody\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n></mn-custom-body-host>\n\n<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 mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <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 readonly: isFieldReadOnly(rowField.field)\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 readonly: isFieldReadOnly(rowField.field)\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)) || isFieldDisabled(rowField.field)\"\n [attr.disabled]=\"isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field) ? '' : null\"\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 disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\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 readonly: isFieldReadOnly(rowField.field)\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 readonly: isFieldReadOnly(rowField.field)\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 autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\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 disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\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=\"rows.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"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
3605
4127
  }
3606
4128
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
3607
4129
  type: Component,
3608
- 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"] }]
4130
+ args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "<mn-custom-body-host\n *ngIf=\"(config.component || config.template) && !hideCustomBody\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n></mn-custom-body-host>\n\n<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 mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <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 readonly: isFieldReadOnly(rowField.field)\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 readonly: isFieldReadOnly(rowField.field)\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)) || isFieldDisabled(rowField.field)\"\n [attr.disabled]=\"isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field) ? '' : null\"\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 disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\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 readonly: isFieldReadOnly(rowField.field)\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 readonly: isFieldReadOnly(rowField.field)\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 autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\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 disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\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=\"rows.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"] }]
3609
4131
  }], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
3610
4132
  type: Input
3611
4133
  }], modalRef: [{
3612
4134
  type: Input
3613
4135
  }], hideFooter: [{
3614
4136
  type: Input
4137
+ }], hideCustomBody: [{
4138
+ type: Input
4139
+ }], formStatusChange: [{
4140
+ type: Output
4141
+ }], inputFields: [{
4142
+ type: ViewChildren,
4143
+ args: [MnInputField]
4144
+ }], textareas: [{
4145
+ type: ViewChildren,
4146
+ args: [MnTextarea]
3615
4147
  }] } });
3616
4148
 
3617
4149
  class MnWizardBodyComponent {
@@ -3646,6 +4178,11 @@ class MnWizardBodyComponent {
3646
4178
  // Pre-build form configs for all form-driven steps
3647
4179
  for (const step of this.config.steps) {
3648
4180
  if (step.fields && step.fields.length > 0) {
4181
+ // Merge top-level initialValue with step-level initialValue
4182
+ const mergedInitialValue = {
4183
+ ...(this.config.initialValue || {}),
4184
+ ...(step.initialValue || {})
4185
+ };
3649
4186
  this.stepFormConfigs[step.id] = {
3650
4187
  kind: ModalKind.FORM,
3651
4188
  fields: step.fields,
@@ -3653,7 +4190,9 @@ class MnWizardBodyComponent {
3653
4190
  fieldGroups: step.fieldGroups,
3654
4191
  formValidators: step.formValidators,
3655
4192
  groupValidators: step.groupValidators,
3656
- initialValue: step.initialValue,
4193
+ initialValue: mergedInitialValue,
4194
+ readOnly: this.config.readOnly,
4195
+ disabled: this.config.disabled
3657
4196
  };
3658
4197
  }
3659
4198
  }
@@ -3666,6 +4205,10 @@ class MnWizardBodyComponent {
3666
4205
  // Subscribe to form bodies list changes to track validity
3667
4206
  this.formBodiesSubscription = this.formBodies.changes.subscribe(() => {
3668
4207
  this.trackCurrentStepValidity();
4208
+ // Apply autofocus when form bodies change (e.g. step navigation)
4209
+ setTimeout(() => {
4210
+ this.getCurrentFormBody()?.applyAutoFocus();
4211
+ }, 100);
3669
4212
  });
3670
4213
  // Initial validity check
3671
4214
  this.trackCurrentStepValidity();
@@ -3891,11 +4434,11 @@ class MnWizardBodyComponent {
3891
4434
  }
3892
4435
  }
3893
4436
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3894
- 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" }] });
4437
+ 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 <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"block\"\n ></mn-custom-body-host>\n\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 [hideCustomBody]=\"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 *ngIf=\"!currentStep?.hideBack\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (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 {{ currentStep?.nextLabel || 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 {{ currentStep?.nextLabel || (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: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
3895
4438
  }
3896
4439
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
3897
4440
  type: Component,
3898
- 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" }]
4441
+ args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent], template: "<div class=\"flex flex-col gap-6\">\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"block\"\n ></mn-custom-body-host>\n\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 [hideCustomBody]=\"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 *ngIf=\"!currentStep?.hideBack\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (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 {{ currentStep?.nextLabel || 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 {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n </div>\n</div>\n" }]
3899
4442
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
3900
4443
  type: Input
3901
4444
  }], modalRef: [{
@@ -3906,10 +4449,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3906
4449
  }] } });
3907
4450
 
3908
4451
  class MnConfirmationBodyComponent {
4452
+ cdr;
3909
4453
  config;
3910
4454
  modalRef;
4455
+ formBody;
4456
+ confirmButtonStatus = 'VALID';
4457
+ hasFormFields = false;
4458
+ constructor(cdr) {
4459
+ this.cdr = cdr;
4460
+ }
4461
+ ngOnInit() {
4462
+ this.hasFormFields = !!((this.config.fields && this.config.fields.length > 0) ||
4463
+ (this.config.fieldGroups && this.config.fieldGroups.length > 0) ||
4464
+ (this.config.rows && this.config.rows.length > 0));
4465
+ }
4466
+ onFormStatusChange(status) {
4467
+ this.confirmButtonStatus = status;
4468
+ this.cdr.markForCheck();
4469
+ this.cdr.detectChanges();
4470
+ }
3911
4471
  async confirm() {
3912
- const result = true;
4472
+ if (this.hasFormFields && this.formBody?.form.invalid) {
4473
+ this.formBody.form.markAllAsTouched();
4474
+ return;
4475
+ }
4476
+ const result = (this.hasFormFields ? this.formBody?.form.value : true);
3913
4477
  if (this.config.confirm?.handler) {
3914
4478
  await this.config.confirm.handler.handle(result);
3915
4479
  }
@@ -3964,75 +4528,31 @@ class MnConfirmationBodyComponent {
3964
4528
  return 'outline';
3965
4529
  }
3966
4530
  }
3967
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3968
- 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"] }] });
3969
- }
3970
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
3971
- type: Component,
3972
- 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" }]
3973
- }], propDecorators: { config: [{
3974
- type: Input
3975
- }], modalRef: [{
3976
- type: Input
3977
- }] } });
3978
-
3979
- class MnCustomBodyHostComponent {
3980
- config;
3981
- modalRef;
3982
- container;
3983
- componentRef;
3984
- ngOnInit() {
3985
- setTimeout(() => this.loadContent(), 0);
3986
- }
3987
- loadContent() {
3988
- if (!this.container)
3989
- return;
3990
- this.container.clear();
3991
- if (this.config.component) {
3992
- this.attachComponent(this.config.component);
3993
- }
3994
- else if (this.config.template) {
3995
- this.attachTemplate(this.config.template);
3996
- }
4531
+ asAny(val) {
4532
+ return val;
3997
4533
  }
3998
- attachComponent(component) {
3999
- this.componentRef = this.container.createComponent(component);
4000
- // Pass inputs to the component
4001
- if (this.config.inputs) {
4002
- Object.entries(this.config.inputs).forEach(([key, value]) => {
4003
- this.componentRef.instance[key] = value;
4004
- });
4005
- }
4006
- // Pass modalRef if the component has a modalRef property
4007
- const instance = this.componentRef.instance;
4008
- if (instance && 'modalRef' in instance) {
4009
- instance.modalRef = this.modalRef;
4534
+ get isConfirmDisabled() {
4535
+ if (this.hasFormFields) {
4536
+ return this.confirmButtonStatus !== 'VALID';
4010
4537
  }
4538
+ return false;
4011
4539
  }
4012
- attachTemplate(template) {
4013
- this.container.createEmbeddedView(template, {
4014
- $implicit: this.modalRef,
4015
- modalRef: this.modalRef,
4016
- });
4540
+ asField(field) {
4541
+ return field;
4017
4542
  }
4018
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4019
- 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 }] });
4543
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
4544
+ 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" }, viewQueries: [{ propertyName: "formBody", first: true, predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n\n <div class=\"flex flex-col items-center text-center gap-6\">\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]\" *ngIf=\"config.message\">\n {{ config.message }}\n </div>\n </div>\n\n <!-- Form Fields / Rows -->\n <mn-form-body\n *ngIf=\"hasFormFields\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\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 disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\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"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
4020
4545
  }
4021
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, decorators: [{
4546
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
4022
4547
  type: Component,
4023
- args: [{
4024
- selector: 'mn-custom-body-host',
4025
- standalone: true,
4026
- imports: [CommonModule],
4027
- template: '<ng-container #container></ng-container>',
4028
- }]
4029
- }], propDecorators: { config: [{
4548
+ args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n\n <div class=\"flex flex-col items-center text-center gap-6\">\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]\" *ngIf=\"config.message\">\n {{ config.message }}\n </div>\n </div>\n\n <!-- Form Fields / Rows -->\n <mn-form-body\n *ngIf=\"hasFormFields\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\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 disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
4549
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
4030
4550
  type: Input
4031
4551
  }], modalRef: [{
4032
4552
  type: Input
4033
- }], container: [{
4553
+ }], formBody: [{
4034
4554
  type: ViewChild,
4035
- args: ['container', { read: ViewContainerRef }]
4555
+ args: [MnFormBodyComponent]
4036
4556
  }] } });
4037
4557
 
4038
4558
  class MnModalShellComponent {
@@ -4040,6 +4560,7 @@ class MnModalShellComponent {
4040
4560
  config;
4041
4561
  modalRef;
4042
4562
  isClosing = false;
4563
+ isStacked = false;
4043
4564
  ModalKind = ModalKind;
4044
4565
  previouslyFocusedElement = null;
4045
4566
  focusTrapListener = null;
@@ -4116,7 +4637,12 @@ class MnModalShellComponent {
4116
4637
  get hostClasses() {
4117
4638
  const size = this.config.size || ModalSize.MD;
4118
4639
  const closing = this.isClosing ? ' closing' : '';
4119
- return `modal-shell modal-${size}${closing}`;
4640
+ const animType = typeof this.config.animation === 'string'
4641
+ ? this.config.animation
4642
+ : this.config.animation?.type || 'slide';
4643
+ const animation = ` anim-${animType}`;
4644
+ const stacked = this.isStacked ? ' is-stacked' : '';
4645
+ return `modal-shell modal-${size}${closing}${animation}${stacked}`;
4120
4646
  }
4121
4647
  startClosing() {
4122
4648
  this.isClosing = true;
@@ -4167,6 +4693,17 @@ class MnModalShellComponent {
4167
4693
  get showCloseButton() {
4168
4694
  return this.config.closeMode !== CloseMode.DISABLED;
4169
4695
  }
4696
+ get animationClass() {
4697
+ const animType = typeof this.config.animation === 'string'
4698
+ ? this.config.animation
4699
+ : this.config.animation?.type || 'slide';
4700
+ switch (animType) {
4701
+ case 'fade': return 'animate-[fadeIn_0.2s_ease-in-out]';
4702
+ case 'zoom': return 'animate-[zoomIn_0.2s_ease-in-out]';
4703
+ case 'slide':
4704
+ default: return 'animate-[slideIn_0.2s_ease-in-out]';
4705
+ }
4706
+ }
4170
4707
  // =========================
4171
4708
  // Footer Actions
4172
4709
  // =========================
@@ -4248,7 +4785,7 @@ class MnModalShellComponent {
4248
4785
  }
4249
4786
  }
4250
4787
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
4251
- 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"] }] });
4788
+ 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\"\n [ngClass]=\"[containerSizeClass, animationClass]\"\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;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}\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", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }] });
4252
4789
  }
4253
4790
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
4254
4791
  type: Component,
@@ -4259,7 +4796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4259
4796
  MnConfirmationBodyComponent,
4260
4797
  MnCustomBodyHostComponent,
4261
4798
  MnButton,
4262
- ], 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"] }]
4799
+ ], 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\"\n [ngClass]=\"[containerSizeClass, animationClass]\"\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;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}\n"] }]
4263
4800
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
4264
4801
  type: Input
4265
4802
  }], modalRef: [{
@@ -4275,6 +4812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4275
4812
  class MnModalService {
4276
4813
  appRef = inject(ApplicationRef);
4277
4814
  injector = inject(EnvironmentInjector);
4815
+ modalStack = [];
4278
4816
  open(config) {
4279
4817
  // Create the modal shell component
4280
4818
  const componentRef = createComponent(MnModalShellComponent, {
@@ -4285,6 +4823,12 @@ class MnModalService {
4285
4823
  // Create modal ref
4286
4824
  const modalRef = new MnModalRef(componentRef, config);
4287
4825
  componentRef.instance.modalRef = modalRef;
4826
+ // Update stack and dim previous modal
4827
+ if (this.modalStack.length > 0) {
4828
+ const prevModal = this.modalStack[this.modalStack.length - 1];
4829
+ prevModal.component.isStacked = true;
4830
+ }
4831
+ this.modalStack.push(modalRef);
4288
4832
  // Attach to application
4289
4833
  this.appRef.attachView(componentRef.hostView);
4290
4834
  const domElem = componentRef.location.nativeElement;
@@ -4293,6 +4837,15 @@ class MnModalService {
4293
4837
  modalRef.afterClosed$.subscribe(() => {
4294
4838
  this.appRef.detachView(componentRef.hostView);
4295
4839
  domElem.remove();
4840
+ // Update stack
4841
+ const index = this.modalStack.indexOf(modalRef);
4842
+ if (index > -1) {
4843
+ this.modalStack.splice(index, 1);
4844
+ if (this.modalStack.length > 0) {
4845
+ const topModal = this.modalStack[this.modalStack.length - 1];
4846
+ topModal.component.isStacked = false;
4847
+ }
4848
+ }
4296
4849
  });
4297
4850
  return modalRef;
4298
4851
  }
@@ -4308,6 +4861,1274 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4308
4861
 
4309
4862
  // Types
4310
4863
 
4864
+ /**
4865
+ * Available calendar view modes.
4866
+ */
4867
+ var CalendarView;
4868
+ (function (CalendarView) {
4869
+ CalendarView["MONTH"] = "MONTH";
4870
+ CalendarView["WEEK"] = "WEEK";
4871
+ CalendarView["DAY"] = "DAY";
4872
+ })(CalendarView || (CalendarView = {}));
4873
+ /**
4874
+ * Builds locale-derived day name arrays from a BCP 47 locale string.
4875
+ * Uses January 1 2024 (a Monday) as the reference date.
4876
+ */
4877
+ function buildDayNames(locale) {
4878
+ const base = new Date(2024, 0, 1); // 2024-01-01 is a Monday
4879
+ const short = [];
4880
+ const long = [];
4881
+ for (let i = 0; i < 7; i++) {
4882
+ const d = new Date(base);
4883
+ d.setDate(base.getDate() + i);
4884
+ short.push(d.toLocaleDateString(locale, { weekday: 'short' }));
4885
+ long.push(d.toLocaleDateString(locale, { weekday: 'long' }));
4886
+ }
4887
+ return { short, long };
4888
+ }
4889
+ /** Default calendar configuration values. */
4890
+ const DEFAULT_CALENDAR_CONFIG = (() => {
4891
+ const locale = 'en-US';
4892
+ const names = buildDayNames(locale);
4893
+ return {
4894
+ startHour: 7,
4895
+ endHour: 22,
4896
+ locale,
4897
+ todayLabel: 'Today',
4898
+ upcomingEventsTitle: 'Upcoming events',
4899
+ viewLabels: { MONTH: 'Month', WEEK: 'Week', DAY: 'Day' },
4900
+ shortDayNames: names.short,
4901
+ longDayNames: names.long,
4902
+ mobileBreakpoint: 768,
4903
+ };
4904
+ })();
4905
+ /**
4906
+ * Injection token for the resolved calendar configuration.
4907
+ *
4908
+ * Prefer using {@link MN_CALENDAR_CONFIG} with `provideMnComponentConfig`
4909
+ * so that settings can be managed via `mn-config.json5`. This token is
4910
+ * kept for backward compatibility and manual `providers` usage.
4911
+ *
4912
+ * @example
4913
+ * ```ts
4914
+ * providers: [
4915
+ * { provide: CALENDAR_CONFIG, useValue: { startHour: 8, endHour: 20, locale: 'nl-NL' } }
4916
+ * ]
4917
+ * ```
4918
+ */
4919
+ const CALENDAR_CONFIG = new InjectionToken('CalendarConfig', {
4920
+ providedIn: 'root',
4921
+ factory: () => DEFAULT_CALENDAR_CONFIG
4922
+ });
4923
+ /**
4924
+ * Injection token resolved via `MnConfigService` (the `mn-config.json5` system).
4925
+ *
4926
+ * Use the helper {@link provideMnCalendarConfig} in the component's `providers`
4927
+ * array so that calendar settings are read from the config file and support
4928
+ * `$translate` markers, section scoping, and instance-id overrides.
4929
+ *
4930
+ * Component name in the config file: `'mn-calendar'`.
4931
+ *
4932
+ * @example
4933
+ * ```json5
4934
+ * // mn-config.json5
4935
+ * {
4936
+ * defaults: {
4937
+ * "mn-calendar": {
4938
+ * startHour: 8,
4939
+ * endHour: 20,
4940
+ * locale: "nl-NL",
4941
+ * todayLabel: { $translate: "calendar.today" }
4942
+ * }
4943
+ * }
4944
+ * }
4945
+ * ```
4946
+ */
4947
+ const MN_CALENDAR_CONFIG = new InjectionToken('MN_CALENDAR_CONFIG');
4948
+ /** Component name used to look up calendar settings in `mn-config.json5`. */
4949
+ const MN_CALENDAR_COMPONENT_NAME = 'mn-calendar';
4950
+ /**
4951
+ * Provider helper that wires the calendar into the `mn-config` system.
4952
+ *
4953
+ * Add this to the `providers` array of the component (or module) that hosts
4954
+ * `<app-calendar-view>`. It reads defaults and overrides from `mn-config.json5`
4955
+ * under the key `"mn-calendar"` and provides them via {@link MN_CALENDAR_CONFIG}.
4956
+ *
4957
+ * @param initial — optional partial defaults merged before config-file values.
4958
+ */
4959
+ function provideMnCalendarConfig(initial) {
4960
+ return provideMnComponentConfig(MN_CALENDAR_CONFIG, MN_CALENDAR_COMPONENT_NAME, initial);
4961
+ }
4962
+ /**
4963
+ * Merges a partial config with defaults, re-deriving day names from locale when needed.
4964
+ */
4965
+ function resolveCalendarConfig(partial) {
4966
+ if (!partial)
4967
+ return { ...DEFAULT_CALENDAR_CONFIG };
4968
+ const locale = partial.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
4969
+ const names = buildDayNames(locale);
4970
+ return {
4971
+ ...DEFAULT_CALENDAR_CONFIG,
4972
+ ...partial,
4973
+ locale,
4974
+ shortDayNames: partial.shortDayNames ?? names.short,
4975
+ longDayNames: partial.longDayNames ?? names.long,
4976
+ };
4977
+ }
4978
+
4979
+ /**
4980
+ * Injection token for the calendar date formatter.
4981
+ *
4982
+ * @example
4983
+ * ```ts
4984
+ * providers: [
4985
+ * { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
4986
+ * ]
4987
+ * ```
4988
+ */
4989
+ const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
4990
+
4991
+ /**
4992
+ * Default implementation of {@link CalendarDateFormatter} that uses the
4993
+ * browser's `Intl.DateTimeFormat` API for locale-aware formatting.
4994
+ *
4995
+ * The locale is read from the injected {@link CALENDAR_CONFIG}. If no config
4996
+ * is provided, `'en-US'` is used as the fallback.
4997
+ *
4998
+ * This service has no dependency on `@ngx-translate` or any other i18n library,
4999
+ * so the calendar library works out of the box. Consumers can replace it with
5000
+ * their own implementation via the `CALENDAR_DATE_FORMATTER` injection token.
5001
+ */
5002
+ class DefaultCalendarDateFormatter {
5003
+ locale;
5004
+ constructor(config) {
5005
+ this.locale = config?.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
5006
+ }
5007
+ /** Formats an hour and minute pair into a locale time string (e.g. "09:00 AM"). */
5008
+ formatTimeI(hour, minute) {
5009
+ const date = new Date();
5010
+ date.setHours(hour, minute, 0, 0);
5011
+ return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
5012
+ }
5013
+ /** Formats the time portion of a Date (e.g. "2:30 PM"). Returns empty string for undefined. */
5014
+ formatTime(date) {
5015
+ if (!date)
5016
+ return Promise.resolve('');
5017
+ return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
5018
+ }
5019
+ /** Formats a Date as a full date-time string (e.g. "May 15, 2026, 02:30 PM"). */
5020
+ formatDateTime(date) {
5021
+ return of(date.toLocaleString(this.locale, {
5022
+ year: 'numeric', month: 'short', day: 'numeric',
5023
+ hour: '2-digit', minute: '2-digit'
5024
+ }));
5025
+ }
5026
+ /** Formats a Date as a date-only string (e.g. "May 15, 2026"). */
5027
+ formatDate(date) {
5028
+ return of(date.toLocaleDateString(this.locale, {
5029
+ year: 'numeric', month: 'short', day: 'numeric'
5030
+ }));
5031
+ }
5032
+ /** Formats a Date as `YYYY-MM-DD` for use in `<input type="date">` controls. */
5033
+ formatDateForFormControl(date) {
5034
+ const y = date.getFullYear();
5035
+ const m = String(date.getMonth() + 1).padStart(2, '0');
5036
+ const d = String(date.getDate()).padStart(2, '0');
5037
+ return `${y}-${m}-${d}`;
5038
+ }
5039
+ /** Returns `true` if both dates fall on the same calendar day. */
5040
+ isSameDay(date1, date2) {
5041
+ return date1.getFullYear() === date2.getFullYear()
5042
+ && date1.getMonth() === date2.getMonth()
5043
+ && date1.getDate() === date2.getDate();
5044
+ }
5045
+ /** Formats a Date as "Month Year" (e.g. "January 2026"). */
5046
+ formatMonthName(date) {
5047
+ return Promise.resolve(date.toLocaleString(this.locale, { month: 'long', year: 'numeric' }));
5048
+ }
5049
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, deps: [{ token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
5050
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter });
5051
+ }
5052
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, decorators: [{
5053
+ type: Injectable
5054
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5055
+ type: Optional
5056
+ }, {
5057
+ type: Inject,
5058
+ args: [CALENDAR_CONFIG]
5059
+ }] }] });
5060
+
5061
+ /**
5062
+ * Month grid view showing a 7×6 grid of day cells.
5063
+ *
5064
+ * Each cell displays the day number and up to 3 coloured dots representing
5065
+ * events on that day. Clicking a cell emits `dayClicked`.
5066
+ */
5067
+ class CalendarMonthComponent {
5068
+ /** The date whose month is displayed. */
5069
+ focusDay;
5070
+ /** Observable that emits the full event list whenever it changes. */
5071
+ eventsChanged;
5072
+ /** Observable that emits when the focus day changes. */
5073
+ focusDayChanged;
5074
+ /** Resolved calendar configuration passed from the parent view. */
5075
+ config;
5076
+ /** Emits the date of a clicked day cell. */
5077
+ dayClicked = new EventEmitter();
5078
+ monthItems = [];
5079
+ longDayNames;
5080
+ events = [];
5081
+ destroy$ = new Subject();
5082
+ formatter;
5083
+ constructor() {
5084
+ this.formatter = new DefaultCalendarDateFormatter();
5085
+ this.longDayNames = DEFAULT_CALENDAR_CONFIG.longDayNames;
5086
+ }
5087
+ ngOnInit() {
5088
+ const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5089
+ this.longDayNames = resolved.longDayNames;
5090
+ this.buildMonth();
5091
+ if (this.eventsChanged) {
5092
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5093
+ this.events = events;
5094
+ this.buildMonth();
5095
+ });
5096
+ }
5097
+ if (this.focusDayChanged) {
5098
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5099
+ this.focusDay = date;
5100
+ this.buildMonth();
5101
+ });
5102
+ }
5103
+ }
5104
+ ngOnDestroy() {
5105
+ this.destroy$.next();
5106
+ this.destroy$.complete();
5107
+ }
5108
+ /** Emits the clicked day's date. */
5109
+ onDayClick(date) {
5110
+ this.dayClicked.emit(date);
5111
+ }
5112
+ /** trackBy for day name headers. */
5113
+ trackByDayName(index) {
5114
+ return index;
5115
+ }
5116
+ /** trackBy for month grid cells. */
5117
+ trackByMonthItem(_index, item) {
5118
+ return item.date.getTime();
5119
+ }
5120
+ /** trackBy for event dots. */
5121
+ trackByEventDot(_index, event) {
5122
+ return event.id;
5123
+ }
5124
+ /** Builds the 42-cell month grid (6 rows × 7 columns). */
5125
+ buildMonth() {
5126
+ if (!this.focusDay)
5127
+ return;
5128
+ const year = this.focusDay.getFullYear();
5129
+ const month = this.focusDay.getMonth();
5130
+ const firstDay = new Date(year, month, 1);
5131
+ const lastDay = new Date(year, month + 1, 0);
5132
+ let startOffset = firstDay.getDay() - 1;
5133
+ if (startOffset < 0)
5134
+ startOffset = 6;
5135
+ const today = new Date();
5136
+ this.monthItems = [];
5137
+ for (let i = startOffset - 1; i >= 0; i--) {
5138
+ const date = new Date(year, month, -i);
5139
+ this.monthItems.push(this.createMonthItem(date, false, today));
5140
+ }
5141
+ for (let d = 1; d <= lastDay.getDate(); d++) {
5142
+ const date = new Date(year, month, d);
5143
+ this.monthItems.push(this.createMonthItem(date, true, today));
5144
+ }
5145
+ const remaining = 42 - this.monthItems.length;
5146
+ for (let i = 1; i <= remaining; i++) {
5147
+ const date = new Date(year, month + 1, i);
5148
+ this.monthItems.push(this.createMonthItem(date, false, today));
5149
+ }
5150
+ }
5151
+ createMonthItem(date, isCurrentMonth, today) {
5152
+ const isToday = this.formatter.isSameDay(date, today);
5153
+ const dayEvents = this.events.filter(e => this.formatter.isSameDay(e.startTime, date) ||
5154
+ this.formatter.isSameDay(e.endTime, date) ||
5155
+ (e.startTime < date && e.endTime > date));
5156
+ return {
5157
+ date,
5158
+ dayNumber: date.getDate(),
5159
+ isCurrentMonth,
5160
+ isToday,
5161
+ events: dayEvents
5162
+ };
5163
+ }
5164
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5165
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "app-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\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"] }] });
5166
+ }
5167
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
5168
+ type: Component,
5169
+ args: [{ selector: 'app-calendar-month', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\n"] }]
5170
+ }], ctorParameters: () => [], propDecorators: { focusDay: [{
5171
+ type: Input
5172
+ }], eventsChanged: [{
5173
+ type: Input
5174
+ }], focusDayChanged: [{
5175
+ type: Input
5176
+ }], config: [{
5177
+ type: Input
5178
+ }], dayClicked: [{
5179
+ type: Output
5180
+ }] } });
5181
+
5182
+ /**
5183
+ * Service that computes the visual layout of calendar events within a
5184
+ * time-grid (week or day view).
5185
+ *
5186
+ * Responsibilities:
5187
+ * - Splitting multi-day events into per-day segments.
5188
+ * - Assigning non-overlapping column indices to concurrent events.
5189
+ * - Computing the width (column span) each event should occupy.
5190
+ *
5191
+ * This service is stateless — all state is passed via method parameters.
5192
+ * Provide it per-component (not root) so each view gets its own instance.
5193
+ */
5194
+ class CalendarEventLayoutService {
5195
+ /**
5196
+ * Returns `true` when two time ranges overlap (exclusive boundaries).
5197
+ */
5198
+ eventsOverlap(startA, endA, startB, endB) {
5199
+ return startA < endB && startB < endA;
5200
+ }
5201
+ /**
5202
+ * Returns all events whose time range overlaps the given `[start, end)` window.
5203
+ */
5204
+ getAllEventsOnSpecificTime(events, start, end) {
5205
+ return events.filter(e => this.eventsOverlap(e.startTime, e.endTime, start, end));
5206
+ }
5207
+ /**
5208
+ * Splits multi-day events into per-day segments that fit within the
5209
+ * visible hour range (`startHour`–`endHour`) and date range.
5210
+ *
5211
+ * Single-day events are shallow-copied as-is. Multi-day events produce
5212
+ * one segment per day with `continued` / `continuedEnd` flags set.
5213
+ */
5214
+ calculateMultiDayEvents(events, startHour, endHour, rangeStart, rangeEnd) {
5215
+ const result = [];
5216
+ for (const event of events) {
5217
+ const eventStart = new Date(event.startTime);
5218
+ const eventEnd = new Date(event.endTime);
5219
+ if (eventStart.toDateString() === eventEnd.toDateString()) {
5220
+ result.push({ ...event });
5221
+ continue;
5222
+ }
5223
+ const current = new Date(eventStart);
5224
+ let isFirst = true;
5225
+ while (current < eventEnd && current < rangeEnd) {
5226
+ if (current >= rangeStart) {
5227
+ const dayStart = new Date(current);
5228
+ const dayEnd = new Date(current);
5229
+ if (isFirst) {
5230
+ dayEnd.setHours(endHour, 0, 0, 0);
5231
+ }
5232
+ else {
5233
+ dayStart.setHours(startHour, 0, 0, 0);
5234
+ if (current.toDateString() === eventEnd.toDateString()) {
5235
+ dayEnd.setHours(eventEnd.getHours(), eventEnd.getMinutes(), 0, 0);
5236
+ }
5237
+ else {
5238
+ dayEnd.setHours(endHour, 0, 0, 0);
5239
+ }
5240
+ }
5241
+ result.push({
5242
+ ...event,
5243
+ startTime: isFirst ? eventStart : dayStart,
5244
+ endTime: current.toDateString() === eventEnd.toDateString() ? eventEnd : dayEnd,
5245
+ continued: !isFirst,
5246
+ continuedEnd: current.toDateString() !== eventEnd.toDateString()
5247
+ });
5248
+ }
5249
+ isFirst = false;
5250
+ current.setDate(current.getDate() + 1);
5251
+ current.setHours(0, 0, 0, 0);
5252
+ }
5253
+ }
5254
+ return result;
5255
+ }
5256
+ /**
5257
+ * Assigns a zero-based `column` index to each event so that overlapping
5258
+ * events occupy different columns.
5259
+ *
5260
+ * Events are processed in start-time order (longest duration first for ties).
5261
+ * Each event gets the earliest column not already occupied by an overlapping event.
5262
+ */
5263
+ assignColumnsToEvents(events) {
5264
+ const sorted = [...events].sort((a, b) => {
5265
+ const diff = a.startTime.getTime() - b.startTime.getTime();
5266
+ if (diff !== 0)
5267
+ return diff;
5268
+ return (b.endTime.getTime() - b.startTime.getTime()) - (a.endTime.getTime() - a.startTime.getTime());
5269
+ });
5270
+ for (const event of sorted) {
5271
+ event.column = this.findEarliestPossibleColumn(event, sorted);
5272
+ }
5273
+ }
5274
+ /**
5275
+ * Assigns a `width` (column span) to each event, expanding it to fill
5276
+ * unused columns to its right within the overlapping group.
5277
+ */
5278
+ assignWidthsToEvents(events, scanStart, scanEnd) {
5279
+ for (const event of events) {
5280
+ const overlapping = this.getAllEventsOnSpecificTime(events, event.startTime, event.endTime);
5281
+ const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
5282
+ const biggestPossible = this.findBiggestPossibleWidth(event, events, scanStart, scanEnd);
5283
+ event.width = Math.max(1, biggestPossible);
5284
+ if ((event.column ?? 0) + event.width > maxCol + 1) {
5285
+ event.width = maxCol + 1 - (event.column ?? 0);
5286
+ }
5287
+ if (event.width < 1)
5288
+ event.width = 1;
5289
+ }
5290
+ }
5291
+ /** Finds the lowest column index not occupied by any overlapping event. */
5292
+ findEarliestPossibleColumn(event, allEvents) {
5293
+ const overlapping = allEvents.filter(e => e !== event
5294
+ && e.column !== undefined
5295
+ && this.eventsOverlap(e.startTime, e.endTime, event.startTime, event.endTime));
5296
+ let column = 0;
5297
+ while (overlapping.some(e => e.column === column)) {
5298
+ column++;
5299
+ }
5300
+ return column;
5301
+ }
5302
+ /** Computes the maximum width an event can span without overlapping a neighbour to its right. */
5303
+ findBiggestPossibleWidth(event, allEvents, _scanStart, _scanEnd) {
5304
+ const overlapping = this.getAllEventsOnSpecificTime(allEvents, event.startTime, event.endTime);
5305
+ const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
5306
+ const totalColumns = maxCol + 1;
5307
+ const occupiedCols = overlapping
5308
+ .filter(e => e !== event && (e.column ?? 0) > (event.column ?? 0))
5309
+ .map(e => e.column ?? 0);
5310
+ if (occupiedCols.length === 0) {
5311
+ return totalColumns - (event.column ?? 0);
5312
+ }
5313
+ return Math.min(...occupiedCols) - (event.column ?? 0);
5314
+ }
5315
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5316
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService });
5317
+ }
5318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, decorators: [{
5319
+ type: Injectable
5320
+ }] });
5321
+
5322
+ /**
5323
+ * Static utility methods for calendar grid positioning.
5324
+ */
5325
+ class CalendarUtility {
5326
+ /**
5327
+ * Converts a weekday (from `Date.getDay()`) to a 1-based Monday-first column index.
5328
+ * Monday = 1, Tuesday = 2, …, Sunday = 7.
5329
+ */
5330
+ static getCorrectColumn(date) {
5331
+ const day = date.getDay();
5332
+ return day === 0 ? 7 : day;
5333
+ }
5334
+ /**
5335
+ * Converts an hour + minute pair to a 1-based CSS grid row index
5336
+ * within a half-hour grid starting at `startHour`.
5337
+ *
5338
+ * Each hour occupies two rows (one per 30-minute slot).
5339
+ * Formula: `(hour - startHour) * 2 + (minute >= 30 ? 1 : 0) + 1`
5340
+ *
5341
+ * @returns Grid row number (minimum 1).
5342
+ */
5343
+ static getCorrectRow(hour, minute, startHour) {
5344
+ const hourOffset = hour - startHour;
5345
+ const row = hourOffset * 2 + (minute >= 30 ? 1 : 0) + 1;
5346
+ return Math.max(1, row);
5347
+ }
5348
+ }
5349
+
5350
+ /**
5351
+ * Default event renderer used when no custom component is provided.
5352
+ *
5353
+ * Displays the event title, formatted time range, and optional description
5354
+ * with the event's colour scheme applied as background and left-border accent.
5355
+ */
5356
+ class CalendarEventDefaultComponent {
5357
+ /** The event to render. Set by {@link CalendarEventComponent} after creation. */
5358
+ event;
5359
+ formattedTime = '';
5360
+ formatter;
5361
+ constructor(formatter) {
5362
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5363
+ }
5364
+ async ngOnInit() {
5365
+ if (this.event) {
5366
+ const start = await this.formatter.formatTime(this.event.startTime);
5367
+ const end = await this.formatter.formatTime(this.event.endTime);
5368
+ this.formattedTime = `${start} - ${end}`;
5369
+ }
5370
+ }
5371
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
5372
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventDefaultComponent, isStandalone: true, selector: "app-calendar-event-default", ngImport: i0, template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
5373
+ }
5374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, decorators: [{
5375
+ type: Component,
5376
+ args: [{ selector: 'app-calendar-event-default', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
5377
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5378
+ type: Optional
5379
+ }, {
5380
+ type: Inject,
5381
+ args: [CALENDAR_DATE_FORMATTER]
5382
+ }] }] });
5383
+
5384
+ /**
5385
+ * Dynamic event renderer that injects a custom or default event component
5386
+ * into its view container.
5387
+ *
5388
+ * The component to render is resolved in this order:
5389
+ * 1. `customComponent` input (set on the parent week/day view)
5390
+ * 2. `event.component` (per-event override)
5391
+ * 3. {@link CalendarEventDefaultComponent} (library default)
5392
+ */
5393
+ class CalendarEventComponent {
5394
+ /** The event data to render. */
5395
+ event;
5396
+ /** Optional custom component type that overrides the default renderer. */
5397
+ customComponent;
5398
+ /** Emits when the rendered event is clicked. */
5399
+ eventClicked = new EventEmitter();
5400
+ eventContainer;
5401
+ rendered = false;
5402
+ ngAfterViewInit() {
5403
+ this.renderComponent();
5404
+ }
5405
+ ngOnChanges(changes) {
5406
+ if (this.rendered && (changes['event'] || changes['customComponent'])) {
5407
+ this.renderComponent();
5408
+ }
5409
+ }
5410
+ /** Emits the event click. */
5411
+ onEventClick() {
5412
+ this.eventClicked.emit(this.event);
5413
+ }
5414
+ /** Creates the event component dynamically and sets its `event` property. */
5415
+ renderComponent() {
5416
+ if (!this.eventContainer)
5417
+ return;
5418
+ this.eventContainer.clear();
5419
+ const component = this.customComponent ?? this.event?.component ?? CalendarEventDefaultComponent;
5420
+ const ref = this.eventContainer.createComponent(component);
5421
+ ref.instance.event = this.event;
5422
+ ref.changeDetectorRef.detectChanges();
5423
+ this.rendered = true;
5424
+ }
5425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5426
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventComponent, isStandalone: true, selector: "app-calendar-event", inputs: { event: "event", customComponent: "customComponent" }, outputs: { eventClicked: "eventClicked" }, viewQueries: [{ propertyName: "eventContainer", first: true, predicate: ["eventContainer"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
5427
+ }
5428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, decorators: [{
5429
+ type: Component,
5430
+ args: [{ selector: 'app-calendar-event', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"] }]
5431
+ }], propDecorators: { event: [{
5432
+ type: Input
5433
+ }], customComponent: [{
5434
+ type: Input
5435
+ }], eventClicked: [{
5436
+ type: Output
5437
+ }], eventContainer: [{
5438
+ type: ViewChild,
5439
+ args: ['eventContainer', { read: ViewContainerRef, static: true }]
5440
+ }] } });
5441
+
5442
+ /**
5443
+ * Week grid view showing 7 day columns with half-hour time slots.
5444
+ *
5445
+ * Overlapping events within the same day are laid out in sub-columns
5446
+ * so they appear side-by-side rather than stacked.
5447
+ */
5448
+ class CalendarWeekComponent {
5449
+ layoutService;
5450
+ /** The date around which the week is centred. */
5451
+ focusDay;
5452
+ /** Observable that emits the full event list whenever it changes. */
5453
+ eventsChanged;
5454
+ /** Observable that emits when the focus day changes. */
5455
+ focusDayChanged;
5456
+ /** Resolved calendar configuration passed from the parent view. */
5457
+ config;
5458
+ /** Optional custom event renderer component. */
5459
+ calendarEventComponent;
5460
+ /** Emits when a calendar event is clicked. */
5461
+ eventClicked = new EventEmitter();
5462
+ columns = [];
5463
+ hourRows = [];
5464
+ displayEvents = [];
5465
+ totalRows = 0;
5466
+ currentTimeRow = 0;
5467
+ currentTimeCol = '';
5468
+ gridTemplateColumns = 'repeat(7, 1fr)';
5469
+ dayColumnMap = [];
5470
+ events = [];
5471
+ destroy$ = new Subject();
5472
+ formatter;
5473
+ resolvedConfig;
5474
+ currentTimeInterval;
5475
+ constructor(layoutService) {
5476
+ this.layoutService = layoutService;
5477
+ this.formatter = new DefaultCalendarDateFormatter();
5478
+ }
5479
+ ngOnInit() {
5480
+ this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5481
+ this.buildHourRows();
5482
+ this.buildColumns();
5483
+ this.updateCurrentTime();
5484
+ this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
5485
+ if (this.eventsChanged) {
5486
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5487
+ this.events = events;
5488
+ this.refreshEvents();
5489
+ });
5490
+ }
5491
+ if (this.focusDayChanged) {
5492
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5493
+ this.focusDay = date;
5494
+ this.buildColumns();
5495
+ this.refreshEvents();
5496
+ this.updateCurrentTime();
5497
+ });
5498
+ }
5499
+ }
5500
+ ngOnDestroy() {
5501
+ this.destroy$.next();
5502
+ this.destroy$.complete();
5503
+ if (this.currentTimeInterval)
5504
+ clearInterval(this.currentTimeInterval);
5505
+ }
5506
+ /** Returns the CSS `grid-row` value for an event based on its start/end times. */
5507
+ getEventRow(event) {
5508
+ const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
5509
+ const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
5510
+ return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
5511
+ }
5512
+ /** Returns the CSS `grid-column` span for a day header, accounting for sub-columns. */
5513
+ getHeaderColumn(dayIndex) {
5514
+ if (!this.dayColumnMap.length)
5515
+ return `${dayIndex + 2} / span 1`;
5516
+ const dayInfo = this.dayColumnMap[dayIndex];
5517
+ return `${dayInfo.startCol + 1} / span ${dayInfo.subColumns}`;
5518
+ }
5519
+ /** Returns the CSS `grid-column` value for an event within its day's sub-columns. */
5520
+ getEventColumn(event) {
5521
+ const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, event.startTime));
5522
+ if (dayIdx < 0)
5523
+ return '1 / span 1';
5524
+ const dayInfo = this.dayColumnMap[dayIdx];
5525
+ const subCol = (event.column ?? 0) + dayInfo.startCol;
5526
+ const width = event.width ?? 1;
5527
+ return `${subCol} / span ${width}`;
5528
+ }
5529
+ /** Forwards event click to parent. */
5530
+ onEventClick(event) {
5531
+ this.eventClicked.emit(event);
5532
+ }
5533
+ /** trackBy for hour rows. */
5534
+ trackByHour(_index, row) {
5535
+ return row.hour;
5536
+ }
5537
+ /** trackBy for day columns. */
5538
+ trackByColumn(_index, col) {
5539
+ return col.date.getTime();
5540
+ }
5541
+ /** trackBy for events. */
5542
+ trackByEvent(_index, event) {
5543
+ return event.id;
5544
+ }
5545
+ async buildHourRows() {
5546
+ this.hourRows = [];
5547
+ const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
5548
+ this.totalRows = hours * 2;
5549
+ for (let i = 0; i < hours; i++) {
5550
+ const hour = this.resolvedConfig.startHour + i;
5551
+ const label = await this.formatter.formatTimeI(hour, 0);
5552
+ this.hourRows.push({
5553
+ hour,
5554
+ topRow: i * 2 + 1,
5555
+ bottomRow: i * 2 + 3,
5556
+ hourLabel: label
5557
+ });
5558
+ }
5559
+ }
5560
+ /** Builds the 7 day columns for the current week (Monday–Sunday). */
5561
+ buildColumns() {
5562
+ if (!this.focusDay)
5563
+ return;
5564
+ const shortNames = this.resolvedConfig.shortDayNames;
5565
+ const today = new Date();
5566
+ const day = this.focusDay.getDay();
5567
+ const mondayOffset = day === 0 ? -6 : 1 - day;
5568
+ const monday = new Date(this.focusDay);
5569
+ monday.setDate(this.focusDay.getDate() + mondayOffset);
5570
+ this.columns = [];
5571
+ for (let i = 0; i < 7; i++) {
5572
+ const date = new Date(monday);
5573
+ date.setDate(monday.getDate() + i);
5574
+ this.columns.push({
5575
+ date,
5576
+ dayName: shortNames[i],
5577
+ dayNumber: date.getDate(),
5578
+ isToday: this.formatter.isSameDay(date, today)
5579
+ });
5580
+ }
5581
+ }
5582
+ /** Filters, splits, and lays out events for the current week. */
5583
+ refreshEvents() {
5584
+ if (!this.columns.length)
5585
+ return;
5586
+ const rangeStart = this.columns[0].date;
5587
+ const rangeEnd = new Date(this.columns[6].date);
5588
+ rangeEnd.setHours(23, 59, 59, 999);
5589
+ const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
5590
+ this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
5591
+ // Assign columns per day so overlapping events within a day get sub-columns
5592
+ for (let i = 0; i < 7; i++) {
5593
+ const dayStart = new Date(this.columns[i].date);
5594
+ dayStart.setHours(0, 0, 0, 0);
5595
+ const dayEnd = new Date(this.columns[i].date);
5596
+ dayEnd.setHours(23, 59, 59, 999);
5597
+ const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
5598
+ this.layoutService.assignColumnsToEvents(dayEvents);
5599
+ this.layoutService.assignWidthsToEvents(dayEvents, dayStart, dayEnd);
5600
+ }
5601
+ this.buildGridColumns();
5602
+ this.updateCurrentTime();
5603
+ }
5604
+ /** Computes the CSS grid-template-columns string based on per-day sub-column counts. */
5605
+ buildGridColumns() {
5606
+ this.dayColumnMap = [];
5607
+ let currentCol = 1;
5608
+ for (let i = 0; i < 7; i++) {
5609
+ const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
5610
+ let maxSubCols = 1;
5611
+ for (const e of dayEvents) {
5612
+ maxSubCols = Math.max(maxSubCols, (e.column ?? 0) + (e.width ?? 1));
5613
+ }
5614
+ this.dayColumnMap.push({ subColumns: maxSubCols, startCol: currentCol });
5615
+ currentCol += maxSubCols;
5616
+ }
5617
+ const parts = [];
5618
+ for (const day of this.dayColumnMap) {
5619
+ for (let j = 0; j < day.subColumns; j++) {
5620
+ parts.push(`${1 / day.subColumns}fr`);
5621
+ }
5622
+ }
5623
+ this.gridTemplateColumns = parts.join(' ');
5624
+ }
5625
+ /** Updates the current-time red line position. */
5626
+ updateCurrentTime() {
5627
+ const now = new Date();
5628
+ const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, now));
5629
+ if (dayIdx >= 0 && this.dayColumnMap.length > 0) {
5630
+ const dayInfo = this.dayColumnMap[dayIdx];
5631
+ this.currentTimeCol = `${dayInfo.startCol} / span ${dayInfo.subColumns}`;
5632
+ this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
5633
+ }
5634
+ else {
5635
+ this.currentTimeCol = '';
5636
+ this.currentTimeRow = 0;
5637
+ }
5638
+ }
5639
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
5640
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "app-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\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: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
5641
+ }
5642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
5643
+ type: Component,
5644
+ args: [{ selector: 'app-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
5645
+ }], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
5646
+ type: Input
5647
+ }], eventsChanged: [{
5648
+ type: Input
5649
+ }], focusDayChanged: [{
5650
+ type: Input
5651
+ }], config: [{
5652
+ type: Input
5653
+ }], calendarEventComponent: [{
5654
+ type: Input
5655
+ }], eventClicked: [{
5656
+ type: Output
5657
+ }] } });
5658
+
5659
+ /**
5660
+ * Day grid view showing a single day with half-hour time slots.
5661
+ *
5662
+ * Shares the same layout algorithm as the week view via
5663
+ * {@link CalendarEventLayoutService}.
5664
+ */
5665
+ class CalendarDayComponent {
5666
+ layoutService;
5667
+ /** The date to display. */
5668
+ focusDay;
5669
+ /** Observable that emits the full event list whenever it changes. */
5670
+ eventsChanged;
5671
+ /** Observable that emits when the focus day changes. */
5672
+ focusDayChanged;
5673
+ /** Resolved calendar configuration passed from the parent view. */
5674
+ config;
5675
+ /** Optional custom event renderer component. */
5676
+ calendarEventComponent;
5677
+ /** Emits when a calendar event is clicked. */
5678
+ eventClicked = new EventEmitter();
5679
+ hourRows = [];
5680
+ displayEvents = [];
5681
+ totalRows = 0;
5682
+ totalColumns = 1;
5683
+ currentTimeRow = 0;
5684
+ isToday = false;
5685
+ dayName = '';
5686
+ events = [];
5687
+ destroy$ = new Subject();
5688
+ formatter;
5689
+ resolvedConfig;
5690
+ currentTimeInterval;
5691
+ constructor(layoutService) {
5692
+ this.layoutService = layoutService;
5693
+ this.formatter = new DefaultCalendarDateFormatter();
5694
+ }
5695
+ ngOnInit() {
5696
+ this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5697
+ this.buildHourRows();
5698
+ this.updateDayInfo();
5699
+ this.updateCurrentTime();
5700
+ this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
5701
+ if (this.eventsChanged) {
5702
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5703
+ this.events = events;
5704
+ this.refreshEvents();
5705
+ });
5706
+ }
5707
+ if (this.focusDayChanged) {
5708
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5709
+ this.focusDay = date;
5710
+ this.updateDayInfo();
5711
+ this.refreshEvents();
5712
+ this.updateCurrentTime();
5713
+ });
5714
+ }
5715
+ }
5716
+ ngOnDestroy() {
5717
+ this.destroy$.next();
5718
+ this.destroy$.complete();
5719
+ if (this.currentTimeInterval)
5720
+ clearInterval(this.currentTimeInterval);
5721
+ }
5722
+ /** Returns the CSS `grid-row` value for an event. */
5723
+ getEventRow(event) {
5724
+ const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
5725
+ const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
5726
+ return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
5727
+ }
5728
+ /** Returns the CSS `grid-column` value for an event within its sub-columns. */
5729
+ getEventColumn(event) {
5730
+ const col = (event.column ?? 0) + 1;
5731
+ const width = event.width ?? 1;
5732
+ return `${col} / span ${width}`;
5733
+ }
5734
+ /** Forwards event click to parent. */
5735
+ onEventClick(event) {
5736
+ this.eventClicked.emit(event);
5737
+ }
5738
+ /** trackBy for hour rows. */
5739
+ trackByHour(_index, row) {
5740
+ return row.hour;
5741
+ }
5742
+ /** trackBy for events. */
5743
+ trackByEvent(_index, event) {
5744
+ return event.id;
5745
+ }
5746
+ async buildHourRows() {
5747
+ this.hourRows = [];
5748
+ const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
5749
+ this.totalRows = hours * 2;
5750
+ for (let i = 0; i < hours; i++) {
5751
+ const hour = this.resolvedConfig.startHour + i;
5752
+ const label = await this.formatter.formatTimeI(hour, 0);
5753
+ this.hourRows.push({
5754
+ hour,
5755
+ topRow: i * 2 + 1,
5756
+ bottomRow: i * 2 + 3,
5757
+ hourLabel: label
5758
+ });
5759
+ }
5760
+ }
5761
+ /** Updates the day name and isToday flag. */
5762
+ updateDayInfo() {
5763
+ if (!this.focusDay)
5764
+ return;
5765
+ const today = new Date();
5766
+ this.isToday = this.formatter.isSameDay(this.focusDay, today);
5767
+ const longNames = this.resolvedConfig.longDayNames;
5768
+ const dayIdx = this.focusDay.getDay();
5769
+ const mondayIdx = dayIdx === 0 ? 6 : dayIdx - 1;
5770
+ this.dayName = longNames[mondayIdx];
5771
+ }
5772
+ /** Filters, splits, and lays out events for the focus day. */
5773
+ refreshEvents() {
5774
+ if (!this.focusDay)
5775
+ return;
5776
+ const rangeStart = new Date(this.focusDay);
5777
+ rangeStart.setHours(0, 0, 0, 0);
5778
+ const rangeEnd = new Date(this.focusDay);
5779
+ rangeEnd.setHours(23, 59, 59, 999);
5780
+ const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
5781
+ this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
5782
+ this.layoutService.assignColumnsToEvents(this.displayEvents);
5783
+ this.layoutService.assignWidthsToEvents(this.displayEvents, rangeStart, rangeEnd);
5784
+ const maxCol = this.displayEvents.reduce((max, e) => Math.max(max, (e.column ?? 0) + (e.width ?? 1)), 1);
5785
+ this.totalColumns = maxCol;
5786
+ }
5787
+ /** Updates the current-time red line position. */
5788
+ updateCurrentTime() {
5789
+ const now = new Date();
5790
+ if (this.focusDay && this.formatter.isSameDay(this.focusDay, now)) {
5791
+ this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
5792
+ this.isToday = true;
5793
+ }
5794
+ else {
5795
+ this.currentTimeRow = 0;
5796
+ this.isToday = false;
5797
+ }
5798
+ }
5799
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
5800
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "app-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\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: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
5801
+ }
5802
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
5803
+ type: Component,
5804
+ args: [{ selector: 'app-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
5805
+ }], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
5806
+ type: Input
5807
+ }], eventsChanged: [{
5808
+ type: Input
5809
+ }], focusDayChanged: [{
5810
+ type: Input
5811
+ }], config: [{
5812
+ type: Input
5813
+ }], calendarEventComponent: [{
5814
+ type: Input
5815
+ }], eventClicked: [{
5816
+ type: Output
5817
+ }] } });
5818
+
5819
+ /**
5820
+ * Renders a single row in the upcoming-events sidebar.
5821
+ * Shows the event title, formatted date/time, and optional description.
5822
+ */
5823
+ class UpcomingEventRowComponent {
5824
+ /** The event to display. */
5825
+ event;
5826
+ /** Emits the event when this row is clicked. */
5827
+ eventClicked = new EventEmitter();
5828
+ formattedDate = '';
5829
+ formatter;
5830
+ constructor(formatter) {
5831
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5832
+ }
5833
+ async ngOnInit() {
5834
+ if (this.event) {
5835
+ const start = await this.formatter.formatTime(this.event.startTime);
5836
+ const end = await this.formatter.formatTime(this.event.endTime);
5837
+ this.formattedDate = `${start} - ${end}`;
5838
+ }
5839
+ }
5840
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
5841
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventRowComponent, isStandalone: true, selector: "app-upcoming-event-row", inputs: { event: "event" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
5842
+ }
5843
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, decorators: [{
5844
+ type: Component,
5845
+ args: [{ selector: 'app-upcoming-event-row', standalone: true, imports: [CommonModule], template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
5846
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5847
+ type: Optional
5848
+ }, {
5849
+ type: Inject,
5850
+ args: [CALENDAR_DATE_FORMATTER]
5851
+ }] }], propDecorators: { event: [{
5852
+ type: Input
5853
+ }], eventClicked: [{
5854
+ type: Output
5855
+ }] } });
5856
+
5857
+ /**
5858
+ * Sidebar component that lists the next 10 upcoming events
5859
+ * (events whose end time is in the future), sorted by start time.
5860
+ */
5861
+ class UpcomingEventsComponent {
5862
+ /** Observable that emits the full event list whenever it changes. */
5863
+ eventsChanged;
5864
+ /** Resolved calendar configuration passed from the parent view. */
5865
+ config;
5866
+ /** Emits when an upcoming event row is clicked. */
5867
+ eventClicked = new EventEmitter();
5868
+ upcomingEvents = [];
5869
+ title;
5870
+ destroy$ = new Subject();
5871
+ constructor() {
5872
+ this.title = DEFAULT_CALENDAR_CONFIG.upcomingEventsTitle;
5873
+ }
5874
+ ngOnInit() {
5875
+ const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5876
+ this.title = resolved.upcomingEventsTitle;
5877
+ if (this.eventsChanged) {
5878
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5879
+ const now = new Date();
5880
+ this.upcomingEvents = events
5881
+ .filter(e => e.endTime > now)
5882
+ .sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
5883
+ .slice(0, 10);
5884
+ });
5885
+ }
5886
+ }
5887
+ ngOnDestroy() {
5888
+ this.destroy$.next();
5889
+ this.destroy$.complete();
5890
+ }
5891
+ /** trackBy for upcoming event rows. */
5892
+ trackByEvent(_index, event) {
5893
+ return event.id;
5894
+ }
5895
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5896
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "app-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\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: "component", type: UpcomingEventRowComponent, selector: "app-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
5897
+ }
5898
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
5899
+ type: Component,
5900
+ args: [{ selector: 'app-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\n"] }]
5901
+ }], ctorParameters: () => [], propDecorators: { eventsChanged: [{
5902
+ type: Input
5903
+ }], config: [{
5904
+ type: Input
5905
+ }], eventClicked: [{
5906
+ type: Output
5907
+ }] } });
5908
+
5909
+ /**
5910
+ * Main calendar orchestrator component.
5911
+ *
5912
+ * Provides a toolbar with view switching (month / week / day), date navigation,
5913
+ * and an optional action button. The active view and an upcoming-events sidebar
5914
+ * are rendered inside a responsive grid layout.
5915
+ *
5916
+ * All configuration (visible hours, locale, labels, mobile breakpoint) is read
5917
+ * from the `mn-config.json5` system via {@link MN_CALENDAR_CONFIG}, falling back
5918
+ * to the legacy {@link CALENDAR_CONFIG} injection token, then to built-in defaults.
5919
+ * Date formatting is delegated to the {@link CALENDAR_DATE_FORMATTER} token.
5920
+ *
5921
+ * @example
5922
+ * ```html
5923
+ * <app-calendar-view
5924
+ * [showButton]="true"
5925
+ * [buttonTitle]="'New Event'"
5926
+ * [NewCalendarItemsEvent]="eventsEmitter"
5927
+ * (RequestNewCalendarItemsEvent)="loadEvents($event)"
5928
+ * (CalendarItemClickedEvent)="onEventClick($event)"
5929
+ * (ButtonClickedEvent)="openModal()">
5930
+ * </app-calendar-view>
5931
+ * ```
5932
+ */
5933
+ class CalendarViewComponent {
5934
+ /** Whether to show the action button in the toolbar. */
5935
+ showButton = false;
5936
+ /** Label text for the action button. */
5937
+ buttonTitle = '';
5938
+ /** Custom event renderer component type. */
5939
+ CalendarEventComponent;
5940
+ /** Observable or EventEmitter that pushes new event arrays into the calendar. */
5941
+ NewCalendarItemsEvent;
5942
+ /** Emits when the calendar needs fresh event data (e.g. after navigation). */
5943
+ RequestNewCalendarItemsEvent = new EventEmitter();
5944
+ /** Emits when a calendar event is clicked. */
5945
+ CalendarItemClickedEvent = new EventEmitter();
5946
+ /** Emits when the action button is clicked. */
5947
+ ButtonClickedEvent = new EventEmitter();
5948
+ CalendarView = CalendarView;
5949
+ currentView = CalendarView.WEEK;
5950
+ focusDay = new Date();
5951
+ dateInputValue = '';
5952
+ viewOptions = [];
5953
+ isMobileView = false;
5954
+ /** BehaviorSubject so late-subscribing child views receive the last emitted events. */
5955
+ internalEventsChanged = new BehaviorSubject([]);
5956
+ /** Subject for broadcasting focus-day changes to child views. */
5957
+ internalFocusDayChanged = new Subject();
5958
+ destroy$ = new Subject();
5959
+ formatter;
5960
+ config;
5961
+ destroyRef = inject(DestroyRef);
5962
+ lang = inject(MnLanguageService);
5963
+ constructor(formatter, mnConfig, legacyConfig) {
5964
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5965
+ // Priority: mn-config system > legacy CALENDAR_CONFIG > built-in defaults
5966
+ const raw = mnConfig ?? legacyConfig ?? undefined;
5967
+ this.config = resolveCalendarConfig(raw);
5968
+ }
5969
+ onResize() {
5970
+ this.checkMobileView();
5971
+ }
5972
+ ngOnInit() {
5973
+ this.rebuildFromConfig();
5974
+ // Re-resolve config when locale changes (supports $translate in mn-config).
5975
+ const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
5976
+ this.rebuildFromConfig();
5977
+ });
5978
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
5979
+ this.checkMobileView();
5980
+ this.updateDateInput();
5981
+ this.RequestNewCalendarItemsEvent.emit(this.focusDay);
5982
+ if (this.NewCalendarItemsEvent) {
5983
+ this.NewCalendarItemsEvent.pipe(takeUntil(this.destroy$)).subscribe(events => {
5984
+ this.internalEventsChanged.next(events);
5985
+ });
5986
+ }
5987
+ }
5988
+ ngOnDestroy() {
5989
+ this.destroy$.next();
5990
+ this.destroy$.complete();
5991
+ }
5992
+ /** Switches the active view. On mobile, forces day view. */
5993
+ switchView(view) {
5994
+ if (this.isMobileView) {
5995
+ this.currentView = CalendarView.DAY;
5996
+ return;
5997
+ }
5998
+ this.currentView = view;
5999
+ }
6000
+ /** Navigates to the previous period (month / week / day). */
6001
+ navigatePrevious() {
6002
+ const d = new Date(this.focusDay);
6003
+ switch (this.currentView) {
6004
+ case CalendarView.MONTH:
6005
+ d.setMonth(d.getMonth() - 1);
6006
+ break;
6007
+ case CalendarView.WEEK:
6008
+ d.setDate(d.getDate() - 7);
6009
+ break;
6010
+ case CalendarView.DAY:
6011
+ d.setDate(d.getDate() - 1);
6012
+ break;
6013
+ }
6014
+ this.setFocusDay(d);
6015
+ }
6016
+ /** Navigates to the next period (month / week / day). */
6017
+ navigateNext() {
6018
+ const d = new Date(this.focusDay);
6019
+ switch (this.currentView) {
6020
+ case CalendarView.MONTH:
6021
+ d.setMonth(d.getMonth() + 1);
6022
+ break;
6023
+ case CalendarView.WEEK:
6024
+ d.setDate(d.getDate() + 7);
6025
+ break;
6026
+ case CalendarView.DAY:
6027
+ d.setDate(d.getDate() + 1);
6028
+ break;
6029
+ }
6030
+ this.setFocusDay(d);
6031
+ }
6032
+ /** Navigates to today. */
6033
+ goToToday() {
6034
+ this.setFocusDay(new Date());
6035
+ }
6036
+ /** Handles the date-picker input change. */
6037
+ onDateInputChange(event) {
6038
+ const value = event.target.value;
6039
+ if (value) {
6040
+ this.setFocusDay(new Date(value));
6041
+ }
6042
+ }
6043
+ /** Handles a day click from the month view — switches to day view. */
6044
+ onMonthDayClick(date) {
6045
+ this.currentView = CalendarView.DAY;
6046
+ this.setFocusDay(date);
6047
+ }
6048
+ /** Forwards a child event click to the parent output. */
6049
+ onEventClick(event) {
6050
+ this.CalendarItemClickedEvent.emit(event);
6051
+ }
6052
+ /** trackBy for view option buttons. */
6053
+ trackByView(_index, item) {
6054
+ return item.value;
6055
+ }
6056
+ /** Rebuilds view options and labels from the current config. */
6057
+ rebuildFromConfig() {
6058
+ this.viewOptions = [
6059
+ { value: CalendarView.MONTH, label: this.config.viewLabels['MONTH'] ?? 'Month' },
6060
+ { value: CalendarView.WEEK, label: this.config.viewLabels['WEEK'] ?? 'Week' },
6061
+ { value: CalendarView.DAY, label: this.config.viewLabels['DAY'] ?? 'Day' }
6062
+ ];
6063
+ }
6064
+ checkMobileView() {
6065
+ const wasMobile = this.isMobileView;
6066
+ this.isMobileView = window.innerWidth < this.config.mobileBreakpoint;
6067
+ if (this.isMobileView && !wasMobile) {
6068
+ this.currentView = CalendarView.DAY;
6069
+ }
6070
+ }
6071
+ setFocusDay(date) {
6072
+ this.focusDay = date;
6073
+ this.updateDateInput();
6074
+ this.internalFocusDayChanged.next(date);
6075
+ this.RequestNewCalendarItemsEvent.emit(date);
6076
+ }
6077
+ updateDateInput() {
6078
+ this.dateInputValue = this.formatter.formatDateForFormControl(this.focusDay);
6079
+ }
6080
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }, { token: MN_CALENDAR_CONFIG, optional: true }, { token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component });
6081
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "app-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
6082
+ provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
6083
+ ], ngImport: i0, template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">&#8249;</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">&#8250;</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\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: "component", type: CalendarMonthComponent, selector: "app-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "app-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "app-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "app-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }] });
6084
+ }
6085
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
6086
+ type: Component,
6087
+ args: [{ selector: 'app-calendar-view', standalone: true, imports: [
6088
+ CommonModule,
6089
+ CalendarMonthComponent,
6090
+ CalendarWeekComponent,
6091
+ CalendarDayComponent,
6092
+ UpcomingEventsComponent
6093
+ ], providers: [
6094
+ provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
6095
+ ], template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">&#8249;</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">&#8250;</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\n"] }]
6096
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
6097
+ type: Optional
6098
+ }, {
6099
+ type: Inject,
6100
+ args: [CALENDAR_DATE_FORMATTER]
6101
+ }] }, { type: undefined, decorators: [{
6102
+ type: Optional
6103
+ }, {
6104
+ type: Inject,
6105
+ args: [MN_CALENDAR_CONFIG]
6106
+ }] }, { type: undefined, decorators: [{
6107
+ type: Optional
6108
+ }, {
6109
+ type: Inject,
6110
+ args: [CALENDAR_CONFIG]
6111
+ }] }], propDecorators: { showButton: [{
6112
+ type: Input
6113
+ }], buttonTitle: [{
6114
+ type: Input
6115
+ }], CalendarEventComponent: [{
6116
+ type: Input
6117
+ }], NewCalendarItemsEvent: [{
6118
+ type: Input
6119
+ }], RequestNewCalendarItemsEvent: [{
6120
+ type: Output
6121
+ }], CalendarItemClickedEvent: [{
6122
+ type: Output
6123
+ }], ButtonClickedEvent: [{
6124
+ type: Output
6125
+ }], onResize: [{
6126
+ type: HostListener,
6127
+ args: ['window:resize']
6128
+ }] } });
6129
+
6130
+ // Main component
6131
+
4311
6132
  class MnSectionDirective {
4312
6133
  /** Section name contributed by this DOM node to the section path */
4313
6134
  mnSection;
@@ -4797,6 +6618,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4797
6618
  }]
4798
6619
  }], ctorParameters: () => [{ type: MnLanguageService }] });
4799
6620
 
6621
+ /**
6622
+ * Enable live preview mode. Listens for postMessage events from
6623
+ * Mn Web Manager and hot-swaps config/translations at runtime.
6624
+ *
6625
+ * Call this once in your app's bootstrap (e.g., APP_INITIALIZER or root component).
6626
+ *
6627
+ * @param configService - The MnConfigService instance
6628
+ * @param langService - The MnLanguageService instance
6629
+ * @param allowedOrigins - Optional whitelist of allowed origins (security)
6630
+ */
6631
+ function enableMnPreviewMode(configService, langService, allowedOrigins) {
6632
+ window.addEventListener('message', async (event) => {
6633
+ if (allowedOrigins?.length && !allowedOrigins.includes(event.origin)) {
6634
+ return;
6635
+ }
6636
+ const data = event.data;
6637
+ if (!data?.type)
6638
+ return;
6639
+ switch (data.type) {
6640
+ case 'mn-config-update':
6641
+ if (data.config) {
6642
+ await configService.loadFromObject(data.config);
6643
+ }
6644
+ break;
6645
+ case 'mn-translations-update':
6646
+ if (data.translations) {
6647
+ for (const [locale, translations] of Object.entries(data.translations)) {
6648
+ langService.registerTranslations(locale, translations);
6649
+ }
6650
+ await langService.setLocale(langService.locale);
6651
+ }
6652
+ break;
6653
+ }
6654
+ });
6655
+ }
6656
+
4800
6657
  /*
4801
6658
  * Public API Surface of mn-lib
4802
6659
  */
@@ -4805,5 +6662,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4805
6662
  * Generated bundle index. Do not edit.
4806
6663
  */
4807
6664
 
4808
- 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 };
6665
+ export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_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, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
4809
6666
  //# sourceMappingURL=mn-angular-lib.mjs.map