mn-angular-lib 1.0.136 → 1.0.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ChangeDetectionStrategy, ApplicationRef, APP_INITIALIZER, Pipe, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, input, output, Injector, viewChildren, linkedSignal, afterNextRender, Renderer2, ChangeDetectorRef, HostListener, ViewChild, ViewContainerRef, forwardRef, afterEveryRender, TemplateRef, ViewChildren, viewChild, EnvironmentInjector, createComponent, isSignal } from '@angular/core';
2
+ import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ApplicationRef, APP_INITIALIZER, Pipe, ChangeDetectionStrategy, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, input, output, Injector, viewChildren, linkedSignal, afterNextRender, Renderer2, ChangeDetectorRef, HostListener, ViewChild, ViewContainerRef, forwardRef, afterEveryRender, TemplateRef, ViewChildren, viewChild, EnvironmentInjector, createComponent, isSignal, effect, untracked, ViewEncapsulation } from '@angular/core';
3
3
  export { TemplateRef, Type } from '@angular/core';
4
4
  import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgClass, NgOptimizedImage, NgTemplateOutlet } from '@angular/common';
7
7
  import { tv } from 'tailwind-variants';
8
+ import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
8
9
  import * as i1$1 from '@angular/forms';
9
10
  import { NgControl, Validators, FormsModule, NG_VALUE_ACCESSOR, FormBuilder, ReactiveFormsModule } from '@angular/forms';
10
- import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
11
11
  import JSON5 from 'json5';
12
12
  import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideCalendarDays, LucideChevronLeft, LucideChevronRight, LucideChevronDown, LucideArrowLeft, LucideArrowRight, LucideCheck, LucideInbox, LucideFilter, LucideDynamicIcon, LucideFunnel, LucideTriangleAlert, LucideCircleAlert } from '@lucide/angular';
13
13
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -442,7 +442,243 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
442
442
  args: ['attr.tabindex']
443
443
  }] } });
444
444
 
445
+ /**
446
+ * Type guard: checks whether a value is a translatable marker object.
447
+ */
448
+ function isTranslatable(value) {
449
+ return (typeof value === 'object' &&
450
+ value !== null &&
451
+ typeof value['$translate'] === 'string');
452
+ }
453
+
454
+ class MnLanguageService {
455
+ http = inject(HttpClient);
456
+ appRef = inject(ApplicationRef);
457
+ _translations = {};
458
+ _locale$ = new BehaviorSubject('en');
459
+ _urlPattern = null;
460
+ _debug = false;
461
+ /** Observable of the current active locale. */
462
+ locale$ = this._locale$.asObservable();
463
+ /** Current active locale. */
464
+ get locale() {
465
+ return this._locale$.value;
466
+ }
467
+ /**
468
+ * Enable or disable debug logging.
469
+ */
470
+ setDebug(enabled) {
471
+ this._debug = enabled;
472
+ if (enabled) {
473
+ console.log(`[MnLanguage] Debug mode enabled`);
474
+ }
475
+ }
476
+ /**
477
+ * Configure the URL pattern used to fetch translation files.
478
+ * Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
479
+ */
480
+ configure(urlPattern) {
481
+ if (this._debug) {
482
+ console.log(`[MnLanguage] Configured urlPattern: ${urlPattern}`);
483
+ }
484
+ this._urlPattern = urlPattern;
485
+ }
486
+ /**
487
+ * Load translations for a locale from the configured URL pattern.
488
+ * If translations are already loaded for this locale, this is a no-op.
489
+ */
490
+ async loadLocale(locale) {
491
+ if (this._translations[locale])
492
+ return;
493
+ if (!this._urlPattern) {
494
+ console.warn(`[MnLanguage] No URL pattern configured. Call configure() or use provideMnLanguage().`);
495
+ return;
496
+ }
497
+ const url = this._urlPattern.replace('{locale}', locale);
498
+ if (this._debug) {
499
+ console.log(`[MnLanguage] Loading locale "${locale}" from ${url}`);
500
+ }
501
+ try {
502
+ const map = await firstValueFrom(this.http.get(url));
503
+ this._translations[locale] = map ?? {};
504
+ if (this._debug) {
505
+ console.log(`[MnLanguage] Loaded locale "${locale}"`, this._translations[locale]);
506
+ }
507
+ }
508
+ catch (err) {
509
+ console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
510
+ this._translations[locale] = {};
511
+ }
512
+ }
513
+ /**
514
+ * Switch the active locale. Loads translations if not yet loaded.
515
+ */
516
+ async setLocale(locale) {
517
+ if (this._debug) {
518
+ console.log(`[MnLanguage] Setting locale to "${locale}"`);
519
+ }
520
+ await this.loadLocale(locale);
521
+ this._locale$.next(locale);
522
+ this.appRef.tick();
523
+ }
524
+ /**
525
+ * Register translations for a locale directly from code (no HTTP needed).
526
+ */
527
+ registerTranslations(locale, translations) {
528
+ this._translations[locale] = {
529
+ ...(this._translations[locale] ?? {}),
530
+ ...translations,
531
+ };
532
+ }
533
+ /**
534
+ * Translate a key using the current locale, with optional parameter interpolation.
535
+ * Falls back to the key itself if no translation is found.
536
+ *
537
+ * Interpolation replaces `{{paramName}}` with the provided value.
538
+ */
539
+ translate(key, params) {
540
+ const map = this._translations[this.locale] ?? {};
541
+ let value = this.getValueFromMap(map, key);
542
+ if (value === undefined) {
543
+ if (this._debug) {
544
+ console.warn(`[MnLanguage] Missing translation for key: "${key}" in locale: "${this.locale}"`);
545
+ }
546
+ return key;
547
+ }
548
+ if (params) {
549
+ for (const [paramKey, paramValue] of Object.entries(params)) {
550
+ value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), String(paramValue));
551
+ }
552
+ }
553
+ return value;
554
+ }
555
+ /**
556
+ * Helper to retrieve a value from a potentially nested translation map using a dot-notated key.
557
+ */
558
+ getValueFromMap(map, key) {
559
+ if (map[key] !== undefined)
560
+ return map[key];
561
+ const parts = key.split('.');
562
+ let current = map;
563
+ for (const part of parts) {
564
+ if (current === null || typeof current !== 'object')
565
+ return undefined;
566
+ current = current[part];
567
+ }
568
+ return typeof current === 'string' ? current : undefined;
569
+ }
570
+ /**
571
+ * Translate a key **only if it is defined**, returning `undefined` otherwise.
572
+ *
573
+ * {@link translate} deliberately returns the key itself when it is missing, which
574
+ * makes it unusable for a library's own default labels: a consumer that never
575
+ * defined `mnCollection.rowsPerPage` would see that raw string in their UI. This
576
+ * lets a caller try a conventional key and fall back to a readable English default
577
+ * when the app has not translated it, so components ship translatable strings
578
+ * without forcing every consumer to define them.
579
+ *
580
+ * @param key The dot-notated translation key.
581
+ * @param params Optional `{{name}}` interpolation values.
582
+ * @returns The translation, or `undefined` when the key is not defined.
583
+ */
584
+ translateIfPresent(key, params) {
585
+ const map = this._translations[this.locale] ?? {};
586
+ if (this.getValueFromMap(map, key) === undefined)
587
+ return undefined;
588
+ return this.translate(key, params);
589
+ }
590
+ /**
591
+ * Shorthand alias for `translate`.
592
+ */
593
+ t(key, params) {
594
+ return this.translate(key, params);
595
+ }
596
+ /**
597
+ * Resolve the effective default locale from a domain-to-locale map.
598
+ * Matches `window.location.hostname` against the map keys.
599
+ * Returns the mapped locale, or the provided fallback if no match is found.
600
+ */
601
+ resolveLocaleForDomain(domainLocaleMap, fallback) {
602
+ if (!domainLocaleMap || typeof window === 'undefined')
603
+ return fallback;
604
+ const hostname = window.location.hostname;
605
+ return domainLocaleMap[hostname] ?? fallback;
606
+ }
607
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
608
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, providedIn: 'root' });
609
+ }
610
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, decorators: [{
611
+ type: Injectable,
612
+ args: [{ providedIn: 'root' }]
613
+ }] });
614
+
615
+ /**
616
+ * Provides an APP_INITIALIZER that configures the MnLanguageService and
617
+ * preloads the requested locales during application bootstrap.
618
+ *
619
+ * Usage in app.config.ts:
620
+ * ...provideMnLanguage({
621
+ * urlPattern: 'assets/i18n/{locale}.json',
622
+ * defaultLocale: 'en',
623
+ * preload: ['en', 'nl'],
624
+ * })
625
+ */
626
+ function provideMnLanguage(config) {
627
+ return [
628
+ {
629
+ provide: APP_INITIALIZER,
630
+ multi: true,
631
+ useFactory: (svc) => async () => {
632
+ if (config.debug) {
633
+ svc.setDebug(true);
634
+ }
635
+ svc.configure(config.urlPattern);
636
+ const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
637
+ const localesToLoad = config.preload ?? [effectiveLocale];
638
+ await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
639
+ await svc.setLocale(effectiveLocale);
640
+ },
641
+ deps: [MnLanguageService],
642
+ },
643
+ ];
644
+ }
645
+
646
+ /**
647
+ * Pipe that translates a key via MnLanguageService.
648
+ *
649
+ * Usage in templates:
650
+ * {{ 'form.email.label' | mnTranslate }}
651
+ * {{ 'greeting' | mnTranslate:{ name: 'World' } }}
652
+ *
653
+ * Note: This pipe is impure so it re-evaluates when the locale changes.
654
+ */
655
+ class MnTranslatePipe {
656
+ lang = inject(MnLanguageService);
657
+ transform(key, params) {
658
+ return this.lang.translate(key, params);
659
+ }
660
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
661
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
662
+ }
663
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
664
+ type: Pipe,
665
+ args: [{
666
+ name: 'mnTranslate',
667
+ standalone: true,
668
+ pure: false,
669
+ }]
670
+ }] });
671
+
445
672
  class MnAlertOutletComponent {
673
+ lang = inject(MnLanguageService);
674
+ /**
675
+ * Accessible name for this control. Resolved through the conventional
676
+ * `mnAlert.close` key so an app can translate it, falling back to English when the
677
+ * key is not defined rather than leaking the raw key into the UI.
678
+ */
679
+ get closeLabel() {
680
+ return this.lang.translateIfPresent('mnAlert.close') ?? 'Close';
681
+ }
446
682
  template;
447
683
  store = inject(MnAlertStore);
448
684
  alerts$ = this.store.alerts$;
@@ -462,11 +698,11 @@ class MnAlertOutletComponent {
462
698
  };
463
699
  }
464
700
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnAlertOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
465
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnAlertOutletComponent, isStandalone: true, selector: "mn-alert-outlet", inputs: { template: "template" }, ngImport: i0, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n aria-label=\"Close\"\n >\n &times;\n </button>\n </div>\n }\n }\n </div>\n}\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
701
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnAlertOutletComponent, isStandalone: true, selector: "mn-alert-outlet", inputs: { template: "template" }, ngImport: i0, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n [attr.aria-label]=\"closeLabel\"\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n type=\"button\"\n >\n &times;\n </button>\n </div>\n }\n }\n </div>\n}\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
466
702
  }
467
703
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnAlertOutletComponent, decorators: [{
468
704
  type: Component,
469
- args: [{ selector: 'mn-alert-outlet', standalone: true, imports: [CommonModule, MnButton], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n aria-label=\"Close\"\n >\n &times;\n </button>\n </div>\n }\n }\n </div>\n}\n" }]
705
+ args: [{ selector: 'mn-alert-outlet', standalone: true, imports: [CommonModule, MnButton], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n [attr.aria-label]=\"closeLabel\"\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n type=\"button\"\n >\n &times;\n </button>\n </div>\n }\n }\n </div>\n}\n" }]
470
706
  }], ctorParameters: () => [], propDecorators: { template: [{
471
707
  type: Input
472
708
  }] } });
@@ -769,240 +1005,33 @@ const mnInputFieldVariants = tv({
769
1005
  true: 'opacity-50 cursor-not-allowed',
770
1006
  },
771
1007
  },
