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,
|
|
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
|
|
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 ×\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
|
|
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 ×\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
|
-
*
|
|
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 ??
|
|
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
|
|
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
|
|
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 {{
|
|
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 {{
|
|
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
|
-
/**
|
|
6237
|
-
|
|
6238
|
-
|
|
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
|
-
/**
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
6449
|
-
*
|
|
6450
|
-
*
|
|
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
|
-
|
|
6453
|
-
|
|
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=\"
|
|
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=\"
|
|
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=\"
|
|
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=\"
|
|
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=\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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=\"
|
|
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=\"
|
|
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(() =>
|
|
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
|
|
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.
|
|
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.
|
|
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
|