mn-angular-lib 1.0.90 → 1.0.92
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.
|
@@ -9,8 +9,8 @@ import * as i2 from '@angular/forms';
|
|
|
9
9
|
import { NgControl, Validators, FormsModule, NG_VALUE_ACCESSOR, FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
10
10
|
import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
|
|
11
11
|
import JSON5 from 'json5';
|
|
12
|
+
import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideFilter, LucideFunnel } from '@lucide/angular';
|
|
12
13
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
13
|
-
import { LucideX } from '@lucide/angular';
|
|
14
14
|
|
|
15
15
|
// projects/mn-angular-lib/src/lib/mn-mn-alert/mn-mn-alert.tokens.ts
|
|
16
16
|
const MN_ALERT_CONFIG = new InjectionToken('MN_ALERT_CONFIG');
|
|
@@ -1619,11 +1619,20 @@ class MnInputField {
|
|
|
1619
1619
|
});
|
|
1620
1620
|
}
|
|
1621
1621
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1622
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1622
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, host: { properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1623
1623
|
}
|
|
1624
1624
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, decorators: [{
|
|
1625
1625
|
type: Component,
|
|
1626
|
-
args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage, FormsModule],
|
|
1626
|
+
args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage, FormsModule], host: {
|
|
1627
|
+
// Native inputs (notably `type="date"`) have a platform-specific intrinsic width.
|
|
1628
|
+
// Without an explicit host width the inline host collapses to that intrinsic size,
|
|
1629
|
+
// so the input's `w-full` (width:100%) resolves against a content-sized box and
|
|
1630
|
+
// fails to fill the parent on real mobile devices (desktop devtools hides this
|
|
1631
|
+
// because it still renders the control with the desktop engine). Give the host a
|
|
1632
|
+
// real width when fullWidth is requested so 100% has something to fill.
|
|
1633
|
+
'[style.display]': "props?.fullWidth ? 'block' : null",
|
|
1634
|
+
'[style.width]': "props?.fullWidth ? '100%' : null",
|
|
1635
|
+
}, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n" }]
|
|
1627
1636
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
1628
1637
|
type: Input,
|
|
1629
1638
|
args: [{ required: true }]
|
|
@@ -2061,137 +2070,6 @@ const mnFileInputVariants = tv({
|
|
|
2061
2070
|
},
|
|
2062
2071
|
});
|
|
2063
2072
|
|
|
2064
|
-
const mnIconVariants = tv({
|
|
2065
|
-
base: 'inline-flex shrink-0',
|
|
2066
|
-
variants: {
|
|
2067
|
-
color: {
|
|
2068
|
-
current: 'text-current',
|
|
2069
|
-
primary: 'text-primary',
|
|
2070
|
-
secondary: 'text-neutral',
|
|
2071
|
-
danger: 'text-error',
|
|
2072
|
-
warning: 'text-warning',
|
|
2073
|
-
success: 'text-success',
|
|
2074
|
-
accent: 'text-accent',
|
|
2075
|
-
},
|
|
2076
|
-
},
|
|
2077
|
-
defaultVariants: {
|
|
2078
|
-
color: 'current',
|
|
2079
|
-
},
|
|
2080
|
-
});
|
|
2081
|
-
|
|
2082
|
-
// Inline SVGs sourced from the .svg files in the ./icons/ directory
|
|
2083
|
-
const pistolSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.72 14.56 9 10h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v4c2.5 0 1 4 1 4-4 6-1 6-1 6h3.38a1 1 0 0 0 .89-.55za1 1 0 0 1 .9-.56H13a2 2 0 0 0 2-2v-1a1 1 0 0 1 1-1" /></svg>`;
|
|
2084
|
-
const pendingSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v5l4 2" /><path d="M14 20.775A9 9 0 0112 21" /><path d="M19 17.656a9 9 0 01-1.5 1.456" /><path d="M21 12a9 9 0 01-.228 2" /><path d="M21 8h-5" /><path d="M7.5 19.794c-6-3.464-6-12.124 0-15.588A9 9 0 0112 3a9.75 9.75 0 016.74 2.74L21 8V3" /></svg>`;
|
|
2085
|
-
// Lucide icons (https://lucide.dev) — inner paths; mn-icon wraps them in a sized svg.
|
|
2086
|
-
const imagePlusSvg = `<path d="M16 5h6" /><path d="M19 2v6" /><path d="M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /><circle cx="9" cy="9" r="2" />`;
|
|
2087
|
-
const uploadSvg = `<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" x2="12" y1="3" y2="15" />`;
|
|
2088
|
-
const fileSvg = `<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" />`;
|
|
2089
|
-
const trash2Svg = `<path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" />`;
|
|
2090
|
-
const xSvg = `<path d="M18 6 6 18" /><path d="m6 6 12 12" />`;
|
|
2091
|
-
const MN_ICON_MAP = {
|
|
2092
|
-
pistol: pistolSvg,
|
|
2093
|
-
pending: pendingSvg,
|
|
2094
|
-
imagePlus: imagePlusSvg,
|
|
2095
|
-
upload: uploadSvg,
|
|
2096
|
-
file: fileSvg,
|
|
2097
|
-
trash2: trash2Svg,
|
|
2098
|
-
x: xSvg,
|
|
2099
|
-
};
|
|
2100
|
-
|
|
2101
|
-
class MnIcon {
|
|
2102
|
-
data = {};
|
|
2103
|
-
sanitizer = inject(DomSanitizer);
|
|
2104
|
-
el = inject(ElementRef);
|
|
2105
|
-
svgContent = '';
|
|
2106
|
-
get iconSize() {
|
|
2107
|
-
return this.data.size ?? 24;
|
|
2108
|
-
}
|
|
2109
|
-
get hostClasses() {
|
|
2110
|
-
return mnIconVariants({
|
|
2111
|
-
color: this.data.color,
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
resolvedName = null;
|
|
2115
|
-
ngOnInit() {
|
|
2116
|
-
const attrs = this.el.nativeElement.attributes;
|
|
2117
|
-
for (const item of Array.from(attrs)) {
|
|
2118
|
-
const attr = item.name.toLowerCase();
|
|
2119
|
-
if (attr.startsWith('mnicon')) {
|
|
2120
|
-
const iconKey = attr.slice('mnicon'.length);
|
|
2121
|
-
for (const key of Object.keys(MN_ICON_MAP)) {
|
|
2122
|
-
if (key.toLowerCase() === iconKey) {
|
|
2123
|
-
this.resolvedName = key;
|
|
2124
|
-
break;
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
if (this.resolvedName)
|
|
2128
|
-
break;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
this.updateContent();
|
|
2132
|
-
}
|
|
2133
|
-
ngOnChanges() {
|
|
2134
|
-
this.updateContent();
|
|
2135
|
-
}
|
|
2136
|
-
updateContent() {
|
|
2137
|
-
const name = this.data.name ?? this.resolvedName;
|
|
2138
|
-
if (name) {
|
|
2139
|
-
const raw = MN_ICON_MAP[name];
|
|
2140
|
-
if (raw) {
|
|
2141
|
-
const size = this.iconSize;
|
|
2142
|
-
const isFullSvg = raw.trim().startsWith('<svg');
|
|
2143
|
-
if (isFullSvg) {
|
|
2144
|
-
const sized = raw.replace(/<svg([^>]*)>/, (_match, attrs) => {
|
|
2145
|
-
let updated = attrs.replace(/width="[^"]*"/, `width="${size}"`);
|
|
2146
|
-
updated = updated.replace(/height="[^"]*"/, `height="${size}"`);
|
|
2147
|
-
if (!attrs.includes('width='))
|
|
2148
|
-
updated += ` width="${size}"`;
|
|
2149
|
-
if (!attrs.includes('height='))
|
|
2150
|
-
updated += ` height="${size}"`;
|
|
2151
|
-
return `<svg${updated}>`;
|
|
2152
|
-
});
|
|
2153
|
-
this.svgContent = this.sanitizer.bypassSecurityTrustHtml(sized);
|
|
2154
|
-
}
|
|
2155
|
-
else {
|
|
2156
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${raw}</svg>`;
|
|
2157
|
-
this.svgContent = this.sanitizer.bypassSecurityTrustHtml(svg);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
else {
|
|
2161
|
-
this.svgContent = '';
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
else {
|
|
2165
|
-
this.svgContent = '';
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIcon, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2169
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnIcon, isStandalone: true, selector: "mn-icon", inputs: { data: "data" }, host: { properties: { "class": "this.hostClasses" } }, usesOnChanges: true, ngImport: i0, template: "<span\n [style.width.px]=\"iconSize\"\n [style.height.px]=\"iconSize\"\n style=\"display: inline-flex; align-items: center; justify-content: center;\"\n [innerHTML]=\"svgContent\"\n></span>\n" });
|
|
2170
|
-
}
|
|
2171
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIcon, decorators: [{
|
|
2172
|
-
type: Component,
|
|
2173
|
-
args: [{ selector: 'mn-icon', standalone: true, template: "<span\n [style.width.px]=\"iconSize\"\n [style.height.px]=\"iconSize\"\n style=\"display: inline-flex; align-items: center; justify-content: center;\"\n [innerHTML]=\"svgContent\"\n></span>\n" }]
|
|
2174
|
-
}], propDecorators: { data: [{
|
|
2175
|
-
type: Input
|
|
2176
|
-
}], hostClasses: [{
|
|
2177
|
-
type: HostBinding,
|
|
2178
|
-
args: ['class']
|
|
2179
|
-
}] } });
|
|
2180
|
-
|
|
2181
|
-
// AUTO-GENERATED — do not edit manually.
|
|
2182
|
-
// Run `npm run generate:icons` to regenerate after modifying MN_ICON_MAP.
|
|
2183
|
-
class MnIconAttributes {
|
|
2184
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIconAttributes, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2185
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnIconAttributes, isStandalone: true, selector: "mn-icon[mnIconPistol], mn-icon[mnIconPending], mn-icon[mnIconImagePlus], mn-icon[mnIconUpload], mn-icon[mnIconFile], mn-icon[mnIconTrash2], mn-icon[mnIconX]", ngImport: i0 });
|
|
2186
|
-
}
|
|
2187
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIconAttributes, decorators: [{
|
|
2188
|
-
type: Directive,
|
|
2189
|
-
args: [{
|
|
2190
|
-
selector: 'mn-icon[mnIconPistol], mn-icon[mnIconPending], mn-icon[mnIconImagePlus], mn-icon[mnIconUpload], mn-icon[mnIconFile], mn-icon[mnIconTrash2], mn-icon[mnIconX]',
|
|
2191
|
-
standalone: true,
|
|
2192
|
-
}]
|
|
2193
|
-
}] });
|
|
2194
|
-
|
|
2195
2073
|
/**
|
|
2196
2074
|
* MnFileInput Component
|
|
2197
2075
|
*
|
|
@@ -2585,11 +2463,11 @@ class MnFileInput {
|
|
|
2585
2463
|
return segment || 'image';
|
|
2586
2464
|
}
|
|
2587
2465
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFileInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2588
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFileInput, isStandalone: true, selector: "mn-lib-file-input", inputs: { props: "props" }, outputs: { filesChange: "filesChange", cleared: "cleared" }, ngImport: i0, template: "<div [class.w-full]=\"props.fullWidth !== false && displayMode !== 'compact'\" class=\"flex flex-col\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Hidden native file input shared by every trigger -->\n <input\n #fileInput\n (change)=\"onFileSelected($event)\"\n [accept]=\"acceptAttr\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.multiple]=\"props.multiple || null\"\n [attr.name]=\"resolvedName\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n class=\"hidden\"\n type=\"file\"\n />\n\n @switch (displayMode) {\n <!-- Compact: inline button + filename chips -->\n @case ('compact') {\n <div class=\"flex flex-wrap items-center gap-2\">\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n class=\"inline-flex items-center gap-2 cursor-pointer hover:bg-base-200\"\n type=\"button\">\n <
|
|
2466
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFileInput, isStandalone: true, selector: "mn-lib-file-input", inputs: { props: "props" }, outputs: { filesChange: "filesChange", cleared: "cleared" }, ngImport: i0, template: "<div [class.w-full]=\"props.fullWidth !== false && displayMode !== 'compact'\" class=\"flex flex-col\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Hidden native file input shared by every trigger -->\n <input\n #fileInput\n (change)=\"onFileSelected($event)\"\n [accept]=\"acceptAttr\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.multiple]=\"props.multiple || null\"\n [attr.name]=\"resolvedName\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n class=\"hidden\"\n type=\"file\"\n />\n\n @switch (displayMode) {\n <!-- Compact: inline button + filename chips -->\n @case ('compact') {\n <div class=\"flex flex-wrap items-center gap-2\">\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n class=\"inline-flex items-center gap-2 cursor-pointer hover:bg-base-200\"\n type=\"button\">\n <svg lucideUpload [size]=\"18\"></svg>\n <span class=\"text-sm\">{{ uiConfig.replaceLabel }}</span>\n </button>\n @for (item of displayItems(); track itemKey(item)) {\n <span class=\"inline-flex items-center gap-1 text-sm text-base-content/70\">\n @if (item.isImage && item.previewUrl) {\n <img [src]=\"item.previewUrl\" alt=\"\" class=\"h-6 w-6 rounded object-cover\"/>\n } @else {\n <svg lucideFile [size]=\"16\" class=\"text-base-content/50\"></svg>\n }\n <span class=\"max-w-40 truncate\">{{ item.name }}</span>\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"text-base-content/50 hover:text-error cursor-pointer\"\n type=\"button\">\n <svg lucideX [size]=\"14\"></svg>\n </button>\n </span>\n }\n </div>\n }\n\n <!-- List: compact rows of file icon + name + size -->\n @case ('list') {\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n class=\"flex flex-row items-center justify-center gap-2 cursor-pointer hover:bg-base-200\"\n type=\"button\">\n <svg lucideUpload [size]=\"18\"></svg>\n <span class=\"text-sm\">{{ uiConfig.replaceLabel }}</span>\n </button>\n @if (displayItems().length > 0) {\n <div class=\"mt-2 flex flex-col gap-1\">\n @for (item of displayItems(); track itemKey(item)) {\n <div class=\"flex items-center gap-2 rounded-lg bg-base-200 px-3 py-1.5 text-sm\">\n <svg lucideFile [size]=\"16\" class=\"text-base-content/50\"></svg>\n <span class=\"flex-1 truncate text-base-content\">{{ item.name }}</span>\n @if (item.sizeLabel) {\n <span class=\"text-base-content/50\">{{ item.sizeLabel }}</span>\n }\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"text-base-content/50 hover:text-error cursor-pointer\"\n type=\"button\">\n <svg lucideX [size]=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n }\n }\n\n <!-- Thumbnail: grid of tiles with overlay remove + add tile -->\n @case ('thumbnail') {\n <div class=\"flex flex-wrap gap-3\">\n @for (item of displayItems(); track itemKey(item)) {\n <div class=\"relative h-24 w-24 overflow-hidden rounded-xl border border-base-300 bg-base-100\">\n @if (item.isImage && item.previewUrl) {\n <img [src]=\"item.previewUrl\" alt=\"\" class=\"h-full w-full object-cover\"/>\n } @else {\n <div class=\"flex h-full w-full flex-col items-center justify-center gap-1 p-1 text-center\">\n <svg lucideFile [size]=\"22\" class=\"text-base-content/50\"></svg>\n <span class=\"w-full truncate text-xs text-base-content/60\">{{ item.name }}</span>\n </div>\n }\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"btn btn-circle btn-xs absolute top-1 right-1 border-none bg-black/60 text-white hover:bg-black/80\"\n type=\"button\">\n <svg lucideX [size]=\"14\"></svg>\n </button>\n </div>\n }\n @if (props.multiple || displayItems().length === 0) {\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n class=\"flex h-24 w-24 flex-col items-center justify-center gap-1 rounded-xl border-2 border-dashed border-base-300 text-base-content/50 hover:border-primary cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed\"\n type=\"button\">\n <svg lucideImagePlus [size]=\"22\"></svg>\n </button>\n }\n </div>\n }\n\n <!-- Dropzone (default): large dashed area + preview rows -->\n @default {\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n type=\"button\">\n <svg lucideImagePlus [size]=\"28\" class=\"text-base-content/40\"></svg>\n <span class=\"text-sm text-base-content/60\">{{ uiConfig.dropzoneHint }}</span>\n </button>\n @if (displayItems().length > 0) {\n <div class=\"mt-2 flex flex-col gap-2\">\n @for (item of displayItems(); track itemKey(item)) {\n <div class=\"flex items-center gap-3 rounded-lg bg-base-200 p-2\">\n @if (item.isImage && item.previewUrl) {\n <img [src]=\"item.previewUrl\" alt=\"\" class=\"h-12 w-12 rounded-md object-cover\"/>\n } @else {\n <svg lucideFile [size]=\"24\" class=\"text-base-content/50\"></svg>\n }\n <span class=\"flex-1 truncate text-sm text-base-content\">{{ item.name }}</span>\n @if (item.sizeLabel) {\n <span class=\"text-sm text-base-content/50\">{{ item.sizeLabel }}</span>\n }\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"btn btn-circle btn-sm btn-ghost text-base-content/60 hover:text-error\"\n type=\"button\">\n <svg lucideTrash2 [size]=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n }\n }\n }\n\n <!-- Selection-limit error (accept / maxSize / maxFiles) -->\n @if (internalError(); as msg) {\n <mn-error-message [errorMessage]=\"msg\" [id]=\"resolvedId + '-selection'\"></mn-error-message>\n }\n\n <!-- Control validation errors -->\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: LucideFile, selector: "svg[lucideFile]" }, { kind: "component", type: LucideImagePlus, selector: "svg[lucideImagePlus]" }, { kind: "component", type: LucideTrash2, selector: "svg[lucideTrash2]" }, { kind: "component", type: LucideUpload, selector: "svg[lucideUpload]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
|
|
2589
2467
|
}
|
|
2590
2468
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFileInput, decorators: [{
|
|
2591
2469
|
type: Component,
|
|
2592
|
-
args: [{ selector: 'mn-lib-file-input', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage,
|
|
2470
|
+
args: [{ selector: 'mn-lib-file-input', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage, LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX], template: "<div [class.w-full]=\"props.fullWidth !== false && displayMode !== 'compact'\" class=\"flex flex-col\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Hidden native file input shared by every trigger -->\n <input\n #fileInput\n (change)=\"onFileSelected($event)\"\n [accept]=\"acceptAttr\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.multiple]=\"props.multiple || null\"\n [attr.name]=\"resolvedName\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n class=\"hidden\"\n type=\"file\"\n />\n\n @switch (displayMode) {\n <!-- Compact: inline button + filename chips -->\n @case ('compact') {\n <div class=\"flex flex-wrap items-center gap-2\">\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n class=\"inline-flex items-center gap-2 cursor-pointer hover:bg-base-200\"\n type=\"button\">\n <svg lucideUpload [size]=\"18\"></svg>\n <span class=\"text-sm\">{{ uiConfig.replaceLabel }}</span>\n </button>\n @for (item of displayItems(); track itemKey(item)) {\n <span class=\"inline-flex items-center gap-1 text-sm text-base-content/70\">\n @if (item.isImage && item.previewUrl) {\n <img [src]=\"item.previewUrl\" alt=\"\" class=\"h-6 w-6 rounded object-cover\"/>\n } @else {\n <svg lucideFile [size]=\"16\" class=\"text-base-content/50\"></svg>\n }\n <span class=\"max-w-40 truncate\">{{ item.name }}</span>\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"text-base-content/50 hover:text-error cursor-pointer\"\n type=\"button\">\n <svg lucideX [size]=\"14\"></svg>\n </button>\n </span>\n }\n </div>\n }\n\n <!-- List: compact rows of file icon + name + size -->\n @case ('list') {\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n class=\"flex flex-row items-center justify-center gap-2 cursor-pointer hover:bg-base-200\"\n type=\"button\">\n <svg lucideUpload [size]=\"18\"></svg>\n <span class=\"text-sm\">{{ uiConfig.replaceLabel }}</span>\n </button>\n @if (displayItems().length > 0) {\n <div class=\"mt-2 flex flex-col gap-1\">\n @for (item of displayItems(); track itemKey(item)) {\n <div class=\"flex items-center gap-2 rounded-lg bg-base-200 px-3 py-1.5 text-sm\">\n <svg lucideFile [size]=\"16\" class=\"text-base-content/50\"></svg>\n <span class=\"flex-1 truncate text-base-content\">{{ item.name }}</span>\n @if (item.sizeLabel) {\n <span class=\"text-base-content/50\">{{ item.sizeLabel }}</span>\n }\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"text-base-content/50 hover:text-error cursor-pointer\"\n type=\"button\">\n <svg lucideX [size]=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n }\n }\n\n <!-- Thumbnail: grid of tiles with overlay remove + add tile -->\n @case ('thumbnail') {\n <div class=\"flex flex-wrap gap-3\">\n @for (item of displayItems(); track itemKey(item)) {\n <div class=\"relative h-24 w-24 overflow-hidden rounded-xl border border-base-300 bg-base-100\">\n @if (item.isImage && item.previewUrl) {\n <img [src]=\"item.previewUrl\" alt=\"\" class=\"h-full w-full object-cover\"/>\n } @else {\n <div class=\"flex h-full w-full flex-col items-center justify-center gap-1 p-1 text-center\">\n <svg lucideFile [size]=\"22\" class=\"text-base-content/50\"></svg>\n <span class=\"w-full truncate text-xs text-base-content/60\">{{ item.name }}</span>\n </div>\n }\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"btn btn-circle btn-xs absolute top-1 right-1 border-none bg-black/60 text-white hover:bg-black/80\"\n type=\"button\">\n <svg lucideX [size]=\"14\"></svg>\n </button>\n </div>\n }\n @if (props.multiple || displayItems().length === 0) {\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n class=\"flex h-24 w-24 flex-col items-center justify-center gap-1 rounded-xl border-2 border-dashed border-base-300 text-base-content/50 hover:border-primary cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed\"\n type=\"button\">\n <svg lucideImagePlus [size]=\"22\"></svg>\n </button>\n }\n </div>\n }\n\n <!-- Dropzone (default): large dashed area + preview rows -->\n @default {\n <button\n (click)=\"fileInput.click()\"\n [disabled]=\"isDisabled\"\n [ngClass]=\"controlClasses\"\n type=\"button\">\n <svg lucideImagePlus [size]=\"28\" class=\"text-base-content/40\"></svg>\n <span class=\"text-sm text-base-content/60\">{{ uiConfig.dropzoneHint }}</span>\n </button>\n @if (displayItems().length > 0) {\n <div class=\"mt-2 flex flex-col gap-2\">\n @for (item of displayItems(); track itemKey(item)) {\n <div class=\"flex items-center gap-3 rounded-lg bg-base-200 p-2\">\n @if (item.isImage && item.previewUrl) {\n <img [src]=\"item.previewUrl\" alt=\"\" class=\"h-12 w-12 rounded-md object-cover\"/>\n } @else {\n <svg lucideFile [size]=\"24\" class=\"text-base-content/50\"></svg>\n }\n <span class=\"flex-1 truncate text-sm text-base-content\">{{ item.name }}</span>\n @if (item.sizeLabel) {\n <span class=\"text-sm text-base-content/50\">{{ item.sizeLabel }}</span>\n }\n <button\n (click)=\"item.existing ? removeExisting(item.index) : removeFile(item.index)\"\n [attr.aria-label]=\"uiConfig.removeLabel\"\n class=\"btn btn-circle btn-sm btn-ghost text-base-content/60 hover:text-error\"\n type=\"button\">\n <svg lucideTrash2 [size]=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n }\n }\n }\n\n <!-- Selection-limit error (accept / maxSize / maxFiles) -->\n @if (internalError(); as msg) {\n <mn-error-message [errorMessage]=\"msg\" [id]=\"resolvedId + '-selection'\"></mn-error-message>\n }\n\n <!-- Control validation errors -->\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n</div>\n" }]
|
|
2593
2471
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
2594
2472
|
type: Input,
|
|
2595
2473
|
args: [{ required: true }]
|
|
@@ -3465,11 +3343,17 @@ class MnSelect {
|
|
|
3465
3343
|
return msgDef;
|
|
3466
3344
|
}
|
|
3467
3345
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3468
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, ngImport: i0, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <select\n (blur)=\"handleBlur()\"\n (change)=\"onSelectChange($event)\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n [name]=\"resolvedName\"\n [ngClass]=\"selectClasses\"\n >\n @if (props.placeholder || uiConfig.placeholder) {\n <option [selected]=\"selectedValue === null\" [value]=\"''\" disabled>\n {{ uiConfig.placeholder || props.placeholder }}\n </option>\n }\n @for (opt of props.options; track opt.value) {\n <option\n [disabled]=\"opt.disabled\"\n [selected]=\"isSelected(opt)\"\n [value]=\"optionIndex(opt)\"\n >{{ opt.label }}\n </option>\n }\n </select>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
3346
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, host: { properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, ngImport: i0, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <select\n (blur)=\"handleBlur()\"\n (change)=\"onSelectChange($event)\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n [name]=\"resolvedName\"\n [ngClass]=\"selectClasses\"\n >\n @if (props.placeholder || uiConfig.placeholder) {\n <option [selected]=\"selectedValue === null\" [value]=\"''\" disabled>\n {{ uiConfig.placeholder || props.placeholder }}\n </option>\n }\n @for (opt of props.options; track opt.value) {\n <option\n [disabled]=\"opt.disabled\"\n [selected]=\"isSelected(opt)\"\n [value]=\"optionIndex(opt)\"\n >{{ opt.label }}\n </option>\n }\n </select>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
3469
3347
|
}
|
|
3470
3348
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, decorators: [{
|
|
3471
3349
|
type: Component,
|
|
3472
|
-
args: [{ selector: 'mn-lib-select', standalone: true, imports: [NgClass, MnErrorMessage],
|
|
3350
|
+
args: [{ selector: 'mn-lib-select', standalone: true, imports: [NgClass, MnErrorMessage], host: {
|
|
3351
|
+
// Without an explicit host width the inline host collapses to its content size, so
|
|
3352
|
+
// the control's `w-full` (width:100%) resolves against a content-sized box and fails
|
|
3353
|
+
// to fill the parent. Give the host a real width when fullWidth is requested.
|
|
3354
|
+
'[style.display]': "props?.fullWidth ? 'block' : null",
|
|
3355
|
+
'[style.width]': "props?.fullWidth ? '100%' : null",
|
|
3356
|
+
}, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <select\n (blur)=\"handleBlur()\"\n (change)=\"onSelectChange($event)\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n [name]=\"resolvedName\"\n [ngClass]=\"selectClasses\"\n >\n @if (props.placeholder || uiConfig.placeholder) {\n <option [selected]=\"selectedValue === null\" [value]=\"''\" disabled>\n {{ uiConfig.placeholder || props.placeholder }}\n </option>\n }\n @for (opt of props.options; track opt.value) {\n <option\n [disabled]=\"opt.disabled\"\n [selected]=\"isSelected(opt)\"\n [value]=\"optionIndex(opt)\"\n >{{ opt.label }}\n </option>\n }\n </select>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
3473
3357
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
3474
3358
|
type: Input,
|
|
3475
3359
|
args: [{ required: true }]
|
|
@@ -5085,6 +4969,16 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5085
4969
|
currentSort = null;
|
|
5086
4970
|
/** Per-column filter values keyed by column key. */
|
|
5087
4971
|
columnFilters = {};
|
|
4972
|
+
/** Viewport width (px) below which the inline filter row collapses into a panel. */
|
|
4973
|
+
static FILTER_COLLAPSE_WIDTH = 640;
|
|
4974
|
+
/**
|
|
4975
|
+
* True when the viewport is narrow enough that the per-column filter inputs no
|
|
4976
|
+
* longer fit under their headers; the inline row is then replaced by a toggle
|
|
4977
|
+
* button and a stacked filter panel.
|
|
4978
|
+
*/
|
|
4979
|
+
filtersCollapsed = false;
|
|
4980
|
+
/** Whether the small-screen filter panel is currently expanded. */
|
|
4981
|
+
filtersPanelOpen = false;
|
|
5088
4982
|
componentName = 'MnTable';
|
|
5089
4983
|
get trackedToolbarTemplate() {
|
|
5090
4984
|
return this.dataSource?.toolbarLeftTemplate;
|
|
@@ -5110,6 +5004,51 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5110
5004
|
get hasColumnFilters() {
|
|
5111
5005
|
return this.dataSource.columns.some(c => c.filterable);
|
|
5112
5006
|
}
|
|
5007
|
+
/** Whether at least one column filter is active. */
|
|
5008
|
+
get hasActiveFilters() {
|
|
5009
|
+
return this.dataSource.columns.some(col => col.filterable && !!this.columnFilters[col.key]);
|
|
5010
|
+
}
|
|
5011
|
+
/** Label for the small-screen filters toggle button. */
|
|
5012
|
+
get filtersButtonLabel() {
|
|
5013
|
+
return this.dataSource.filtersLabel ?? 'Filters';
|
|
5014
|
+
}
|
|
5015
|
+
/** Label for the "clear all filters" action in the small-screen panel. */
|
|
5016
|
+
get clearFiltersButtonLabel() {
|
|
5017
|
+
return this.dataSource.clearFiltersLabel ?? 'Clear all';
|
|
5018
|
+
}
|
|
5019
|
+
/** Opens/closes the stacked filter panel shown on small screens. */
|
|
5020
|
+
toggleFiltersPanel() {
|
|
5021
|
+
this.filtersPanelOpen = !this.filtersPanelOpen;
|
|
5022
|
+
}
|
|
5023
|
+
/** Resets every column filter and re-applies filtering. */
|
|
5024
|
+
clearAllFilters() {
|
|
5025
|
+
for (const col of this.dataSource.columns) {
|
|
5026
|
+
if (col.filterable)
|
|
5027
|
+
this.columnFilters[col.key] = '';
|
|
5028
|
+
}
|
|
5029
|
+
this.currentPage = 1;
|
|
5030
|
+
this.applyFilter(false);
|
|
5031
|
+
this.cdr.markForCheck();
|
|
5032
|
+
}
|
|
5033
|
+
/** True when the viewport is below the filter-collapse breakpoint. */
|
|
5034
|
+
isFilterViewport() {
|
|
5035
|
+
return typeof window !== 'undefined' && window.innerWidth < MnTable.FILTER_COLLAPSE_WIDTH;
|
|
5036
|
+
}
|
|
5037
|
+
/**
|
|
5038
|
+
* Recomputes whether the inline filter row should collapse into the panel.
|
|
5039
|
+
* Closes the panel when returning to the wide layout so reopened state never
|
|
5040
|
+
* leaks across the breakpoint. Marks for check only when the layout flips.
|
|
5041
|
+
*/
|
|
5042
|
+
updateFilterLayout(reflow) {
|
|
5043
|
+
const collapsed = this.isFilterViewport();
|
|
5044
|
+
if (collapsed === this.filtersCollapsed)
|
|
5045
|
+
return;
|
|
5046
|
+
this.filtersCollapsed = collapsed;
|
|
5047
|
+
if (!collapsed)
|
|
5048
|
+
this.filtersPanelOpen = false;
|
|
5049
|
+
if (reflow)
|
|
5050
|
+
this.cdr.markForCheck();
|
|
5051
|
+
}
|
|
5113
5052
|
sort(column) {
|
|
5114
5053
|
if (!column.sortType || column.sortType === ColumnSortType.NONE)
|
|
5115
5054
|
return;
|
|
@@ -5182,9 +5121,10 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5182
5121
|
if (reflow)
|
|
5183
5122
|
this.cdr.markForCheck();
|
|
5184
5123
|
}
|
|
5185
|
-
/** Re-evaluate
|
|
5124
|
+
/** Re-evaluate responsive page size and filter layout when the viewport changes. */
|
|
5186
5125
|
onWindowResize() {
|
|
5187
5126
|
this.applyResponsivePageSize(true);
|
|
5127
|
+
this.updateFilterLayout(true);
|
|
5188
5128
|
}
|
|
5189
5129
|
/** Tracks the desktop page size when the user picks one (selector only shows at >= md). */
|
|
5190
5130
|
onPageSizeChange(newSize) {
|
|
@@ -5197,6 +5137,8 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5197
5137
|
// Force the mobile row count below `md`; use the consumer's pageSize (or 10) above it.
|
|
5198
5138
|
this.desktopPageSize = this.dataSource.pageSize ?? 10;
|
|
5199
5139
|
this.applyResponsivePageSize(false);
|
|
5140
|
+
// Seed the filter layout for the initial viewport (no markForCheck pre-render).
|
|
5141
|
+
this.updateFilterLayout(false);
|
|
5200
5142
|
this.currentSort = this.dataSource.defaultSort ?? null;
|
|
5201
5143
|
for (const col of this.dataSource.columns) {
|
|
5202
5144
|
if (col.filterable) {
|
|
@@ -5242,6 +5184,12 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5242
5184
|
col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
|
|
5243
5185
|
}
|
|
5244
5186
|
}
|
|
5187
|
+
if (this.dataSource.filtersLabelKey) {
|
|
5188
|
+
this.dataSource.filtersLabel = this.lang.t(this.dataSource.filtersLabelKey);
|
|
5189
|
+
}
|
|
5190
|
+
if (this.dataSource.clearFiltersLabelKey) {
|
|
5191
|
+
this.dataSource.clearFiltersLabel = this.lang.t(this.dataSource.clearFiltersLabelKey);
|
|
5192
|
+
}
|
|
5245
5193
|
}
|
|
5246
5194
|
// ── Filtering & sorting ──
|
|
5247
5195
|
applyFilter(searchForItems) {
|
|
@@ -5303,11 +5251,11 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5303
5251
|
});
|
|
5304
5252
|
}
|
|
5305
5253
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
5306
|
-
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()" } }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\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-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" 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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @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 -->\n @if (hasColumnFilters) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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]=\"column.width ?? null\"\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 {\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 <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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\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", 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: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5254
|
+
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()" } }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\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-[375px]:w-auto min-[375px]: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: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [class.border-primary]=\"hasActiveFilters\"\n [class.text-primary]=\"hasActiveFilters\"\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<!-- 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 @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\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\" 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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @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 @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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]=\"column.width ?? null\"\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 {\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 <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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\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", 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: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.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]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5307
5255
|
}
|
|
5308
5256
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
5309
5257
|
type: Component,
|
|
5310
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\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-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" 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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @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 -->\n @if (hasColumnFilters) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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]=\"column.width ?? null\"\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 {\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 <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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\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" }]
|
|
5258
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\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-[375px]:w-auto min-[375px]: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: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [class.border-primary]=\"hasActiveFilters\"\n [class.text-primary]=\"hasActiveFilters\"\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<!-- 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 @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\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\" 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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @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 @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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]=\"column.width ?? null\"\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 {\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 <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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\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" }]
|
|
5311
5259
|
}], propDecorators: { sortChange: [{
|
|
5312
5260
|
type: Output
|
|
5313
5261
|
}], rowClick: [{
|
|
@@ -5806,6 +5754,7 @@ class MnFormBodyComponent {
|
|
|
5806
5754
|
label: this.asField(field).label,
|
|
5807
5755
|
placeholder: this.isFieldLoading(field.key) ? this.labels.loading : this.labels.selectPlaceholder,
|
|
5808
5756
|
options: this.getSelectOptions(field),
|
|
5757
|
+
fullWidth: true,
|
|
5809
5758
|
};
|
|
5810
5759
|
}
|
|
5811
5760
|
async loadFieldOptions(key, dataSource, formValue) {
|
|
@@ -5967,14 +5916,6 @@ class MnFormBodyComponent {
|
|
|
5967
5916
|
});
|
|
5968
5917
|
this.previousFormValue = { ...formValue };
|
|
5969
5918
|
}
|
|
5970
|
-
getGridColumns(row) {
|
|
5971
|
-
const cols = row.columns || 1;
|
|
5972
|
-
return `repeat(${cols}, 1fr)`;
|
|
5973
|
-
}
|
|
5974
|
-
getGridSpan(rowField) {
|
|
5975
|
-
const span = rowField.span || 1;
|
|
5976
|
-
return span > 1 ? `span ${span}` : '';
|
|
5977
|
-
}
|
|
5978
5919
|
// =========================
|
|
5979
5920
|
// Feature: File Upload Fields
|
|
5980
5921
|
// =========================
|
|
@@ -6011,11 +5952,11 @@ class MnFormBodyComponent {
|
|
|
6011
5952
|
}
|
|
6012
5953
|
}
|
|
6013
5954
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6014
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div>\n <mn-lib-file-input\n (cleared)=\"onFileCleared(rowField.field)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n accept: asField(rowField.field).accept,\n multiple: asField(rowField.field).multiple,\n maxFiles: asField(rowField.field).maxFiles,\n maxSize: asField(rowField.field).maxSize,\n displayMode: asField(rowField.field).displayMode,\n dropzoneHint: asField(rowField.field).dropzoneHint,\n replaceLabel: asField(rowField.field).replaceLabel,\n removeLabel: asField(rowField.field).removeLabel,\n currentUrl: asField(rowField.field).currentUrl,\n currentUrls: asField(rowField.field).currentUrls,\n disabled: isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\n })\"\n ></mn-lib-file-input>\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnFileInput, selector: "mn-lib-file-input", inputs: ["props"], outputs: ["filesChange", "cleared"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", outputs: ["sortChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
5955
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div>\n <mn-lib-file-input\n (cleared)=\"onFileCleared(rowField.field)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n accept: asField(rowField.field).accept,\n multiple: asField(rowField.field).multiple,\n maxFiles: asField(rowField.field).maxFiles,\n maxSize: asField(rowField.field).maxSize,\n displayMode: asField(rowField.field).displayMode,\n dropzoneHint: asField(rowField.field).dropzoneHint,\n replaceLabel: asField(rowField.field).replaceLabel,\n removeLabel: asField(rowField.field).removeLabel,\n currentUrl: asField(rowField.field).currentUrl,\n currentUrls: asField(rowField.field).currentUrls,\n disabled: isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\n })\"\n ></mn-lib-file-input>\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div [style.--mn-form-cols]=\"row.columns || 1\" class=\"grid gap-4 mn-form-row\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div [style.--mn-form-span]=\"rowField.span || 1\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\"\n class=\"flex flex-col gap-2 mn-form-cell\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div [style.--mn-form-cols]=\"row.columns || 1\" class=\"grid gap-4 mn-form-row\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div [style.--mn-form-span]=\"rowField.span || 1\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\"\n class=\"flex flex-col gap-2 mn-form-cell\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}.mn-form-row{grid-template-columns:repeat(var(--mn-form-cols, 1),minmax(0,1fr))}.mn-form-cell{grid-column:span var(--mn-form-span, 1)}@media(max-width:639.98px){.mn-form-row{grid-template-columns:1fr}.mn-form-cell{grid-column:auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnFileInput, selector: "mn-lib-file-input", inputs: ["props"], outputs: ["filesChange", "cleared"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", outputs: ["sortChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
6015
5956
|
}
|
|
6016
5957
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
6017
5958
|
type: Component,
|
|
6018
|
-
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnFileInput, MnSelect, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div>\n <mn-lib-file-input\n (cleared)=\"onFileCleared(rowField.field)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n accept: asField(rowField.field).accept,\n multiple: asField(rowField.field).multiple,\n maxFiles: asField(rowField.field).maxFiles,\n maxSize: asField(rowField.field).maxSize,\n displayMode: asField(rowField.field).displayMode,\n dropzoneHint: asField(rowField.field).dropzoneHint,\n replaceLabel: asField(rowField.field).replaceLabel,\n removeLabel: asField(rowField.field).removeLabel,\n currentUrl: asField(rowField.field).currentUrl,\n currentUrls: asField(rowField.field).currentUrls,\n disabled: isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\n })\"\n ></mn-lib-file-input>\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"] }]
|
|
5959
|
+
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnFileInput, MnSelect, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field),\n fullWidth: true\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div>\n <mn-lib-file-input\n (cleared)=\"onFileCleared(rowField.field)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n accept: asField(rowField.field).accept,\n multiple: asField(rowField.field).multiple,\n maxFiles: asField(rowField.field).maxFiles,\n maxSize: asField(rowField.field).maxSize,\n displayMode: asField(rowField.field).displayMode,\n dropzoneHint: asField(rowField.field).dropzoneHint,\n replaceLabel: asField(rowField.field).replaceLabel,\n removeLabel: asField(rowField.field).removeLabel,\n currentUrl: asField(rowField.field).currentUrl,\n currentUrls: asField(rowField.field).currentUrls,\n disabled: isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\n })\"\n ></mn-lib-file-input>\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div [style.--mn-form-cols]=\"row.columns || 1\" class=\"grid gap-4 mn-form-row\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div [style.--mn-form-span]=\"rowField.span || 1\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\"\n class=\"flex flex-col gap-2 mn-form-cell\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div [style.--mn-form-cols]=\"row.columns || 1\" class=\"grid gap-4 mn-form-row\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div [style.--mn-form-span]=\"rowField.span || 1\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\"\n class=\"flex flex-col gap-2 mn-form-cell\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}.mn-form-row{grid-template-columns:repeat(var(--mn-form-cols, 1),minmax(0,1fr))}.mn-form-cell{grid-column:span var(--mn-form-span, 1)}@media(max-width:639.98px){.mn-form-row{grid-template-columns:1fr}.mn-form-cell{grid-column:auto}}\n"] }]
|
|
6019
5960
|
}], propDecorators: { config: [{
|
|
6020
5961
|
type: Input
|
|
6021
5962
|
}], modalRef: [{
|
|
@@ -8535,6 +8476,129 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
8535
8476
|
type: Output
|
|
8536
8477
|
}] } });
|
|
8537
8478
|
|
|
8479
|
+
const mnIconVariants = tv({
|
|
8480
|
+
base: 'inline-flex shrink-0',
|
|
8481
|
+
variants: {
|
|
8482
|
+
color: {
|
|
8483
|
+
current: 'text-current',
|
|
8484
|
+
primary: 'text-primary',
|
|
8485
|
+
secondary: 'text-neutral',
|
|
8486
|
+
danger: 'text-error',
|
|
8487
|
+
warning: 'text-warning',
|
|
8488
|
+
success: 'text-success',
|
|
8489
|
+
accent: 'text-accent',
|
|
8490
|
+
},
|
|
8491
|
+
},
|
|
8492
|
+
defaultVariants: {
|
|
8493
|
+
color: 'current',
|
|
8494
|
+
},
|
|
8495
|
+
});
|
|
8496
|
+
|
|
8497
|
+
// Custom SVGs that are NOT available in lucide (https://lucide.dev).
|
|
8498
|
+
// For any lucide icon, use the @lucide/angular directive directly instead —
|
|
8499
|
+
// e.g. `<svg lucideX [size]="18"></svg>` — rather than adding it here.
|
|
8500
|
+
// Sourced from the .svg files in the ./icons/ directory.
|
|
8501
|
+
const pistolSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.72 14.56 9 10h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v4c2.5 0 1 4 1 4-4 6-1 6-1 6h3.38a1 1 0 0 0 .89-.55za1 1 0 0 1 .9-.56H13a2 2 0 0 0 2-2v-1a1 1 0 0 1 1-1" /></svg>`;
|
|
8502
|
+
const pendingSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v5l4 2" /><path d="M14 20.775A9 9 0 0112 21" /><path d="M19 17.656a9 9 0 01-1.5 1.456" /><path d="M21 12a9 9 0 01-.228 2" /><path d="M21 8h-5" /><path d="M7.5 19.794c-6-3.464-6-12.124 0-15.588A9 9 0 0112 3a9.75 9.75 0 016.74 2.74L21 8V3" /></svg>`;
|
|
8503
|
+
const MN_ICON_MAP = {
|
|
8504
|
+
pistol: pistolSvg,
|
|
8505
|
+
pending: pendingSvg,
|
|
8506
|
+
};
|
|
8507
|
+
|
|
8508
|
+
class MnIcon {
|
|
8509
|
+
data = {};
|
|
8510
|
+
sanitizer = inject(DomSanitizer);
|
|
8511
|
+
el = inject(ElementRef);
|
|
8512
|
+
svgContent = '';
|
|
8513
|
+
get iconSize() {
|
|
8514
|
+
return this.data.size ?? 24;
|
|
8515
|
+
}
|
|
8516
|
+
get hostClasses() {
|
|
8517
|
+
return mnIconVariants({
|
|
8518
|
+
color: this.data.color,
|
|
8519
|
+
});
|
|
8520
|
+
}
|
|
8521
|
+
resolvedName = null;
|
|
8522
|
+
ngOnInit() {
|
|
8523
|
+
const attrs = this.el.nativeElement.attributes;
|
|
8524
|
+
for (const item of Array.from(attrs)) {
|
|
8525
|
+
const attr = item.name.toLowerCase();
|
|
8526
|
+
if (attr.startsWith('mnicon')) {
|
|
8527
|
+
const iconKey = attr.slice('mnicon'.length);
|
|
8528
|
+
for (const key of Object.keys(MN_ICON_MAP)) {
|
|
8529
|
+
if (key.toLowerCase() === iconKey) {
|
|
8530
|
+
this.resolvedName = key;
|
|
8531
|
+
break;
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
if (this.resolvedName)
|
|
8535
|
+
break;
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
this.updateContent();
|
|
8539
|
+
}
|
|
8540
|
+
ngOnChanges() {
|
|
8541
|
+
this.updateContent();
|
|
8542
|
+
}
|
|
8543
|
+
updateContent() {
|
|
8544
|
+
const name = this.data.name ?? this.resolvedName;
|
|
8545
|
+
if (name) {
|
|
8546
|
+
const raw = MN_ICON_MAP[name];
|
|
8547
|
+
if (raw) {
|
|
8548
|
+
const size = this.iconSize;
|
|
8549
|
+
const isFullSvg = raw.trim().startsWith('<svg');
|
|
8550
|
+
if (isFullSvg) {
|
|
8551
|
+
const sized = raw.replace(/<svg([^>]*)>/, (_match, attrs) => {
|
|
8552
|
+
let updated = attrs.replace(/width="[^"]*"/, `width="${size}"`);
|
|
8553
|
+
updated = updated.replace(/height="[^"]*"/, `height="${size}"`);
|
|
8554
|
+
if (!attrs.includes('width='))
|
|
8555
|
+
updated += ` width="${size}"`;
|
|
8556
|
+
if (!attrs.includes('height='))
|
|
8557
|
+
updated += ` height="${size}"`;
|
|
8558
|
+
return `<svg${updated}>`;
|
|
8559
|
+
});
|
|
8560
|
+
this.svgContent = this.sanitizer.bypassSecurityTrustHtml(sized);
|
|
8561
|
+
}
|
|
8562
|
+
else {
|
|
8563
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${raw}</svg>`;
|
|
8564
|
+
this.svgContent = this.sanitizer.bypassSecurityTrustHtml(svg);
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
8567
|
+
else {
|
|
8568
|
+
this.svgContent = '';
|
|
8569
|
+
}
|
|
8570
|
+
}
|
|
8571
|
+
else {
|
|
8572
|
+
this.svgContent = '';
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIcon, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8576
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnIcon, isStandalone: true, selector: "mn-icon", inputs: { data: "data" }, host: { properties: { "class": "this.hostClasses" } }, usesOnChanges: true, ngImport: i0, template: "<span\n [style.width.px]=\"iconSize\"\n [style.height.px]=\"iconSize\"\n style=\"display: inline-flex; align-items: center; justify-content: center;\"\n [innerHTML]=\"svgContent\"\n></span>\n" });
|
|
8577
|
+
}
|
|
8578
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIcon, decorators: [{
|
|
8579
|
+
type: Component,
|
|
8580
|
+
args: [{ selector: 'mn-icon', standalone: true, template: "<span\n [style.width.px]=\"iconSize\"\n [style.height.px]=\"iconSize\"\n style=\"display: inline-flex; align-items: center; justify-content: center;\"\n [innerHTML]=\"svgContent\"\n></span>\n" }]
|
|
8581
|
+
}], propDecorators: { data: [{
|
|
8582
|
+
type: Input
|
|
8583
|
+
}], hostClasses: [{
|
|
8584
|
+
type: HostBinding,
|
|
8585
|
+
args: ['class']
|
|
8586
|
+
}] } });
|
|
8587
|
+
|
|
8588
|
+
// AUTO-GENERATED — do not edit manually.
|
|
8589
|
+
// Run `npm run generate:icons` to regenerate after modifying MN_ICON_MAP.
|
|
8590
|
+
class MnIconAttributes {
|
|
8591
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIconAttributes, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
8592
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnIconAttributes, isStandalone: true, selector: "mn-icon[mnIconPistol], mn-icon[mnIconPending]", ngImport: i0 });
|
|
8593
|
+
}
|
|
8594
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnIconAttributes, decorators: [{
|
|
8595
|
+
type: Directive,
|
|
8596
|
+
args: [{
|
|
8597
|
+
selector: 'mn-icon[mnIconPistol], mn-icon[mnIconPending]',
|
|
8598
|
+
standalone: true,
|
|
8599
|
+
}]
|
|
8600
|
+
}] });
|
|
8601
|
+
|
|
8538
8602
|
/**
|
|
8539
8603
|
* Injection token for the base URL used by all CRUD service requests.
|
|
8540
8604
|
*
|