772
- defaultVariants: {
773
- size: 'md',
774
- borderRadius: 'md',
775
- hover: true,
776
- }
777
- });
778
-
779
- class MnErrorMessage {
780
- errorMessage;
781
- id;
782
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnErrorMessage, deps: [], target: i0.ɵɵFactoryTarget.Component });
783
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnErrorMessage, isStandalone: true, selector: "mn-error-message", inputs: { errorMessage: "errorMessage", id: "id" }, ngImport: i0, template: "<div [id]=\"id + '-error'\" class=\"text-red-500 mt-2 text-sm\">\n {{ errorMessage }}\n</div>\n" });
784
- }
785
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnErrorMessage, decorators: [{
786
- type: Component,
787
- args: [{ selector: 'mn-error-message', imports: [], template: "<div [id]=\"id + '-error'\" class=\"text-red-500 mt-2 text-sm\">\n {{ errorMessage }}\n</div>\n" }]
788
- }], propDecorators: { errorMessage: [{
789
- type: Input,
790
- args: [{ required: true }]
791
- }], id: [{
792
- type: Input,
793
- args: [{ required: true }]
794
- }] } });
795
-
796
- /**
797
- * Types for mn-lib configuration.
798
- */
799
-
800
- /**
801
- * Type guard: checks whether a value is a translatable marker object.
802
- */
803
- function isTranslatable(value) {
804
- return (typeof value === 'object' &&
805
- value !== null &&
806
- typeof value['$translate'] === 'string');
807
- }
808
-
809
- class MnLanguageService {
810
- http = inject(HttpClient);
811
- appRef = inject(ApplicationRef);
812
- _translations = {};
813
- _locale$ = new BehaviorSubject('en');
814
- _urlPattern = null;
815
- _debug = false;
816
- /** Observable of the current active locale. */
817
- locale$ = this._locale$.asObservable();
818
- /** Current active locale. */
819
- get locale() {
820
- return this._locale$.value;
821
- }
822
- /**
823
- * Enable or disable debug logging.
824
- */
825
- setDebug(enabled) {
826
- this._debug = enabled;
827
- if (enabled) {
828
- console.log(`[MnLanguage] Debug mode enabled`);
829
- }
830
- }
831
- /**
832
- * Configure the URL pattern used to fetch translation files.
833
- * Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
834
- */
835
- configure(urlPattern) {
836
- if (this._debug) {
837
- console.log(`[MnLanguage] Configured urlPattern: ${urlPattern}`);
838
- }
839
- this._urlPattern = urlPattern;
840
- }
841
- /**
842
- * Load translations for a locale from the configured URL pattern.
843
- * If translations are already loaded for this locale, this is a no-op.
844
- */
845
- async loadLocale(locale) {
846
- if (this._translations[locale])
847
- return;
848
- if (!this._urlPattern) {
849
- console.warn(`[MnLanguage] No URL pattern configured. Call configure() or use provideMnLanguage().`);
850
- return;
851
- }
852
- const url = this._urlPattern.replace('{locale}', locale);
853
- if (this._debug) {
854
- console.log(`[MnLanguage] Loading locale "${locale}" from ${url}`);
855
- }
856
- try {
857
- const map = await firstValueFrom(this.http.get(url));
858
- this._translations[locale] = map ?? {};
859
- if (this._debug) {
860
- console.log(`[MnLanguage] Loaded locale "${locale}"`, this._translations[locale]);
861
- }
862
- }
863
- catch (err) {
864
- console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
865
- this._translations[locale] = {};
866
- }
867
- }
868
- /**
869
- * Switch the active locale. Loads translations if not yet loaded.
870
- */
871
- async setLocale(locale) {
872
- if (this._debug) {
873
- console.log(`[MnLanguage] Setting locale to "${locale}"`);
874
- }
875
- await this.loadLocale(locale);
876
- this._locale$.next(locale);
877
- this.appRef.tick();
878
- }
879
- /**
880
- * Register translations for a locale directly from code (no HTTP needed).
881
- */
882
- registerTranslations(locale, translations) {
883
- this._translations[locale] = {
884
- ...(this._translations[locale] ?? {}),
885
- ...translations,
886
- };
887
- }
888
- /**
889
- * Translate a key using the current locale, with optional parameter interpolation.
890
- * Falls back to the key itself if no translation is found.
891
- *
892
- * Interpolation replaces `{{paramName}}` with the provided value.
893
- */
894
- translate(key, params) {
895
- const map = this._translations[this.locale] ?? {};
896
- let value = this.getValueFromMap(map, key);
897
- if (value === undefined) {
898
- if (this._debug) {
899
- console.warn(`[MnLanguage] Missing translation for key: "${key}" in locale: "${this.locale}"`);
900
- }
901
- return key;
902
- }
903
- if (params) {
904
- for (const [paramKey, paramValue] of Object.entries(params)) {
905
- value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), String(paramValue));
906
- }
907
- }
908
- return value;
909
- }
910
- /**
911
- * Helper to retrieve a value from a potentially nested translation map using a dot-notated key.
912
- */
913
- getValueFromMap(map, key) {
914
- if (map[key] !== undefined)
915
- return map[key];
916
- const parts = key.split('.');
917
- let current = map;
918
- for (const part of parts) {
919
- if (current === null || typeof current !== 'object')
920
- return undefined;
921
- current = current[part];
922
- }
923
- return typeof current === 'string' ? current : undefined;
924
- }
925
- /**
926
- * Shorthand alias for `translate`.
927
- */
928
- t(key, params) {
929
- return this.translate(key, params);
930
- }
931
- /**
932
- * Resolve the effective default locale from a domain-to-locale map.
933
- * Matches `window.location.hostname` against the map keys.
934
- * Returns the mapped locale, or the provided fallback if no match is found.
935
- */
936
- resolveLocaleForDomain(domainLocaleMap, fallback) {
937
- if (!domainLocaleMap || typeof window === 'undefined')
938
- return fallback;
939
- const hostname = window.location.hostname;
940
- return domainLocaleMap[hostname] ?? fallback;
941
- }
942
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
943
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, providedIn: 'root' });
944
- }
945
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, decorators: [{
946
- type: Injectable,
947
- args: [{ providedIn: 'root' }]
948
- }] });
949
-
950
- /**
951
- * Provides an APP_INITIALIZER that configures the MnLanguageService and
952
- * preloads the requested locales during application bootstrap.
953
- *
954
- * Usage in app.config.ts:
955
- * ...provideMnLanguage({
956
- * urlPattern: 'assets/i18n/{locale}.json',
957
- * defaultLocale: 'en',
958
- * preload: ['en', 'nl'],
959
- * })
960
- */
961
- function provideMnLanguage(config) {
962
- return [
963
- {
964
- provide: APP_INITIALIZER,
965
- multi: true,
966
- useFactory: (svc) => async () => {
967
- if (config.debug) {
968
- svc.setDebug(true);
969
- }
970
- svc.configure(config.urlPattern);
971
- const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
972
- const localesToLoad = config.preload ?? [effectiveLocale];
973
- await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
974
- await svc.setLocale(effectiveLocale);
975
- },
976
- deps: [MnLanguageService],
977
- },
978
- ];
1008
+ defaultVariants: {
1009
+ size: 'md',
1010
+ borderRadius: 'md',
1011
+ hover: true,
1012
+ }
1013
+ });
1014
+
1015
+ class MnErrorMessage {
1016
+ errorMessage;
1017
+ id;
1018
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnErrorMessage, deps: [], target: i0.ɵɵFactoryTarget.Component });
1019
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnErrorMessage, isStandalone: true, selector: "mn-error-message", inputs: { errorMessage: "errorMessage", id: "id" }, ngImport: i0, template: "<div [id]=\"id + '-error'\" class=\"text-red-500 mt-2 text-sm\">\n {{ errorMessage }}\n</div>\n" });
979
1020
  }
1021
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnErrorMessage, decorators: [{
1022
+ type: Component,
1023
+ args: [{ selector: 'mn-error-message', imports: [], template: "<div [id]=\"id + '-error'\" class=\"text-red-500 mt-2 text-sm\">\n {{ errorMessage }}\n</div>\n" }]
1024
+ }], propDecorators: { errorMessage: [{
1025
+ type: Input,
1026
+ args: [{ required: true }]
1027
+ }], id: [{
1028
+ type: Input,
1029
+ args: [{ required: true }]
1030
+ }] } });
980
1031
 
981
1032
  /**
982
- * Pipe that translates a key via MnLanguageService.
983
- *
984
- * Usage in templates:
985
- * {{ 'form.email.label' | mnTranslate }}
986
- * {{ 'greeting' | mnTranslate:{ name: 'World' } }}
987
- *
988
- * Note: This pipe is impure so it re-evaluates when the locale changes.
1033
+ * Types for mn-lib configuration.
989
1034
  */
990
- class MnTranslatePipe {
991
- lang = inject(MnLanguageService);
992
- transform(key, params) {
993
- return this.lang.translate(key, params);
994
- }
995
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
996
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
997
- }
998
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
999
- type: Pipe,
1000
- args: [{
1001
- name: 'mnTranslate',
1002
- standalone: true,
1003
- pure: false,
1004
- }]
1005
- }] });
1006
1035
 
1007
1036
  function isPlainObject(value) {
1008
1037
  return (typeof value === 'object' &&
@@ -5626,6 +5655,28 @@ class MnCollectionBase {
5626
5655
  onRowsChanged() {
5627
5656
  // no-op by default
5628
5657
  }
5658
+ /**
5659
+ * Resolves a label three ways, in order: the consumer's explicit key, a
5660
+ * conventional `mnCollection.*` key when the app defines one, and finally a
5661
+ * readable English default.
5662
+ *
5663
+ * The middle step is what makes the components translatable out of the box: an
5664
+ * app that adds the `mnCollection` namespace to its locale files gets every
5665
+ * table, list and grid translated at once, with no per-call-site wiring across
5666
+ * dozens of data sources. An app that does not keeps today's English text rather
5667
+ * than leaking raw keys into the UI.
5668
+ *
5669
+ * @param consumerKey The data source's own translation key, if it set one.
5670
+ * @param defaultKey The conventional key this label falls back to.
5671
+ * @param fallback The English text used when neither key resolves.
5672
+ * @param params Optional interpolation values.
5673
+ * @returns The resolved label.
5674
+ */
5675
+ resolveLabel(consumerKey, defaultKey, fallback, params) {
5676
+ if (consumerKey)
5677
+ return this.lang.t(consumerKey, params);
5678
+ return this.lang.translateIfPresent(defaultKey, params) ?? fallback;
5679
+ }
5629
5680
  /**
5630
5681
  * Resolves translation keys to display strings via {@link MnLanguageService}.
5631
5682
  * Subclasses override to resolve their own keys; call `super` to keep these.
@@ -5794,12 +5845,6 @@ class MnSelectableCollectionBase extends MnCollectionBase {
5794
5845
  * its ids matched a loaded row yet. See {@link beforeInitialFilter}.
5795
5846
  */
5796
5847
  pendingInitialEmit = false;
5797
- /**
5798
- * The ids that arrived pre-selected, kept apart from {@link selectedIds} so
5799
- * {@link prioritizeInitialSelection} has a set that does **not** move as the user
5800
- * clicks. Null when the collection opened with nothing selected.
5801
- */
5802
- pinnedSelectionIds = null;
5803
5848
  /**
5804
5849
  * Every selected row, in selection order, for the summary. Ids whose row was
5805
5850
  * never seen are skipped rather than rendered as a bare id.
@@ -5819,7 +5864,21 @@ class MnSelectableCollectionBase extends MnCollectionBase {
5819
5864
  }
5820
5865
  /** How many tags to show before collapsing the remainder. */
5821
5866
  get selectionSummaryLimit() {
5822
- return this.dataSource.selectionSummaryLimit ?? 8;
5867
+ return this.dataSource.selectionSummaryLimit ?? this.defaultSelectionSummaryLimit;
5868
+ }
5869
+ /**
5870
+ * Tag count the summary collapses at when the data source names no limit.
5871
+ *
5872
+ * Subclasses that know their own width narrow this: the same eight tags that
5873
+ * read as a compact header on a wide table become seven stacked lines in a phone
5874
+ * sheet, pushing the rows they describe off screen. Overridden by
5875
+ * {@link MnCollectionDataSource.selectionSummaryLimit}.
5876
+ */
5877
+ // Deliberately an accessor, not a readonly field: MnTable overrides it with a
5878
+ // width-dependent getter, and TypeScript cannot override a property with one.
5879
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
5880
+ get defaultSelectionSummaryLimit() {
5881
+ return 8;
5823
5882
  }
5824
5883
  /**
5825
5884
  * The tags to render: the first {@link selectionSummaryLimit} rows, or all of them
@@ -5924,34 +5983,6 @@ class MnSelectableCollectionBase extends MnCollectionBase {
5924
5983
  defaultSelectionLabel(_row) {
5925
5984
  return null;
5926
5985
  }
5927
- /**
5928
- * Reorders rows so the ones that were already selected when the collection
5929
- * opened come first, preserving the incoming order within each group.
5930
- *
5931
- * Deliberately keyed on the *initial* selection rather than the live one: pinning
5932
- * what the user is currently ticking would make a row jump to the top the instant
5933
- * it is clicked, moving the next row under the pointer mid-click. Freezing the set
5934
- * answers the actual question — "what was already chosen?" — and leaves the list
5935
- * still while it is being worked with. A row deselected during the session keeps
5936
- * its place for the same reason.
5937
- *
5938
- * Callers apply this only when no explicit sort is active, so a sorted column
5939
- * always wins.
5940
- * @param items The rows in their current order.
5941
- * @returns The rows with the initially-selected ones hoisted to the top.
5942
- */
5943
- prioritizeInitialSelection(items) {
5944
- const pinned = this.pinnedSelectionIds;
5945
- if (!pinned?.size)
5946
- return items;
5947
- const selected = [];
5948
- const rest = [];
5949
- for (const item of items) {
5950
- (pinned.has(this.dataSource.getID(item)) ? selected : rest).push(item);
5951
- }
5952
- // Nothing to hoist (e.g. the pinned rows are on another server-side page).
5953
- return selected.length === 0 ? items : [...selected, ...rest];
5954
- }
5955
5986
  /** Seeds selection from `initialSelectedIds` before the first filter pass. */
5956
5987
  beforeInitialFilter() {
5957
5988
  super.beforeInitialFilter();
@@ -5960,7 +5991,6 @@ class MnSelectableCollectionBase extends MnCollectionBase {
5960
5991
  for (const id of this.dataSource.initialSelectedIds) {
5961
5992
  this.selectedIds.add(id);
5962
5993
  }
5963
- this.pinnedSelectionIds = new Set(this.dataSource.initialSelectedIds);
5964
5994
  for (const row of this.dataSource.initialSelectedRows ?? []) {
5965
5995
  this.selectedRowsById.set(this.dataSource.getID(row), row);
5966
5996
  }
@@ -6023,6 +6053,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6023
6053
  * reacts to the outputs.
6024
6054
  */
6025
6055
  class MnCollectionPagination {
6056
+ lang = inject(MnLanguageService);
6026
6057
  /** Prefix for the page-size select's id, keeping it unique per host. */
6027
6058
  idPrefix = 'mn-collection';
6028
6059
  isPaginated = false;
@@ -6076,25 +6107,25 @@ class MnCollectionPagination {
6076
6107
  }
6077
6108
  return slots;
6078
6109
  }
6079
- /** Wrapper classes for a slot: anchors and their gaps are md+ only. */
6080
- slotVisibility(slot) {
6081
- return slot.anchor ? 'hidden md:inline-flex' : 'inline-flex';
6082
- }
6083
6110
  /** e.g. `Page 5 of 50`. */
6084
6111
  get pageIndicatorLabel() {
6085
- return this.fill(this.labels?.pageIndicator || 'Page {{current}} of {{total}}', {
6112
+ return this.fill(this.label(this.labels?.pageIndicator, 'mnCollection.pageIndicator', 'Page {{current}} of {{total}}'), {
6086
6113
  current: this.currentPage,
6087
6114
  total: this.totalPages,
6088
6115
  });
6089
6116
  }
6090
6117
  /** e.g. `41–50 of 250`. */
6091
6118
  get itemRangeLabel() {
6092
- return this.fill(this.labels?.itemRange || '{{start}}–{{end}} of {{total}}', {
6119
+ return this.fill(this.label(this.labels?.itemRange, 'mnCollection.itemRange', '{{start}}–{{end}} of {{total}}'), {
6093
6120
  start: this.rangeStart,
6094
6121
  end: this.rangeEnd,
6095
6122
  total: this.totalItemCount,
6096
6123
  });
6097
6124
  }
6125
+ /** "Items per page" label beside the page-size selector. */
6126
+ get rowsPerPageLabel() {
6127
+ return this.label(this.labels?.rowsPerPage, 'mnCollection.rowsPerPage', 'Items per page:');
6128
+ }
6098
6129
  /**
6099
6130
  * Substitutes `{{name}}` placeholders, matching the interpolation syntax used
6100
6131
  * by MnLanguageService so the same translation strings work either way.
@@ -6102,12 +6133,75 @@ class MnCollectionPagination {
6102
6133
  fill(template, params) {
6103
6134
  return Object.entries(params).reduce((result, [key, value]) => result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value)), template);
6104
6135
  }
6136
+ /** Label for the load-more button. */
6137
+ get loadMoreLabel() {
6138
+ return this.label(this.labels?.loadMore, 'mnCollection.loadMore', 'Load more');
6139
+ }
6140
+ /** Accessible label for the first-page control. */
6141
+ get firstPageLabel() {
6142
+ return this.label(undefined, 'mnCollection.firstPage', 'First page');
6143
+ }
6144
+ /** Accessible label for the previous-page control. */
6145
+ get previousPageLabel() {
6146
+ return this.label(undefined, 'mnCollection.previousPage', 'Previous page');
6147
+ }
6148
+ /** Accessible label for the next-page control. */
6149
+ get nextPageLabel() {
6150
+ return this.label(undefined, 'mnCollection.nextPage', 'Next page');
6151
+ }
6152
+ /** Accessible label for the last-page control. */
6153
+ get lastPageLabel() {
6154
+ return this.label(undefined, 'mnCollection.lastPage', 'Last page');
6155
+ }
6156
+ /**
6157
+ * Wrapper classes for one slot in the page strip, shrinking it in two steps as
6158
+ * the footer narrows. Container queries, so the measurement is the footer's own
6159
+ * width — the same strip is wide on a page and cramped in a modal.
6160
+ *
6161
+ * The first/last anchors and their gaps drop below 640px, where « and » already
6162
+ * jump to either end. Below 380px every number except the current one drops too:
6163
+ * the strip would otherwise wrap onto a second line and push the footer over the
6164
+ * table, and the "Page 3 of 9" readout beside it already says where the user is.
6165
+ * The arrows survive both steps, so navigation never depends on a number.
6166
+ */
6167
+ slotVisibility(slot) {
6168
+ if (slot.anchor)
6169
+ return 'hidden @min-[640px]:inline-flex';
6170
+ return slot.page === this.currentPage ? 'inline-flex' : 'hidden @min-[380px]:inline-flex';
6171
+ }
6172
+ /**
6173
+ * Accessible label for a page-number button.
6174
+ * @param page The page the button jumps to.
6175
+ * @returns The label, naming the page.
6176
+ */
6177
+ pageLabel(page) {
6178
+ const template = this.label(undefined, 'mnCollection.page', 'Page {{page}}');
6179
+ return template.replace('{{page}}', String(page));
6180
+ }
6181
+ /**
6182
+ * Resolves a label three ways, in order: the consumer's explicit text, the
6183
+ * conventional `mnCollection.*` key when the app defines one, and finally a
6184
+ * readable English default.
6185
+ *
6186
+ * Mirrors `MnCollectionBase.resolveLabel`; this component is presentational and
6187
+ * does not extend that base, but its chrome must be just as translatable — the
6188
+ * page-size label and the item-range readout are on screen for every paged
6189
+ * collection in the app.
6190
+ *
6191
+ * @param explicit The label the host passed in, if any.
6192
+ * @param key The conventional translation key to try.
6193
+ * @param fallback The English text used when neither resolves.
6194
+ * @returns The resolved label.
6195
+ */
6196
+ label(explicit, key, fallback) {
6197
+ return explicit ?? this.lang.translateIfPresent(key) ?? fallback;
6198
+ }
6105
6199
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, deps: [], target: i0.ɵɵFactoryTarget.Component });
6106
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCollectionPagination, isStandalone: true, selector: "mn-collection-pagination", inputs: { idPrefix: "idPrefix", isPaginated: "isPaginated", isServerPaginated: "isServerPaginated", showLoadMore: "showLoadMore", loadingMoreRows: "loadingMoreRows", currentPage: "currentPage", pageSize: "pageSize", totalPages: "totalPages", totalItemCount: "totalItemCount", visiblePages: "visiblePages", pageSizeSelectOptions: "pageSizeSelectOptions", labels: "labels" }, outputs: { loadMore: "loadMore", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"flex items-center justify-between gap-2 px-2 py-3 text-sm text-base-content\">\n @if (pageSizeSelectOptions.length > 1) {\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n } @else {\n <div class=\"hidden md:block\"></div>\n }\n\n <!-- Narrow viewports state the page position outright: the strip's last-page\n anchor only appears once the window stops reaching the end, so it can't\n be relied on to carry the total. -->\n <span aria-live=\"polite\" class=\"md:hidden text-xs opacity-70 whitespace-nowrap\">{{ pageIndicatorLabel }}</span>\n\n <div class=\"flex flex-wrap items-center justify-center gap-0.5 md:gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ itemRangeLabel }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"First page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Previous page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u2039\n </button>\n\n @for (slot of pageSlots; track $index) {\n <span [class]=\"slotVisibility(slot)\">\n @if (slot.page !== null) {\n <button\n (click)=\"pageChange.emit(slot.page)\"\n [attr.aria-current]=\"slot.page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"'Page ' + slot.page\"\n [data]=\"{ size: 'sm', variant: 'text', color: slot.page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-1.5 md:px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n type=\"button\"\n >{{ slot.page }}\n </button>\n } @else {\n <span aria-hidden=\"true\" class=\"px-0.5 text-xs opacity-50 select-none\">\u2026</span>\n }\n </span>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Next page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Last page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00BB\n </button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6200
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCollectionPagination, isStandalone: true, selector: "mn-collection-pagination", inputs: { idPrefix: "idPrefix", isPaginated: "isPaginated", isServerPaginated: "isServerPaginated", showLoadMore: "showLoadMore", loadingMoreRows: "loadingMoreRows", currentPage: "currentPage", pageSize: "pageSize", totalPages: "totalPages", totalItemCount: "totalItemCount", visiblePages: "visiblePages", pageSizeSelectOptions: "pageSizeSelectOptions", labels: "labels" }, outputs: { loadMore: "loadMore", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ loadMoreLabel }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"@container flex items-center justify-between gap-2 px-2 py-3 text-sm text-base-content\">\n @if (pageSizeSelectOptions.length > 1) {\n <div class=\"hidden @min-[640px]:flex items-center gap-2\">\n <span>{{ rowsPerPageLabel }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n } @else {\n <div class=\"hidden @min-[640px]:block\"></div>\n }\n\n <!-- Narrow viewports state the page position outright: the strip's last-page\n anchor only appears once the window stops reaching the end, so it can't\n be relied on to carry the total. -->\n <span aria-live=\"polite\"\n class=\"@min-[640px]:hidden text-xs opacity-70 whitespace-nowrap\">{{ pageIndicatorLabel }}</span>\n\n <div class=\"flex flex-wrap items-center justify-center gap-0.5 @min-[640px]:gap-1\">\n <span class=\"text-xs mr-2 hidden @min-[640px]:inline\">{{ itemRangeLabel }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"firstPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"previousPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u2039\n </button>\n\n @for (slot of pageSlots; track $index) {\n <span [class]=\"slotVisibility(slot)\">\n @if (slot.page !== null) {\n <button\n (click)=\"pageChange.emit(slot.page)\"\n [attr.aria-current]=\"slot.page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"pageLabel(slot.page)\"\n [data]=\"{ size: 'sm', variant: 'text', color: slot.page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-1.5 @min-[640px]:px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n type=\"button\"\n >{{ slot.page }}\n </button>\n } @else {\n <span aria-hidden=\"true\" class=\"px-0.5 text-xs opacity-50 select-none\">\u2026</span>\n }\n </span>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"nextPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"lastPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00BB\n </button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6107
6201
  }
6108
6202
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, decorators: [{
6109
6203
  type: Component,
6110
- args: [{ selector: 'mn-collection-pagination', standalone: true, imports: [MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"flex items-center justify-between gap-2 px-2 py-3 text-sm text-base-content\">\n @if (pageSizeSelectOptions.length > 1) {\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n } @else {\n <div class=\"hidden md:block\"></div>\n }\n\n <!-- Narrow viewports state the page position outright: the strip's last-page\n anchor only appears once the window stops reaching the end, so it can't\n be relied on to carry the total. -->\n <span aria-live=\"polite\" class=\"md:hidden text-xs opacity-70 whitespace-nowrap\">{{ pageIndicatorLabel }}</span>\n\n <div class=\"flex flex-wrap items-center justify-center gap-0.5 md:gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ itemRangeLabel }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"First page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Previous page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u2039\n </button>\n\n @for (slot of pageSlots; track $index) {\n <span [class]=\"slotVisibility(slot)\">\n @if (slot.page !== null) {\n <button\n (click)=\"pageChange.emit(slot.page)\"\n [attr.aria-current]=\"slot.page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"'Page ' + slot.page\"\n [data]=\"{ size: 'sm', variant: 'text', color: slot.page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-1.5 md:px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n type=\"button\"\n >{{ slot.page }}\n </button>\n } @else {\n <span aria-hidden=\"true\" class=\"px-0.5 text-xs opacity-50 select-none\">\u2026</span>\n }\n </span>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Next page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Last page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00BB\n </button>\n </div>\n </div>\n}\n" }]
6204
+ args: [{ selector: 'mn-collection-pagination', standalone: true, imports: [MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ loadMoreLabel }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"@container flex items-center justify-between gap-2 px-2 py-3 text-sm text-base-content\">\n @if (pageSizeSelectOptions.length > 1) {\n <div class=\"hidden @min-[640px]:flex items-center gap-2\">\n <span>{{ rowsPerPageLabel }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n } @else {\n <div class=\"hidden @min-[640px]:block\"></div>\n }\n\n <!-- Narrow viewports state the page position outright: the strip's last-page\n anchor only appears once the window stops reaching the end, so it can't\n be relied on to carry the total. -->\n <span aria-live=\"polite\"\n class=\"@min-[640px]:hidden text-xs opacity-70 whitespace-nowrap\">{{ pageIndicatorLabel }}</span>\n\n <div class=\"flex flex-wrap items-center justify-center gap-0.5 @min-[640px]:gap-1\">\n <span class=\"text-xs mr-2 hidden @min-[640px]:inline\">{{ itemRangeLabel }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"firstPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"previousPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u2039\n </button>\n\n @for (slot of pageSlots; track $index) {\n <span [class]=\"slotVisibility(slot)\">\n @if (slot.page !== null) {\n <button\n (click)=\"pageChange.emit(slot.page)\"\n [attr.aria-current]=\"slot.page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"pageLabel(slot.page)\"\n [data]=\"{ size: 'sm', variant: 'text', color: slot.page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-1.5 @min-[640px]:px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n type=\"button\"\n >{{ slot.page }}\n </button>\n } @else {\n <span aria-hidden=\"true\" class=\"px-0.5 text-xs opacity-50 select-none\">\u2026</span>\n }\n </span>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"nextPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"lastPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00BB\n </button>\n </div>\n </div>\n}\n" }]
6111
6205
  }], propDecorators: { idPrefix: [{
6112
6206
  type: Input
6113
6207
  }], isPaginated: [{
@@ -6233,22 +6327,16 @@ class MnTable extends MnSelectableCollectionBase {
6233
6327
  getFilterMultiSelectOptions(column) {
6234
6328
  return (column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) }));
6235
6329
  }
6236
- /** Any / Yes / No options for a boolean column filter. */
6237
- getBooleanFilterOptions(column) {
6238
- const labels = this.dataSource.filterLabels;
6239
- return [
6240
- { label: column.filterPlaceholder ?? labels?.any ?? 'Any', value: '' },
6241
- { label: labels?.yes ?? 'Yes', value: 'true' },
6242
- { label: labels?.no ?? 'No', value: 'false' },
6243
- ];
6330
+ /** Label for the small-screen filters toggle button. */
6331
+ get filtersButtonLabel() {
6332
+ return this.resolveLabel(this.dataSource.filtersLabelKey, 'mnCollection.filters', this.dataSource.filtersLabel ?? 'Filters');
6244
6333
  }
6245
- /** Filter options formatted for mn-select for a given column. */
6246
- getFilterSelectOptions(column) {
6247
- const placeholder = column.filterPlaceholder ?? 'All';
6248
- return [
6249
- { label: placeholder, value: '' },
6250
- ...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
6251
- ];
6334
+ /**
6335
+ * Summary a multi-select filter collapses to once more than one option is picked.
6336
+ * Resolved with the `{count}` token intact for mn-multi-select to fill in.
6337
+ */
6338
+ get filterSelectedLabel() {
6339
+ return this.resolveLabel(this.dataSource.filterLabels?.selectedKey, 'mnCollection.filterSelected', this.dataSource.filterLabels?.selected ?? '{count} selected');
6252
6340
  }
6253
6341
  /** Current text/select filter value for a column. */
6254
6342
  textFilterValue(column) {
@@ -6281,20 +6369,20 @@ class MnTable extends MnSelectableCollectionBase {
6281
6369
  get hasColumnFilters() {
6282
6370
  return this.dataSource.columns.some(c => c.filterable);
6283
6371
  }
6284
- /** Label for the small-screen filters toggle button. */
6285
- get filtersButtonLabel() {
6286
- return this.dataSource.filtersLabel ?? 'Filters';
6287
- }
6288
- /**
6289
- * Summary a multi-select filter collapses to once more than one option is picked.
6290
- * Resolved with the `{count}` token intact for mn-multi-select to fill in.
6291
- */
6292
- get filterSelectedLabel() {
6293
- return this.dataSource.filterLabels?.selected ?? '{count} selected';
6294
- }
6295
6372
  /** Label for the "clear all filters" action in the small-screen panel. */
6296
6373
  get clearFiltersButtonLabel() {
6297
- return this.dataSource.clearFiltersLabel ?? 'Clear all';
6374
+ return this.resolveLabel(this.dataSource.clearFiltersLabelKey, 'mnCollection.clearAll', this.dataSource.clearFiltersLabel ?? 'Clear all');
6375
+ }
6376
+ /** Heading for the selection summary, with the count filled in. */
6377
+ get selectionSummaryTitle() {
6378
+ const labels = this.dataSource.selectionSummaryLabels;
6379
+ const template = this.resolveLabel(labels?.titleKey, 'mnCollection.selectedCount', labels?.title ?? 'Selected ({{count}})');
6380
+ return template.replace('{{count}}', String(this.selectedIds.size));
6381
+ }
6382
+ /** Label for the summary's clear-everything action. */
6383
+ get selectionClearAllLabel() {
6384
+ const labels = this.dataSource.selectionSummaryLabels;
6385
+ return this.resolveLabel(labels?.clearAllKey, 'mnCollection.clearAll', labels?.clearAll ?? 'Clear all');
6298
6386
  }
6299
6387
  /** Opens/closes the stacked filter panel shown on small screens. */
6300
6388
  toggleFiltersPanel() {
@@ -6427,31 +6515,35 @@ class MnTable extends MnSelectableCollectionBase {
6427
6515
  }
6428
6516
  /** Page size to use at/above the `md` breakpoint (consumer's pageSize, or the user's selection). */
6429
6517
  desktopPageSize = 10;
6430
- /** Heading for the selection summary, with the count filled in. */
6431
- get selectionSummaryTitle() {
6432
- const template = this.dataSource.selectionSummaryLabels?.title ?? 'Selected ({{count}})';
6433
- return template.replace('{{count}}', String(this.selectedIds.size));
6434
- }
6435
- /** Label for the summary's clear-everything action. */
6436
- get selectionClearAllLabel() {
6437
- return this.dataSource.selectionSummaryLabels?.clearAll ?? 'Clear all';
6438
- }
6439
6518
  /** Label for the summary's expand/collapse control. */
6440
6519
  get selectionSummaryToggleLabel() {
6441
6520
  const labels = this.dataSource.selectionSummaryLabels;
6442
- if (this.selectionSummaryExpanded)
6443
- return labels?.showLess ?? 'Show less';
6444
- const template = labels?.showMore ?? '+{{count}} more';
6521
+ if (this.selectionSummaryExpanded) {
6522
+ return this.resolveLabel(labels?.showLessKey, 'mnCollection.showLess', labels?.showLess ?? 'Show less');
6523
+ }
6524
+ const template = this.resolveLabel(labels?.showMoreKey, 'mnCollection.showMore', labels?.showMore ?? '+{{count}} more');
6445
6525
  return template.replace('{{count}}', String(this.hiddenSelectionCount));
6446
6526
  }
6527
+ /** Placeholder and accessible name for the search box. */
6528
+ get searchPlaceholderLabel() {
6529
+ return this.resolveLabel(this.dataSource.searchPlaceholderKey, 'mnCollection.search', this.dataSource.searchPlaceholder ?? 'Search...');
6530
+ }
6531
+ /** Accessible name for the scrollable table region. */
6532
+ get tableRegionLabel() {
6533
+ return this.resolveLabel(undefined, 'mnCollection.dataTable', 'Data table');
6534
+ }
6447
6535
  /**
6448
- * Accessible label for a tag's remove button.
6449
- * @param row The row the tag stands for.
6450
- * @returns The label, naming the row so screen readers announce which one goes.
6536
+ * Fewer tags once the table is narrow. A tag holding a person's full name takes
6537
+ * a whole line at phone width, so the eight that read as a compact header on a
6538
+ * wide table become eight stacked lines in a modal sheet the summary then
6539
+ * occupies more of the screen than the rows it is summarising.
6540
+ *
6541
+ * Reuses {@link filtersCollapsed} rather than measuring again: it is already
6542
+ * maintained on every resize and means exactly "this table is under 640px".
6543
+ * The heading still states the true total, so the hidden tags cost no information.
6451
6544
  */
6452
- selectionRemoveLabel(row) {
6453
- const template = this.dataSource.selectionSummaryLabels?.remove ?? 'Remove {{label}}';
6454
- return template.replace('{{label}}', this.selectionLabelFor(row));
6545
+ get defaultSelectionSummaryLimit() {
6546
+ return this.filtersCollapsed ? 5 : 8;
6455
6547
  }
6456
6548
  /** Tracks the desktop page size when the user picks one (selector only shows at >= md). */
6457
6549
  onPageSizeChange(newSize) {
@@ -6471,6 +6563,18 @@ class MnTable extends MnSelectableCollectionBase {
6471
6563
  get widthsArePinned() {
6472
6564
  return this.layoutMode === 'fixed' || (this.layoutMode === 'stable' && this.widthsPinned);
6473
6565
  }
6566
+ /** Any / Yes / No options for a boolean column filter. */
6567
+ getBooleanFilterOptions(column) {
6568
+ const labels = this.dataSource.filterLabels;
6569
+ return [
6570
+ {
6571
+ label: column.filterPlaceholder ?? this.resolveLabel(labels?.anyKey, 'mnCollection.filterAny', labels?.any ?? 'Any'),
6572
+ value: ''
6573
+ },
6574
+ { label: this.resolveLabel(labels?.yesKey, 'mnCollection.filterYes', labels?.yes ?? 'Yes'), value: 'true' },
6575
+ { label: this.resolveLabel(labels?.noKey, 'mnCollection.filterNo', labels?.no ?? 'No'), value: 'false' },
6576
+ ];
6577
+ }
6474
6578
  /**
6475
6579
  * The width to render for a column: the consumer's own declared width always
6476
6580
  * wins, then a width pinned by the `stable` layout, otherwise none.
@@ -6547,11 +6651,6 @@ class MnTable extends MnSelectableCollectionBase {
6547
6651
  }
6548
6652
  }
6549
6653
  items = this.applySorting(items);
6550
- // With no column sorted, the order carries no meaning the user chose, so spend
6551
- // it on showing what is already selected. A sorted column always wins.
6552
- if (!this.currentSort) {
6553
- items = this.prioritizeInitialSelection(items);
6554
- }
6555
6654
  this.filteredItems = items;
6556
6655
  this.applyPagination();
6557
6656
  if (searchForItems) {
@@ -6800,12 +6899,30 @@ class MnTable extends MnSelectableCollectionBase {
6800
6899
  }
6801
6900
  });
6802
6901
  }
6902
+ /** Filter options formatted for mn-select for a given column. */
6903
+ getFilterSelectOptions(column) {
6904
+ const placeholder = column.filterPlaceholder ?? this.resolveLabel(undefined, 'mnCollection.filterAll', 'All');
6905
+ return [
6906
+ { label: placeholder, value: '' },
6907
+ ...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
6908
+ ];
6909
+ }
6910
+ /**
6911
+ * Accessible label for a tag's remove button.
6912
+ * @param row The row the tag stands for.
6913
+ * @returns The label, naming the row so screen readers announce which one goes.
6914
+ */
6915
+ selectionRemoveLabel(row) {
6916
+ const labels = this.dataSource.selectionSummaryLabels;
6917
+ const template = this.resolveLabel(labels?.removeKey, 'mnCollection.removeSelected', labels?.remove ?? 'Remove {{label}}');
6918
+ return template.replace('{{label}}', this.selectionLabelFor(row));
6919
+ }
6803
6920
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
6804
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6921
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6805
6922
  }
6806
6923
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
6807
6924
  type: Component,
6808
- args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n" }]
6925
+ args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n" }]
6809
6926
  }], ctorParameters: () => [], propDecorators: { sortChange: [{
6810
6927
  type: Output
6811
6928
  }], rowClick: [{
@@ -7336,6 +7453,11 @@ class MnFormBodyComponent {
7336
7453
  // sight, which is exactly what the summary exists to prevent. Set
7337
7454
  // `selectionSummary: false` on the data source to opt a field out.
7338
7455
  ds.selectionSummary ??= true;
7456
+ // A modal is too short to make a rows-per-page choice meaningful, and the
7457
+ // selector costs a row of footer the sheet cannot spare. Pinning the options
7458
+ // to the single configured size hides it through the paginator's existing
7459
+ // "more than one option" rule rather than adding a second way to hide it.
7460
+ ds.pageSizeOptions = [ds.pageSize ?? 10];
7339
7461
  // Pre-select rows from the form's initial value
7340
7462
  const control = this.form.get(field.key);
7341
7463
  if (control && Array.isArray(control.value) && control.value.length > 0) {
@@ -8128,11 +8250,11 @@ class MnWizardBodyComponent {
8128
8250
  }
8129
8251
  }
8130
8252
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8131
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "stepScroller", first: true, predicate: ["stepScroller"], descendants: true }, { propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
8253
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "stepScroller", first: true, predicate: ["stepScroller"], descendants: true }, { propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
8132
8254
  }
8133
8255
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
8134
8256
  type: Component,
8135
- args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent, LucideDynamicIcon], template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
8257
+ args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent, LucideDynamicIcon], template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
8136
8258
  }], propDecorators: { config: [{
8137
8259
  type: Input
8138
8260
  }], modalRef: [{
@@ -8264,11 +8386,11 @@ class MnConfirmationBodyComponent {
8264
8386
  return this.config.cancel?.icon ?? MN_MODAL_ACTION_ICONS.cancel;
8265
8387
  }
8266
8388
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8267
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.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 @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\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 @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\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 @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\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: "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 }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideTriangleAlert, selector: "svg[lucideTriangleAlert], svg[lucideAlertTriangle]" }, { kind: "component", type: LucideCircleAlert, selector: "svg[lucideCircleAlert], svg[lucideAlertCircle]" }] });
8389
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.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 @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\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 @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n type=\"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 @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\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: "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 }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideTriangleAlert, selector: "svg[lucideTriangleAlert], svg[lucideAlertTriangle]" }, { kind: "component", type: LucideCircleAlert, selector: "svg[lucideCircleAlert], svg[lucideAlertCircle]" }] });
8268
8390
  }
8269
8391
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
8270
8392
  type: Component,
8271
- args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule, LucideDynamicIcon, LucideTriangleAlert, LucideCircleAlert], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\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 @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\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 @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
8393
+ args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule, LucideDynamicIcon, LucideTriangleAlert, LucideCircleAlert], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\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 @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n type=\"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 @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
8272
8394
  }], propDecorators: { config: [{
8273
8395
  type: Input
8274
8396
  }], modalRef: [{
@@ -8279,6 +8401,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
8279
8401
  }] } });
8280
8402
 
8281
8403
  class MnModalShellComponent {
8404
+ lang = inject(MnLanguageService);
8405
+ /**
8406
+ * Accessible name for this control. Resolved through the conventional
8407
+ * `mnModal.close` key so an app can translate it, falling back to English when the
8408
+ * key is not defined rather than leaking the raw key into the UI.
8409
+ */
8410
+ get closeModalLabel() {
8411
+ return this.lang.translateIfPresent('mnModal.close') ?? 'Close modal';
8412
+ }
8282
8413
  el = inject(ElementRef);
8283
8414
  cdr = inject(ChangeDetectorRef);
8284
8415
  /** Downward release speed (px/ms) above which a short drag still dismisses — a "flick".
@@ -8699,7 +8830,7 @@ class MnModalShellComponent {
8699
8830
  }
8700
8831
  }
8701
8832
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8702
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.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" } }, viewQueries: [{ propertyName: "wizardBody", first: true, predicate: MnWizardBodyComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\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>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);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}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@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)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}: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}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
8833
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.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" } }, viewQueries: [{ propertyName: "wizardBody", first: true, predicate: MnWizardBodyComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\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>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);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}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@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)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}: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}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
8703
8834
  }
8704
8835
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
8705
8836
  type: Component,
@@ -8712,7 +8843,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
8712
8843
  MnFooterActionsComponent,
8713
8844
  MnButton,
8714
8845
  LucideX,
8715
- ], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\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>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);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}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@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)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}: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}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"] }]
8846
+ ], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\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>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);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}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@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)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}: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}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"] }]
8716
8847
  }], propDecorators: { config: [{
8717
8848
  type: Input
8718
8849
  }], modalRef: [{
@@ -8825,12 +8956,16 @@ class MnList extends MnSelectableCollectionBase {
8825
8956
  this.loadMoreRows();
8826
8957
  }
8827
8958
  }
8959
+ /** Accessible name for the scrollable list region. */
8960
+ get listRegionLabel() {
8961
+ return this.resolveLabel(undefined, 'mnCollection.dataList', 'Data list');
8962
+ }
8828
8963
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: null, target: i0.ɵɵFactoryTarget.Component });
8829
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8964
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8830
8965
  }
8831
8966
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
8832
8967
  type: Component,
8833
- args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n" }]
8968
+ args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n" }]
8834
8969
  }], propDecorators: { itemClick: [{
8835
8970
  type: Output
8836
8971
  }], collectionBody: [{
@@ -8976,12 +9111,20 @@ class MnGrid extends MnCollectionBase {
8976
9111
  this.loadMoreRows();
8977
9112
  }
8978
9113
  }
9114
+ /** Accessible name for the scrollable grid region. */
9115
+ get gridRegionLabel() {
9116
+ return this.resolveLabel(undefined, 'mnCollection.cardGrid', 'Card grid');
9117
+ }
9118
+ /** Accessible name for the loading placeholder. */
9119
+ get loadingLabel() {
9120
+ return this.resolveLabel(undefined, 'mnCollection.loading', 'Loading');
9121
+ }
8979
9122
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, deps: null, target: i0.ɵɵFactoryTarget.Component });
8980
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n aria-label=\"Loading\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-label=\"Card grid\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n [attr.aria-label]=\"loadingLabel\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n [attr.aria-label]=\"gridRegionLabel\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8981
9124
  }
8982
9125
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, decorators: [{
8983
9126
  type: Component,
8984
- args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination, LucideDynamicIcon], host: { class: 'block' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n aria-label=\"Loading\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-label=\"Card grid\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n" }]
9127
+ args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination, LucideDynamicIcon], host: { class: 'block' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n [attr.aria-label]=\"loadingLabel\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n [attr.aria-label]=\"gridRegionLabel\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n" }]
8985
9128
  }], propDecorators: { itemClick: [{
8986
9129
  type: Output
8987
9130
  }], collectionBody: [{
@@ -9182,6 +9325,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
9182
9325
  * events on that day. Clicking a cell emits `dayClicked`.
9183
9326
  */
9184
9327
  class CalendarMonthComponent {
9328
+ lang = inject(MnLanguageService);
9329
+ /**
9330
+ * Accessible name for this control. Resolved through the conventional
9331
+ * `mnCalendar.monthView` key so an app can translate it, falling back to English when the
9332
+ * key is not defined rather than leaking the raw key into the UI.
9333
+ */
9334
+ get monthViewLabel() {
9335
+ return this.lang.translateIfPresent('mnCalendar.monthView') ?? 'Month view';
9336
+ }
9185
9337
  /** The date whose month is displayed. */
9186
9338
  focusDay;
9187
9339
  /** Observable that emits the full event list whenever it changes. */
@@ -9283,11 +9435,11 @@ class CalendarMonthComponent {
9283
9435
  };
9284
9436
  }
9285
9437
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9286
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "mn-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div class=\"w-full flex flex-col\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"grid grid-cols-7 pb-2\">\n @for (day of weekdayLabels; track day) {\n <div class=\"pl-2 text-left text-[11px] font-semibold uppercase tracking-wide opacity-45\" role=\"columnheader\">{{ day }}</div>\n }\n </div>\n <div class=\"grid grid-cols-7 gap-1 auto-rows-[minmax(92px,auto)]\">\n @for (item of monthItems; track item.date.getTime()) {\n <div\n class=\"flex flex-col gap-1 min-h-0 overflow-hidden rounded-xl p-1.5 cursor-pointer transition-colors hover:bg-base-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary motion-reduce:transition-none\"\n [class.opacity-40]=\"!item.isCurrentMonth\"\n (keyup.enter)=\"onDayClick(item.date)\"\n (click)=\"onDayClick(item.date)\"\n tabindex=\"0\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"text-[13px] font-bold leading-none tabular-nums\" [class.text-primary]=\"item.isToday\">{{ item.dayNumber }}</span>\n <div class=\"flex flex-col gap-0.5 min-h-0\">\n @for (event of item.events.slice(0, 3); track $index) {\n <div\n class=\"flex items-center overflow-hidden rounded-md border-l-[3px] px-1.5 py-0.5 text-[11px] leading-tight\"\n [style.background-color]=\"event.color.secondaryColor\"\n [style.border-left-color]=\"event.color.primaryColor\"\n [style.color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n <span class=\"truncate font-semibold\">{{ event.title }}</span>\n </div>\n }\n <!-- Overflow indicator. Styled as a pill that lights up on hover so it\n reads as actionable; the whole cell already navigates to the Day\n view for this date on click / Enter, where every event is listed. -->\n @if (item.events.length > 3) {\n <span class=\"mt-0.5 inline-flex w-fit items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold opacity-60 transition-colors hover:bg-base-300 hover:opacity-100 motion-reduce:transition-none\">\n +{{ item.events.length - 3 }} {{ moreEventsLabel }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
9438
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "mn-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div [attr.aria-label]=\"monthViewLabel\" class=\"w-full flex flex-col\" role=\"grid\">\n <div class=\"grid grid-cols-7 pb-2\">\n @for (day of weekdayLabels; track day) {\n <div class=\"pl-2 text-left text-[11px] font-semibold uppercase tracking-wide opacity-45\" role=\"columnheader\">{{ day }}</div>\n }\n </div>\n <div class=\"grid grid-cols-7 gap-1 auto-rows-[minmax(92px,auto)]\">\n @for (item of monthItems; track item.date.getTime()) {\n <div\n class=\"flex flex-col gap-1 min-h-0 overflow-hidden rounded-xl p-1.5 cursor-pointer transition-colors hover:bg-base-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary motion-reduce:transition-none\"\n [class.opacity-40]=\"!item.isCurrentMonth\"\n (keyup.enter)=\"onDayClick(item.date)\"\n (click)=\"onDayClick(item.date)\"\n tabindex=\"0\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"text-[13px] font-bold leading-none tabular-nums\" [class.text-primary]=\"item.isToday\">{{ item.dayNumber }}</span>\n <div class=\"flex flex-col gap-0.5 min-h-0\">\n @for (event of item.events.slice(0, 3); track $index) {\n <div\n class=\"flex items-center overflow-hidden rounded-md border-l-[3px] px-1.5 py-0.5 text-[11px] leading-tight\"\n [style.background-color]=\"event.color.secondaryColor\"\n [style.border-left-color]=\"event.color.primaryColor\"\n [style.color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n <span class=\"truncate font-semibold\">{{ event.title }}</span>\n </div>\n }\n <!-- Overflow indicator. Styled as a pill that lights up on hover so it\n reads as actionable; the whole cell already navigates to the Day\n view for this date on click / Enter, where every event is listed. -->\n @if (item.events.length > 3) {\n <span class=\"mt-0.5 inline-flex w-fit items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold opacity-60 transition-colors hover:bg-base-300 hover:opacity-100 motion-reduce:transition-none\">\n +{{ item.events.length - 3 }} {{ moreEventsLabel }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
9287
9439
  }
9288
9440
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
9289
9441
  type: Component,
9290
- args: [{ selector: 'mn-calendar-month', standalone: true, imports: [CommonModule], template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div class=\"w-full flex flex-col\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"grid grid-cols-7 pb-2\">\n @for (day of weekdayLabels; track day) {\n <div class=\"pl-2 text-left text-[11px] font-semibold uppercase tracking-wide opacity-45\" role=\"columnheader\">{{ day }}</div>\n }\n </div>\n <div class=\"grid grid-cols-7 gap-1 auto-rows-[minmax(92px,auto)]\">\n @for (item of monthItems; track item.date.getTime()) {\n <div\n class=\"flex flex-col gap-1 min-h-0 overflow-hidden rounded-xl p-1.5 cursor-pointer transition-colors hover:bg-base-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary motion-reduce:transition-none\"\n [class.opacity-40]=\"!item.isCurrentMonth\"\n (keyup.enter)=\"onDayClick(item.date)\"\n (click)=\"onDayClick(item.date)\"\n tabindex=\"0\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"text-[13px] font-bold leading-none tabular-nums\" [class.text-primary]=\"item.isToday\">{{ item.dayNumber }}</span>\n <div class=\"flex flex-col gap-0.5 min-h-0\">\n @for (event of item.events.slice(0, 3); track $index) {\n <div\n class=\"flex items-center overflow-hidden rounded-md border-l-[3px] px-1.5 py-0.5 text-[11px] leading-tight\"\n [style.background-color]=\"event.color.secondaryColor\"\n [style.border-left-color]=\"event.color.primaryColor\"\n [style.color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n <span class=\"truncate font-semibold\">{{ event.title }}</span>\n </div>\n }\n <!-- Overflow indicator. Styled as a pill that lights up on hover so it\n reads as actionable; the whole cell already navigates to the Day\n view for this date on click / Enter, where every event is listed. -->\n @if (item.events.length > 3) {\n <span class=\"mt-0.5 inline-flex w-fit items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold opacity-60 transition-colors hover:bg-base-300 hover:opacity-100 motion-reduce:transition-none\">\n +{{ item.events.length - 3 }} {{ moreEventsLabel }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n</div>\n" }]
9442
+ args: [{ selector: 'mn-calendar-month', standalone: true, imports: [CommonModule], template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div [attr.aria-label]=\"monthViewLabel\" class=\"w-full flex flex-col\" role=\"grid\">\n <div class=\"grid grid-cols-7 pb-2\">\n @for (day of weekdayLabels; track day) {\n <div class=\"pl-2 text-left text-[11px] font-semibold uppercase tracking-wide opacity-45\" role=\"columnheader\">{{ day }}</div>\n }\n </div>\n <div class=\"grid grid-cols-7 gap-1 auto-rows-[minmax(92px,auto)]\">\n @for (item of monthItems; track item.date.getTime()) {\n <div\n class=\"flex flex-col gap-1 min-h-0 overflow-hidden rounded-xl p-1.5 cursor-pointer transition-colors hover:bg-base-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary motion-reduce:transition-none\"\n [class.opacity-40]=\"!item.isCurrentMonth\"\n (keyup.enter)=\"onDayClick(item.date)\"\n (click)=\"onDayClick(item.date)\"\n tabindex=\"0\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"text-[13px] font-bold leading-none tabular-nums\" [class.text-primary]=\"item.isToday\">{{ item.dayNumber }}</span>\n <div class=\"flex flex-col gap-0.5 min-h-0\">\n @for (event of item.events.slice(0, 3); track $index) {\n <div\n class=\"flex items-center overflow-hidden rounded-md border-l-[3px] px-1.5 py-0.5 text-[11px] leading-tight\"\n [style.background-color]=\"event.color.secondaryColor\"\n [style.border-left-color]=\"event.color.primaryColor\"\n [style.color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n <span class=\"truncate font-semibold\">{{ event.title }}</span>\n </div>\n }\n <!-- Overflow indicator. Styled as a pill that lights up on hover so it\n reads as actionable; the whole cell already navigates to the Day\n view for this date on click / Enter, where every event is listed. -->\n @if (item.events.length > 3) {\n <span class=\"mt-0.5 inline-flex w-fit items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold opacity-60 transition-colors hover:bg-base-300 hover:opacity-100 motion-reduce:transition-none\">\n +{{ item.events.length - 3 }} {{ moreEventsLabel }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n</div>\n" }]
9291
9443
  }], ctorParameters: () => [], propDecorators: { focusDay: [{
9292
9444
  type: Input
9293
9445
  }], eventsChanged: [{
@@ -9577,6 +9729,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
9577
9729
  * so they appear side-by-side rather than stacked.
9578
9730
  */
9579
9731
  class CalendarWeekComponent {
9732
+ lang = inject(MnLanguageService);
9733
+ /**
9734
+ * Accessible name for this control. Resolved through the conventional
9735
+ * `mnCalendar.weekView` key so an app can translate it, falling back to English when the
9736
+ * key is not defined rather than leaking the raw key into the UI.
9737
+ */
9738
+ get weekViewLabel() {
9739
+ return this.lang.translateIfPresent('mnCalendar.weekView') ?? 'Week view';
9740
+ }
9580
9741
  layoutService = inject(CalendarEventLayoutService);
9581
9742
  cdr = inject(ChangeDetectorRef);
9582
9743
  /** The date around which the week is centred. */
@@ -9780,11 +9941,11 @@ class CalendarWeekComponent {
9780
9941
  }
9781
9942
  }
9782
9943
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9783
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "mn-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"grid\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div></div>\n @for (col of columns; track col.dayName; let i = $index) {\n <div class=\"py-2 px-1 text-center\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"block text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ col.dayName }}</span>\n <span class=\"text-lg font-bold tabular-nums\" [class.text-primary]=\"col.isToday\">{{ col.dayNumber }}</span>\n </div>\n }\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && currentTimeCol) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarEventComponent, selector: "mn-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
9944
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "mn-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div [attr.aria-label]=\"weekViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div></div>\n @for (col of columns; track col.dayName; let i = $index) {\n <div class=\"py-2 px-1 text-center\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"block text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ col.dayName }}</span>\n <span class=\"text-lg font-bold tabular-nums\" [class.text-primary]=\"col.isToday\">{{ col.dayNumber }}</span>\n </div>\n }\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && currentTimeCol) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarEventComponent, selector: "mn-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
9784
9945
  }
9785
9946
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
9786
9947
  type: Component,
9787
- args: [{ selector: 'mn-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"grid\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div></div>\n @for (col of columns; track col.dayName; let i = $index) {\n <div class=\"py-2 px-1 text-center\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"block text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ col.dayName }}</span>\n <span class=\"text-lg font-bold tabular-nums\" [class.text-primary]=\"col.isToday\">{{ col.dayNumber }}</span>\n </div>\n }\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && currentTimeCol) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
9948
+ args: [{ selector: 'mn-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div [attr.aria-label]=\"weekViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div></div>\n @for (col of columns; track col.dayName; let i = $index) {\n <div class=\"py-2 px-1 text-center\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"block text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ col.dayName }}</span>\n <span class=\"text-lg font-bold tabular-nums\" [class.text-primary]=\"col.isToday\">{{ col.dayNumber }}</span>\n </div>\n }\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && currentTimeCol) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
9788
9949
  }], ctorParameters: () => [], propDecorators: { focusDay: [{
9789
9950
  type: Input
9790
9951
  }], eventsChanged: [{
@@ -9806,6 +9967,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
9806
9967
  * {@link CalendarEventLayoutService}.
9807
9968
  */
9808
9969
  class CalendarDayComponent {
9970
+ lang = inject(MnLanguageService);
9971
+ /**
9972
+ * Accessible name for this control. Resolved through the conventional
9973
+ * `mnCalendar.dayView` key so an app can translate it, falling back to English when the
9974
+ * key is not defined rather than leaking the raw key into the UI.
9975
+ */
9976
+ get dayViewLabel() {
9977
+ return this.lang.translateIfPresent('mnCalendar.dayView') ?? 'Day view';
9978
+ }
9809
9979
  layoutService = inject(CalendarEventLayoutService);
9810
9980
  cdr = inject(ChangeDetectorRef);
9811
9981
  /** The date to display. */
@@ -9951,11 +10121,11 @@ class CalendarDayComponent {
9951
10121
  }
9952
10122
  }
9953
10123
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9954
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "mn-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"grid grid-cols-[60px_1fr]\">\n <div></div>\n <div class=\"flex items-center justify-center gap-2 py-2 px-1\" role=\"columnheader\">\n <span class=\"text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ dayName }}</span>\n <span class=\"text-base font-bold tabular-nums\" [class.text-primary]=\"isToday\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && isToday) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarEventComponent, selector: "mn-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
10124
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "mn-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div [attr.aria-label]=\"dayViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid grid-cols-[60px_1fr]\">\n <div></div>\n <div class=\"flex items-center justify-center gap-2 py-2 px-1\" role=\"columnheader\">\n <span class=\"text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ dayName }}</span>\n <span class=\"text-base font-bold tabular-nums\" [class.text-primary]=\"isToday\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && isToday) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarEventComponent, selector: "mn-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
9955
10125
  }
9956
10126
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
9957
10127
  type: Component,
9958
- args: [{ selector: 'mn-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"grid grid-cols-[60px_1fr]\">\n <div></div>\n <div class=\"flex items-center justify-center gap-2 py-2 px-1\" role=\"columnheader\">\n <span class=\"text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ dayName }}</span>\n <span class=\"text-base font-bold tabular-nums\" [class.text-primary]=\"isToday\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && isToday) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
10128
+ args: [{ selector: 'mn-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div [attr.aria-label]=\"dayViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid grid-cols-[60px_1fr]\">\n <div></div>\n <div class=\"flex items-center justify-center gap-2 py-2 px-1\" role=\"columnheader\">\n <span class=\"text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ dayName }}</span>\n <span class=\"text-base font-bold tabular-nums\" [class.text-primary]=\"isToday\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && isToday) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
9959
10129
  }], ctorParameters: () => [], propDecorators: { focusDay: [{
9960
10130
  type: Input
9961
10131
  }], eventsChanged: [{
@@ -10009,6 +10179,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
10009
10179
  * (events whose end time is in the future), sorted by start time.
10010
10180
  */
10011
10181
  class UpcomingEventsComponent {
10182
+ lang = inject(MnLanguageService);
10183
+ /**
10184
+ * Accessible name for this control. Resolved through the conventional
10185
+ * `mnCalendar.upcomingEvents` key so an app can translate it, falling back to English when the
10186
+ * key is not defined rather than leaking the raw key into the UI.
10187
+ */
10188
+ get upcomingEventsLabel() {
10189
+ return this.lang.translateIfPresent('mnCalendar.upcomingEvents') ?? 'Upcoming events';
10190
+ }
10012
10191
  /** Observable that emits the full event list whenever it changes. */
10013
10192
  eventsChanged;
10014
10193
  /** Resolved calendar configuration passed from the parent view. */
@@ -10054,11 +10233,11 @@ class UpcomingEventsComponent {
10054
10233
  return event.id;
10055
10234
  }
10056
10235
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10057
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "mn-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"p-4\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"mb-3 flex items-center gap-2\">\n <h3 class=\"text-sm font-bold tracking-wide\">{{ title }}</h3>\n @if (upcomingEvents.length) {\n <span class=\"text-xs font-semibold tabular-nums opacity-45\">{{ upcomingEvents.length }}</span>\n }\n </div>\n @for (event of upcomingEvents; track $index) {\n <mn-upcoming-event-row\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </mn-upcoming-event-row>\n }\n @if (upcomingEvents.length === 0) {\n <div class=\"text-sm opacity-50\">{{ noEventsMessage }}</div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UpcomingEventRowComponent, selector: "mn-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
10236
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "mn-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, usesOnChanges: true, ngImport: i0, template: "<div [attr.aria-label]=\"upcomingEventsLabel\" class=\"p-4\" role=\"complementary\">\n <div class=\"mb-3 flex items-center gap-2\">\n <h3 class=\"text-sm font-bold tracking-wide\">{{ title }}</h3>\n @if (upcomingEvents.length) {\n <span class=\"text-xs font-semibold tabular-nums opacity-45\">{{ upcomingEvents.length }}</span>\n }\n </div>\n @for (event of upcomingEvents; track $index) {\n <mn-upcoming-event-row\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </mn-upcoming-event-row>\n }\n @if (upcomingEvents.length === 0) {\n <div class=\"text-sm opacity-50\">{{ noEventsMessage }}</div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UpcomingEventRowComponent, selector: "mn-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
10058
10237
  }
10059
10238
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
10060
10239
  type: Component,
10061
- args: [{ selector: 'mn-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div class=\"p-4\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"mb-3 flex items-center gap-2\">\n <h3 class=\"text-sm font-bold tracking-wide\">{{ title }}</h3>\n @if (upcomingEvents.length) {\n <span class=\"text-xs font-semibold tabular-nums opacity-45\">{{ upcomingEvents.length }}</span>\n }\n </div>\n @for (event of upcomingEvents; track $index) {\n <mn-upcoming-event-row\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </mn-upcoming-event-row>\n }\n @if (upcomingEvents.length === 0) {\n <div class=\"text-sm opacity-50\">{{ noEventsMessage }}</div>\n }\n</div>\n" }]
10240
+ args: [{ selector: 'mn-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div [attr.aria-label]=\"upcomingEventsLabel\" class=\"p-4\" role=\"complementary\">\n <div class=\"mb-3 flex items-center gap-2\">\n <h3 class=\"text-sm font-bold tracking-wide\">{{ title }}</h3>\n @if (upcomingEvents.length) {\n <span class=\"text-xs font-semibold tabular-nums opacity-45\">{{ upcomingEvents.length }}</span>\n }\n </div>\n @for (event of upcomingEvents; track $index) {\n <mn-upcoming-event-row\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </mn-upcoming-event-row>\n }\n @if (upcomingEvents.length === 0) {\n <div class=\"text-sm opacity-50\">{{ noEventsMessage }}</div>\n }\n</div>\n" }]
10062
10241
  }], ctorParameters: () => [], propDecorators: { eventsChanged: [{
10063
10242
  type: Input
10064
10243
  }], config: [{
@@ -10094,6 +10273,14 @@ let instanceCounter = 0;
10094
10273
  * ```
10095
10274
  */
10096
10275
  class CalendarViewComponent {
10276
+ /**
10277
+ * Accessible name for this control. Resolved through the conventional
10278
+ * `mnCalendar.calendarView` key so an app can translate it, falling back to English when the
10279
+ * key is not defined rather than leaking the raw key into the UI.
10280
+ */
10281
+ get calendarViewLabel() {
10282
+ return this.lang.translateIfPresent('mnCalendar.calendarView') ?? 'Calendar view';
10283
+ }
10097
10284
  /** Whether to show the action button in the toolbar. */
10098
10285
  showButton = false;
10099
10286
  /** Label text for the action button. */
@@ -10305,7 +10492,7 @@ class CalendarViewComponent {
10305
10492
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10306
10493
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "mn-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", buttons: "buttons", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
10307
10494
  provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
10308
- ], ngImport: i0, template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div aria-label=\"Calendar view\" class=\"flex border border-base-300 rounded-md overflow-hidden\" role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\"\n type=\"button\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [data]=\"btn.buttonData || {}\" mnButton type=\"button\">\n {{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [data]=\"{}\" mnButton type=\"button\">\n {{ buttonTitle }}\n </button>\n }\n }\n </div>\n\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div [id]=\"panelId\" class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\" role=\"tabpanel\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: CalendarMonthComponent, selector: "mn-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "mn-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "mn-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "mn-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: LucideChevronLeft, selector: "svg[lucideChevronLeft]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }] });
10495
+ ], ngImport: i0, template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div [attr.aria-label]=\"calendarViewLabel\" class=\"flex border border-base-300 rounded-md overflow-hidden\"\n role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\"\n type=\"button\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [data]=\"btn.buttonData || {}\" mnButton type=\"button\">\n {{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [data]=\"{}\" mnButton type=\"button\">\n {{ buttonTitle }}\n </button>\n }\n }\n </div>\n\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div [id]=\"panelId\" class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\" role=\"tabpanel\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: CalendarMonthComponent, selector: "mn-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "mn-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "mn-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "mn-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: LucideChevronLeft, selector: "svg[lucideChevronLeft]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }] });
10309
10496
  }
10310
10497
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
10311
10498
  type: Component,
@@ -10322,7 +10509,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
10322
10509
  LucideChevronRight
10323
10510
  ], providers: [
10324
10511
  provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
10325
- ], template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div aria-label=\"Calendar view\" class=\"flex border border-base-300 rounded-md overflow-hidden\" role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\"\n type=\"button\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [data]=\"btn.buttonData || {}\" mnButton type=\"button\">\n {{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [data]=\"{}\" mnButton type=\"button\">\n {{ buttonTitle }}\n </button>\n }\n }\n </div>\n\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div [id]=\"panelId\" class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\" role=\"tabpanel\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"] }]
10512
+ ], template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div [attr.aria-label]=\"calendarViewLabel\" class=\"flex border border-base-300 rounded-md overflow-hidden\"\n role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\"\n type=\"button\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [data]=\"btn.buttonData || {}\" mnButton type=\"button\">\n {{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [data]=\"{}\" mnButton type=\"button\">\n {{ buttonTitle }}\n </button>\n }\n }\n </div>\n\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div [id]=\"panelId\" class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\" role=\"tabpanel\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"] }]
10326
10513
  }], ctorParameters: () => [], propDecorators: { showButton: [{
10327
10514
  type: Input
10328
10515
  }], buttonTitle: [{
@@ -10355,6 +10542,12 @@ const DEFAULT_SKELETON_TAB_COUNT = 3;
10355
10542
  class MnTabComponent {
10356
10543
  /** The horizontally-scrolling wrapper the edge fade is painted onto. */
10357
10544
  scrollContainer;
10545
+ /** The tab row; queried for the active tab so the indicator can measure it. */
10546
+ tabList;
10547
+ /** The shared underline that slides to the active tab. */
10548
+ indicator;
10549
+ /** Pending indicator remeasure, cancelled on destroy so a post-destroy frame can't read a detached ref. */
10550
+ indicatorFrame;
10358
10551
  /** How far the fade reaches in from each overflowing edge. */
10359
10552
  static FADE = '2rem';
10360
10553
  /** Data source containing tab items and default active index. */
@@ -10418,14 +10611,23 @@ class MnTabComponent {
10418
10611
  const el = this.scrollContainer?.nativeElement;
10419
10612
  if (!el)
10420
10613
  return;
10421
- this.resizeObserver = new ResizeObserver(() => this.updateEdgeFades());
10614
+ this.resizeObserver = new ResizeObserver(() => {
10615
+ this.updateEdgeFades();
10616
+ // Tabs may have reflowed (viewport change, justified widths); snap the
10617
+ // indicator to the new geometry — animating a resize tick reads as jank.
10618
+ this.updateIndicator(false);
10619
+ });
10422
10620
  this.resizeObserver.observe(el);
10423
10621
  if (el.firstElementChild)
10424
10622
  this.resizeObserver.observe(el.firstElementChild);
10425
10623
  this.updateEdgeFades();
10624
+ // Place the indicator on the default tab without a slide-in from zero.
10625
+ this.updateIndicator(false);
10426
10626
  }
10427
10627
  ngOnDestroy() {
10428
10628
  this.resizeObserver?.disconnect();
10629
+ if (this.indicatorFrame !== undefined)
10630
+ cancelAnimationFrame(this.indicatorFrame);
10429
10631
  }
10430
10632
  /**
10431
10633
  * Paints a fade over whichever edge has tabs scrolled out of view — a soft
@@ -10465,6 +10667,52 @@ class MnTabComponent {
10465
10667
  item.onClick?.();
10466
10668
  this.currentActive = item;
10467
10669
  this.activeChange.emit(item);
10670
+ // Slide the underline to the new tab. Measure on the next frame, after
10671
+ // change detection has applied the active tab's `font-bold` (which widens
10672
+ // it) so the indicator lands on the final, bolded geometry.
10673
+ this.scheduleIndicator(true);
10674
+ }
10675
+ /**
10676
+ * Moves the shared underline to the active tab. When `animate` is false the
10677
+ * move is snapped (no slide) by disabling the transition for one reflow —
10678
+ * used on init, async selection and resize, where a slide would read as jank.
10679
+ * @param animate - Whether the move should slide (true) or snap (false).
10680
+ */
10681
+ updateIndicator(animate) {
10682
+ const bar = this.indicator?.nativeElement;
10683
+ const list = this.tabList?.nativeElement;
10684
+ if (!bar || !list)
10685
+ return;
10686
+ const active = list.querySelector('[role="tab"][aria-selected="true"]');
10687
+ if (!active) {
10688
+ bar.style.opacity = '0';
10689
+ return;
10690
+ }
10691
+ if (!animate)
10692
+ bar.style.transition = 'none';
10693
+ bar.style.opacity = '1';
10694
+ bar.style.width = `${active.offsetWidth}px`;
10695
+ bar.style.transform = `translateX(${active.offsetLeft}px)`;
10696
+ if (!animate) {
10697
+ // Force a reflow so the snapped values apply before the transition is
10698
+ // restored, then hand animation back to the CSS class.
10699
+ void bar.offsetWidth;
10700
+ bar.style.transition = '';
10701
+ }
10702
+ }
10703
+ /**
10704
+ * Remeasures the indicator on the next animation frame, so the read happens
10705
+ * after layout reflects the latest active-tab classes. Coalesces bursts and
10706
+ * is cancellable on destroy.
10707
+ * @param animate - Whether the resulting move should slide.
10708
+ */
10709
+ scheduleIndicator(animate) {
10710
+ if (this.indicatorFrame !== undefined)
10711
+ cancelAnimationFrame(this.indicatorFrame);
10712
+ this.indicatorFrame = requestAnimationFrame(() => {
10713
+ this.indicatorFrame = undefined;
10714
+ this.updateIndicator(animate);
10715
+ });
10468
10716
  }
10469
10717
  /**
10470
10718
  * Returns the resolved badge value for a tab item, supporting both plain numbers and Signal<number>.
@@ -10483,7 +10731,10 @@ class MnTabComponent {
10483
10731
  syncActiveTab() {
10484
10732
  const items = this.dataSource?.items;
10485
10733
  if (!items || items.length === 0) {
10486
- this.currentActive = undefined;
10734
+ if (this.currentActive !== undefined) {
10735
+ this.currentActive = undefined;
10736
+ this.scheduleIndicator(false);
10737
+ }
10487
10738
  return;
10488
10739
  }
10489
10740
  if (this.currentActive && items.includes(this.currentActive)) {
@@ -10492,16 +10743,24 @@ class MnTabComponent {
10492
10743
  const defaultIndex = this.dataSource.defaultActive;
10493
10744
  const index = defaultIndex >= 0 && defaultIndex < items.length ? defaultIndex : 0;
10494
10745
  this.currentActive = items[index];
10746
+ // Selection resolved from data (not a user click): snap, don't slide.
10747
+ this.scheduleIndicator(false);
10495
10748
  }
10496
10749
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10497
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
10750
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "tabList", first: true, predicate: ["tabList"], descendants: true }, { propertyName: "indicator", first: true, predicate: ["indicator"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
10498
10751
  }
10499
10752
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, decorators: [{
10500
10753
  type: Component,
10501
- args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
10754
+ args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
10502
10755
  }], propDecorators: { scrollContainer: [{
10503
10756
  type: ViewChild,
10504
10757
  args: ['scrollContainer']
10758
+ }], tabList: [{
10759
+ type: ViewChild,
10760
+ args: ['tabList']
10761
+ }], indicator: [{
10762
+ type: ViewChild,
10763
+ args: ['indicator']
10505
10764
  }], dataSource: [{
10506
10765
  type: Input
10507
10766
  }], scrollable: [{
@@ -10512,6 +10771,263 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
10512
10771
  type: Output
10513
10772
  }] } });
10514
10773
 
10774
+ /**
10775
+ * Toolbar the editor renders when the consumer does not supply one. Deliberately
10776
+ * small: it covers written prose, so there is no colour picker, no font picker
10777
+ * and no image embedding (an image needs an upload and storage path the editor
10778
+ * knows nothing about).
10779
+ */
10780
+ const DEFAULT_TOOLBAR = [
10781
+ [{ header: [2, 3, false] }],
10782
+ ['bold', 'italic', 'underline', 'strike'],
10783
+ [{ list: 'ordered' }, { list: 'bullet' }],
10784
+ ['blockquote', 'code-block'],
10785
+ ['link'],
10786
+ ['clean'],
10787
+ ];
10788
+ /** Where each control sits in the toolbar Quill builds. */
10789
+ const CONTROL_SELECTORS = {
10790
+ textStyle: '.ql-header .ql-picker-label',
10791
+ bold: 'button.ql-bold',
10792
+ italic: 'button.ql-italic',
10793
+ underline: 'button.ql-underline',
10794
+ strike: 'button.ql-strike',
10795
+ orderedList: 'button.ql-list[value="ordered"]',
10796
+ bulletList: 'button.ql-list[value="bullet"]',
10797
+ blockquote: 'button.ql-blockquote',
10798
+ codeBlock: 'button.ql-code-block',
10799
+ link: 'button.ql-link',
10800
+ clean: 'button.ql-clean',
10801
+ };
10802
+ /** Hover labels used when the consumer supplies neither a label nor a key. */
10803
+ const DEFAULT_LABELS = {
10804
+ textStyle: 'Text style',
10805
+ bold: 'Bold',
10806
+ italic: 'Italic',
10807
+ underline: 'Underline',
10808
+ strike: 'Strikethrough',
10809
+ orderedList: 'Numbered list',
10810
+ bulletList: 'Bulleted list',
10811
+ blockquote: 'Quote',
10812
+ codeBlock: 'Code block',
10813
+ link: 'Link',
10814
+ clean: 'Clear formatting',
10815
+ };
10816
+ /** Quill's document when it holds no text; treated as an empty value. */
10817
+ const EMPTY_DOCUMENT = '<p><br></p>';
10818
+ /**
10819
+ * Thin wrapper around the {@link Quill} rich-text editor.
10820
+ *
10821
+ * Quill is used **directly** rather than through an Angular wrapper package: the
10822
+ * wrapper libraries carry peer-dependency ranges that lag behind Angular's
10823
+ * release train, and none of them add anything this component needs.
10824
+ *
10825
+ * Consumers must install `quill` themselves (it is an optional peer dependency)
10826
+ * and load its snow theme, e.g. `node_modules/quill/dist/quill.snow.css` in the
10827
+ * `styles` array of `angular.json`. Only the chrome around that theme — radius,
10828
+ * borders, height limits and the toolbar tooltips — belongs to this component;
10829
+ * recolouring Quill's own palette to an app theme stays with the app, because
10830
+ * the same `.ql-snow` markup is normally reused to render stored HTML in places
10831
+ * where no editor is mounted.
10832
+ *
10833
+ * Quill itself is pulled in with a dynamic `import()` when the editor mounts.
10834
+ * This component sits in the library's single entry point, which apps import
10835
+ * eagerly, so a static import would put the whole editor engine in every app's
10836
+ * initial bundle — including the pages that never open one.
10837
+ *
10838
+ * Zoneless notes: nothing here relies on an implicit change-detection tick. The
10839
+ * editor is created inside {@link afterNextRender} (the host element only exists
10840
+ * after the first render pass) and every value that flows back out is written to
10841
+ * a signal or emitted through an `output`, both of which schedule change
10842
+ * detection themselves. No `setTimeout`, no manual `detectChanges`.
10843
+ *
10844
+ * The produced HTML is **not** trusted: sanitise it before rendering it anywhere.
10845
+ *
10846
+ * @example
10847
+ * ```html
10848
+ * <mn-rich-text-editor
10849
+ * [content]="draft()"
10850
+ * [placeholder]="'minutes.placeholder' | mnTranslate"
10851
+ * [labelKeys]="{ bold: 'editor.bold', italic: 'editor.italic' }"
10852
+ * (contentChange)="draft.set($event)">
10853
+ * </mn-rich-text-editor>
10854
+ * ```
10855
+ */
10856
+ class MnRichTextEditor {
10857
+ /**
10858
+ * The initial HTML content. Later changes are applied only when they differ
10859
+ * from what the editor currently holds, so a parent echoing the emitted value
10860
+ * back never moves the caret.
10861
+ */
10862
+ content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
10863
+ /** Placeholder shown while the editor is empty. */
10864
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
10865
+ /** Accessible label for the editing surface. */
10866
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
10867
+ /** Toolbar layout, in Quill's own format. Defaults to a prose-oriented set. */
10868
+ toolbar = input(DEFAULT_TOOLBAR, ...(ngDevMode ? [{ debugName: "toolbar" }] : []));
10869
+ /** Literal hover labels per toolbar control. */
10870
+ labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
10871
+ /** Translation keys per toolbar control; takes precedence over `labels`. */
10872
+ labelKeys = input({}, ...(ngDevMode ? [{ debugName: "labelKeys" }] : []));
10873
+ /**
10874
+ * Utilities applied to the wrapper, for sizing the writing surface. Overriding
10875
+ * this replaces the default height limits, so pass both bounds when you do.
10876
+ */
10877
+ editorClass = input('[&_.ql-editor]:max-h-104 [&_.ql-editor]:min-h-72 [&_.ql-editor]:overflow-y-auto', ...(ngDevMode ? [{ debugName: "editorClass" }] : []));
10878
+ /** Emits the editor's HTML on every user edit. */
10879
+ contentChange = output();
10880
+ /**
10881
+ * Chrome around Quill's snow theme: the field's radius, border and surface.
10882
+ *
10883
+ * Descendant variants rather than a stylesheet — the utilities come from the
10884
+ * consuming app's Tailwind build (which scans this bundle), so they follow the
10885
+ * app's theme tokens the same way the rest of the library does.
10886
+ */
10887
+ chromeClass = '[&_.ql-container.ql-snow]:rounded-b-xl [&_.ql-container.ql-snow]:border-base-300 ' +
10888
+ '[&_.ql-container.ql-snow]:bg-base-100 [&_.ql-container.ql-snow]:text-base ' +
10889
+ '[&_.ql-toolbar.ql-snow]:rounded-t-xl [&_.ql-toolbar.ql-snow]:border-base-300 ' +
10890
+ '[&_.ql-toolbar.ql-snow]:bg-base-100';
10891
+ /** Host element, used to keep DOM queries inside this component. */
10892
+ host = inject(ElementRef);
10893
+ /** Language service, used to resolve the toolbar labels from keys. */
10894
+ lang = inject(MnLanguageService);
10895
+ /** The container Quill mounts into. */
10896
+ editorHost = viewChild.required('editorHost');
10897
+ /** The live editor instance, or null before Quill has loaded. */
10898
+ quill = null;
10899
+ /** Whether the component is gone, so a late Quill load knows to stop. */
10900
+ destroyed = false;
10901
+ /** The last HTML this component emitted, used to skip redundant writes. */
10902
+ lastEmitted = signal('', ...(ngDevMode ? [{ debugName: "lastEmitted" }] : []));
10903
+ constructor() {
10904
+ afterNextRender(() => void this.createEditor());
10905
+ // Push a changed `content` input into a live editor. `lastEmitted` is read
10906
+ // `untracked` on purpose: it must gate the write (skip when the incoming HTML
10907
+ // already matches what we hold) WITHOUT making the effect depend on it. If it
10908
+ // were tracked, every keystroke (which updates `lastEmitted`) would re-run the
10909
+ // effect and re-paste the now-stale `content` seed — wiping the user's typing
10910
+ // and resetting the caret after any wholesale reseed.
10911
+ effect(() => {
10912
+ const incoming = this.content();
10913
+ if (!this.quill || incoming === untracked(this.lastEmitted))
10914
+ return;
10915
+ this.setEditorHtml(incoming);
10916
+ });
10917
+ }
10918
+ /** Drops the editor reference so the instance can be garbage collected. */
10919
+ ngOnDestroy() {
10920
+ this.destroyed = true;
10921
+ this.quill = null;
10922
+ }
10923
+ /** Moves focus into the editing surface. */
10924
+ focusEditor() {
10925
+ this.quill?.focus();
10926
+ if (!this.quill) {
10927
+ this.host.nativeElement.querySelector('.ql-editor')?.focus();
10928
+ }
10929
+ }
10930
+ /**
10931
+ * Loads Quill, builds the instance and wires its change handler.
10932
+ *
10933
+ * Nothing awaits this beyond the component itself: the surface appears once
10934
+ * the engine has loaded, and until then the `content` effect is a no-op that
10935
+ * the seeding below makes good.
10936
+ */
10937
+ async createEditor() {
10938
+ const { default: QuillEditor } = await import('quill');
10939
+ if (this.destroyed)
10940
+ return;
10941
+ const container = this.editorHost().nativeElement;
10942
+ this.quill = new QuillEditor(container, {
10943
+ theme: 'snow',
10944
+ placeholder: this.placeholder(),
10945
+ modules: { toolbar: this.toolbar() },
10946
+ });
10947
+ const label = this.ariaLabel();
10948
+ if (label) {
10949
+ this.quill.root.setAttribute('aria-label', label);
10950
+ }
10951
+ this.applyToolbarLabels();
10952
+ this.setEditorHtml(this.content());
10953
+ this.quill.on('text-change', () => this.emitCurrentHtml());
10954
+ }
10955
+ /**
10956
+ * Gives each toolbar control a hover label, so hovering explains what the
10957
+ * style does. Set on the Quill-generated DOM after init; a control the current
10958
+ * toolbar does not render is simply skipped.
10959
+ */
10960
+ applyToolbarLabels() {
10961
+ const toolbar = this.host.nativeElement.querySelector('.ql-toolbar');
10962
+ if (!toolbar)
10963
+ return;
10964
+ const labels = this.labels();
10965
+ const keys = this.labelKeys();
10966
+ for (const [control, selector] of Object.entries(CONTROL_SELECTORS)) {
10967
+ const element = toolbar.querySelector(selector);
10968
+ if (!element)
10969
+ continue;
10970
+ element.classList.add('mn-rte-tooltip');
10971
+ element.setAttribute('data-tip', this.resolveLabel(control, labels, keys));
10972
+ }
10973
+ }
10974
+ /**
10975
+ * Picks the hover label for one control.
10976
+ * @param control The control being labelled.
10977
+ * @param labels Literal labels supplied by the consumer.
10978
+ * @param keys Translation keys supplied by the consumer.
10979
+ * @returns The translated key, the literal label, or the built-in default.
10980
+ */
10981
+ resolveLabel(control, labels, keys) {
10982
+ const key = keys[control];
10983
+ if (key) {
10984
+ // `t()` echoes the key back when the consumer has no translation for it;
10985
+ // that is a miss, not a label, so fall through to the remaining sources.
10986
+ const translated = this.lang.t(key);
10987
+ if (translated !== key)
10988
+ return translated;
10989
+ }
10990
+ return labels[control] ?? DEFAULT_LABELS[control];
10991
+ }
10992
+ /**
10993
+ * Replaces the editor content with stored HTML. Quill parses it into its own
10994
+ * document model, which silently drops anything it has no format for — a
10995
+ * useful extra filter on top of the consumer's sanitiser.
10996
+ * @param html The HTML to load into the editor.
10997
+ */
10998
+ setEditorHtml(html) {
10999
+ if (!this.quill)
11000
+ return;
11001
+ // `dangerouslyPasteHTML` is Quill's documented name for "parse this HTML".
11002
+ this.quill.clipboard.dangerouslyPasteHTML(html ?? '', 'silent');
11003
+ this.lastEmitted.set(this.readHtml());
11004
+ }
11005
+ /** Emits the editor's current HTML, normalising Quill's "empty" document. */
11006
+ emitCurrentHtml() {
11007
+ const html = this.readHtml();
11008
+ this.lastEmitted.set(html);
11009
+ this.contentChange.emit(html);
11010
+ }
11011
+ /**
11012
+ * Reads the editor's HTML.
11013
+ * @returns The current HTML, or an empty string when the editor is blank.
11014
+ */
11015
+ readHtml() {
11016
+ if (!this.quill)
11017
+ return '';
11018
+ // Quill leaves an empty paragraph behind after a clear; treat that as empty
11019
+ // so an untouched editor does not count as authored content.
11020
+ const html = this.quill.root.innerHTML;
11021
+ return html === EMPTY_DOCUMENT ? '' : html;
11022
+ }
11023
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnRichTextEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
11024
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: MnRichTextEditor, isStandalone: true, selector: "mn-rich-text-editor", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, labelKeys: { classPropertyName: "labelKeys", publicName: "labelKeys", isSignal: true, isRequired: false, transformFunction: null }, editorClass: { classPropertyName: "editorClass", publicName: "editorClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange" }, viewQueries: [{ propertyName: "editorHost", first: true, predicate: ["editorHost"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"mn-rich-text-editor {{ chromeClass }} {{ editorClass() }}\">\n <div #editorHost></div>\n</div>\n", styles: [".mn-rich-text-editor .mn-rte-tooltip{position:relative}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;translate:-50% -.375rem;padding:.25rem .5rem;border-radius:.375rem;background-color:var(--color-secondary);color:var(--color-secondary-content);font-size:.75rem;line-height:1rem;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:50}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:hover:after,.mn-rich-text-editor .mn-rte-tooltip[data-tip]:focus-visible:after{opacity:1}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11025
+ }
11026
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnRichTextEditor, decorators: [{
11027
+ type: Component,
11028
+ args: [{ selector: 'mn-rich-text-editor', standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mn-rich-text-editor {{ chromeClass }} {{ editorClass() }}\">\n <div #editorHost></div>\n</div>\n", styles: [".mn-rich-text-editor .mn-rte-tooltip{position:relative}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;translate:-50% -.375rem;padding:.25rem .5rem;border-radius:.375rem;background-color:var(--color-secondary);color:var(--color-secondary-content);font-size:.75rem;line-height:1rem;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:50}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:hover:after,.mn-rich-text-editor .mn-rte-tooltip[data-tip]:focus-visible:after{opacity:1}\n"] }]
11029
+ }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], labelKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKeys", required: false }] }], editorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "editorClass", required: false }] }], contentChange: [{ type: i0.Output, args: ["contentChange"] }], editorHost: [{ type: i0.ViewChild, args: ['editorHost', { isSignal: true }] }] } });
11030
+
10515
11031
  const mnIconVariants = tv({
10516
11032
  base: 'inline-flex shrink-0',
10517
11033
  variants: {
@@ -11134,5 +11650,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
11134
11650
  * Generated bundle index. Do not edit.
11135
11651
  */
11136
11652
 
11137
- 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_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
11653
+ 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_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnRichTextEditor, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
11138
11654
  //# sourceMappingURL=mn-angular-lib.mjs.map