mn-angular-lib 1.0.162 → 1.0.163

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.
@@ -4753,9 +4753,47 @@ class MnMultiSelect {
4753
4753
  */
4754
4754
  get collapseSummaryText() {
4755
4755
  const allSelectedTemplate = this.allSelected ? this.props.allSelectedPlaceholder : undefined;
4756
- const template = allSelectedTemplate ?? this.props.collapsePlaceholder ?? '{count} selected';
4756
+ const template = allSelectedTemplate ??
4757
+ this.props.collapsePlaceholder ??
4758
+ this.resolveLabel(undefined, 'mnMultiSelect.selectedCount', '{count} selected');
4757
4759
  return template.replace(/\{count}/g, String(this.selectedOptions.length));
4758
4760
  }
4761
+ /** Trigger text shown while nothing is selected. */
4762
+ get placeholderLabel() {
4763
+ return this.resolveLabel(this.props.placeholder, 'mnMultiSelect.placeholder', 'Select...', this.uiConfig.placeholder);
4764
+ }
4765
+ /**
4766
+ * Placeholder and accessible name of the dropdown's search input.
4767
+ *
4768
+ * Search auto-enables at `searchThreshold` options, so this box appears without any
4769
+ * call site opting in — which is exactly why it must be translatable without one.
4770
+ */
4771
+ get searchPlaceholderLabel() {
4772
+ return this.resolveLabel(this.props.searchPlaceholder, 'mnMultiSelect.search', 'Search...', this.uiConfig.searchPlaceholder);
4773
+ }
4774
+ /** Empty text shown when the search filters every option away. */
4775
+ get noOptionsLabel() {
4776
+ return this.resolveLabel(undefined, 'mnMultiSelect.noOptions', 'No options found', this.uiConfig.noOptionsFound);
4777
+ }
4778
+ /**
4779
+ * Resolves one of the component's own labels, preferring what the caller gave it
4780
+ * and falling back through the config layer, a conventional translation key and
4781
+ * finally a readable English default.
4782
+ *
4783
+ * Mirrors `MnCollectionBase.resolveLabel`. Every string this component puts on
4784
+ * screen that is not caller data goes through here: without the key step a
4785
+ * consumer could only translate these by repeating the same literal at every call
4786
+ * site, which is how "Search..." ends up in English on an otherwise Dutch page.
4787
+ *
4788
+ * @param explicit The label the caller passed through `props`, if any.
4789
+ * @param key The conventional translation key to try next.
4790
+ * @param fallback The English text used when neither resolves.
4791
+ * @param configured The value the config layer resolved, if any.
4792
+ * @returns The resolved label.
4793
+ */
4794
+ resolveLabel(explicit, key, fallback, configured) {
4795
+ return explicit ?? configured ?? this.lang.translateIfPresent(key) ?? fallback;
4796
+ }
4759
4797
  handleBlur() {
4760
4798
  this.onTouched();
4761
4799
  }
@@ -4828,11 +4866,11 @@ class MnMultiSelect {
4828
4866
  });
4829
4867
  }
4830
4868
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
4831
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </ng-template>\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", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { 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: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
4869
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ placeholderLabel }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\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", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { 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: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
4832
4870
  }
4833
4871
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
4834
4872
  type: Component,
4835
- args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnErrorMessage, MnButton, MnInputField, MnBottomSheet, LucideX, LucideChevronDown], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </ng-template>\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" }]
4873
+ args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnErrorMessage, MnButton, MnInputField, MnBottomSheet, LucideX, LucideChevronDown], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ placeholderLabel }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\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" }]
4836
4874
  }], ctorParameters: () => [], propDecorators: { props: [{
4837
4875
  type: Input,
4838
4876
  args: [{ required: true }]
@@ -5811,8 +5849,39 @@ class MnSelect {
5811
5849
  }
5812
5850
  /** The label shown in the trigger: the selected option, else the placeholder. */
5813
5851
  get displayText() {
5814
- return this.selectedOption?.label
5815
- ?? this.uiConfig.placeholder ?? this.props.placeholder ?? 'Select...';
5852
+ return this.selectedOption?.label ?? this.placeholderLabel;
5853
+ }
5854
+ /** Trigger text shown while no option is selected. */
5855
+ get placeholderLabel() {
5856
+ return this.resolveLabel(this.props.placeholder, 'mnSelect.placeholder', 'Select...', this.uiConfig.placeholder);
5857
+ }
5858
+ /** Placeholder and accessible name of the dropdown's search input. */
5859
+ get searchPlaceholderLabel() {
5860
+ return this.resolveLabel(this.props.searchPlaceholder, 'mnSelect.search', 'Search...', this.uiConfig.searchPlaceholder);
5861
+ }
5862
+ /** Empty text shown when the search filters every option away. */
5863
+ get noOptionsLabel() {
5864
+ return this.resolveLabel(undefined, 'mnSelect.noOptions', 'No options found', this.uiConfig.noOptionsFound);
5865
+ }
5866
+ /**
5867
+ * Resolves one of the component's own labels, preferring what the caller gave it
5868
+ * and falling back through the config layer, a conventional translation key and
5869
+ * finally a readable English default.
5870
+ *
5871
+ * Mirrors `MnCollectionBase.resolveLabel` and its twin in `MnMultiSelect`. Without
5872
+ * the key step a consumer could only translate these by repeating the same literal
5873
+ * at every call site, and the search box in particular auto-enables on option
5874
+ * count — it appears without anyone asking for it, so it must be translatable
5875
+ * without anyone asking either.
5876
+ *
5877
+ * @param explicit The label the caller passed through `props`, if any.
5878
+ * @param key The conventional translation key to try next.
5879
+ * @param fallback The English text used when neither resolves.
5880
+ * @param configured The value the config layer resolved, if any.
5881
+ * @returns The resolved label.
5882
+ */
5883
+ resolveLabel(explicit, key, fallback, configured) {
5884
+ return explicit ?? configured ?? this.lang.translateIfPresent(key) ?? fallback;
5816
5885
  }
5817
5886
  get showError() {
5818
5887
  const c = this.control;
@@ -6186,7 +6255,7 @@ class MnSelect {
6186
6255
  return msgDef;
6187
6256
  }
6188
6257
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
6189
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" }, properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], 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 <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </ng-template>\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: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
6258
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" }, properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], 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 <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\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: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
6190
6259
  }
6191
6260
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, decorators: [{
6192
6261
  type: Component,
@@ -6196,7 +6265,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6196
6265
  // to fill the parent. Give the host a real width when fullWidth is requested.
6197
6266
  '[style.display]': "props?.fullWidth ? 'block' : null",
6198
6267
  '[style.width]': "props?.fullWidth ? '100%' : null",
6199
- }, 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 <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </ng-template>\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" }]
6268
+ }, 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 <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\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" }]
6200
6269
  }], ctorParameters: () => [], propDecorators: { props: [{
6201
6270
  type: Input,
6202
6271
  args: [{ required: true }]
@@ -11191,12 +11260,16 @@ class MnList extends MnSelectableCollectionBase {
11191
11260
  get listRegionLabel() {
11192
11261
  return this.resolveLabel(undefined, 'mnCollection.dataList', 'Data list');
11193
11262
  }
11263
+ /** Label on the header checkbox that selects or clears every visible row. */
11264
+ get selectAllLabel() {
11265
+ return this.resolveLabel(undefined, 'mnCollection.selectAll', 'Select all');
11266
+ }
11194
11267
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: null, target: i0.ɵɵFactoryTarget.Component });
11195
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: selectAllLabel, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11196
11269
  }
11197
11270
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
11198
11271
  type: Component,
11199
- args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n" }]
11272
+ args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: selectAllLabel, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n" }]
11200
11273
  }], propDecorators: { itemClick: [{
11201
11274
  type: Output
11202
11275
  }], collectionBody: [{
@@ -13680,19 +13753,44 @@ class MnBreadcrumbs {
13680
13753
  crumbClick = new EventEmitter();
13681
13754
  /** Emits when the fallback "Back" control is activated. */
13682
13755
  back = new EventEmitter();
13683
- /** Default translation key / literal for the Back control's label. */
13684
- static DEFAULT_BACK_LABEL = 'back';
13756
+ /** Conventional key an app defines to translate the Back control's label. */
13757
+ static BACK_LABEL_KEY = 'mnBreadcrumbs.back';
13758
+ /** Conventional key an app defines to name the navigation landmark. */
13759
+ static NAV_LABEL_KEY = 'mnBreadcrumbs.label';
13760
+ /** Resolves this component's own labels against the app's bundle. */
13761
+ lang = inject(MnLanguageService);
13685
13762
  /** Resolved tailwind-variants slot functions for the current size. */
13686
13763
  get styles() {
13687
13764
  return mnBreadcrumbsVariants({ size: this.data.size });
13688
13765
  }
13766
+ /**
13767
+ * Accessible name of the `<nav>` landmark.
13768
+ *
13769
+ * A landmark's name is announced verbatim, so leaving it as a hardcoded English
13770
+ * "Breadcrumb" put one English word into every page of a translated app — in the
13771
+ * one place only screen-reader users hear.
13772
+ */
13773
+ get navLabel() {
13774
+ return this.lang.translateIfPresent(MnBreadcrumbs.NAV_LABEL_KEY) ?? 'Breadcrumb';
13775
+ }
13689
13776
  /** Whether a linkable trail should render (vs the Back fallback). */
13690
13777
  get hasTrail() {
13691
13778
  return (this.data.items?.length ?? 0) > 0;
13692
13779
  }
13693
- /** Label for the Back control — the configured key/literal, or the default. */
13780
+ /**
13781
+ * Text of the Back control, already translated.
13782
+ *
13783
+ * `data.backLabel` is a key (or a literal, which `translate` passes through
13784
+ * unchanged). Without one this falls back to the conventional key and then to
13785
+ * English — never to a raw key: the old default was the bare key `'back'`, which
13786
+ * the template's translate pipe echoed as lowercase "back" in every app that had
13787
+ * not happened to define it.
13788
+ */
13694
13789
  get backLabel() {
13695
- return this.data.backLabel ?? MnBreadcrumbs.DEFAULT_BACK_LABEL;
13790
+ if (this.data.backLabel) {
13791
+ return this.lang.translate(this.data.backLabel);
13792
+ }
13793
+ return this.lang.translateIfPresent(MnBreadcrumbs.BACK_LABEL_KEY) ?? 'Back';
13696
13794
  }
13697
13795
  /** The last crumb is the current page and is rendered as plain text. */
13698
13796
  isCurrent(index) {
@@ -13714,11 +13812,11 @@ class MnBreadcrumbs {
13714
13812
  }
13715
13813
  }
13716
13814
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnBreadcrumbs, deps: [], target: i0.ɵɵFactoryTarget.Component });
13717
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnBreadcrumbs, isStandalone: true, selector: "mn-breadcrumbs", inputs: { data: "data" }, outputs: { crumbClick: "crumbClick", back: "back" }, ngImport: i0, template: "<nav aria-label=\"Breadcrumb\" [class]=\"styles.root()\">\n @if (hasTrail) {\n <ol [class]=\"styles.list()\">\n @for (item of data.items; track $index; let first = $first) {\n @if (!first) {\n <li aria-hidden=\"true\" [class]=\"styles.separator()\">\n <svg [size]=\"14\" lucideChevronRight></svg>\n </li>\n }\n <li class=\"inline-flex items-center\">\n @if (isCurrent($index)) {\n <span aria-current=\"page\" [class]=\"styles.current()\">{{ item.label | mnTranslate }}</span>\n } @else if (item.href) {\n <a [attr.href]=\"item.href\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</a>\n } @else {\n <button type=\"button\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</button>\n }\n </li>\n }\n </ol>\n } @else if (data.backHref) {\n <a [attr.href]=\"data.backHref\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel | mnTranslate }}\n </a>\n } @else {\n <button type=\"button\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel | mnTranslate }}\n </button>\n }\n</nav>\n", dependencies: [{ kind: "component", type: LucideChevronLeft, selector: "svg[lucideChevronLeft]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
13815
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnBreadcrumbs, isStandalone: true, selector: "mn-breadcrumbs", inputs: { data: "data" }, outputs: { crumbClick: "crumbClick", back: "back" }, ngImport: i0, template: "<nav [attr.aria-label]=\"navLabel\" [class]=\"styles.root()\">\n @if (hasTrail) {\n <ol [class]=\"styles.list()\">\n @for (item of data.items; track $index; let first = $first) {\n @if (!first) {\n <li aria-hidden=\"true\" [class]=\"styles.separator()\">\n <svg [size]=\"14\" lucideChevronRight></svg>\n </li>\n }\n <li class=\"inline-flex items-center\">\n @if (isCurrent($index)) {\n <span aria-current=\"page\" [class]=\"styles.current()\">{{ item.label | mnTranslate }}</span>\n } @else if (item.href) {\n <a [attr.href]=\"item.href\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</a>\n } @else {\n <button type=\"button\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</button>\n }\n </li>\n }\n </ol>\n } @else if (data.backHref) {\n <a [attr.href]=\"data.backHref\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel }}\n </a>\n } @else {\n <button type=\"button\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel }}\n </button>\n }\n</nav>\n", dependencies: [{ kind: "component", type: LucideChevronLeft, selector: "svg[lucideChevronLeft]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
13718
13816
  }
13719
13817
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnBreadcrumbs, decorators: [{
13720
13818
  type: Component,
13721
- args: [{ selector: 'mn-breadcrumbs', standalone: true, imports: [MnTranslatePipe, LucideChevronLeft, LucideChevronRight], template: "<nav aria-label=\"Breadcrumb\" [class]=\"styles.root()\">\n @if (hasTrail) {\n <ol [class]=\"styles.list()\">\n @for (item of data.items; track $index; let first = $first) {\n @if (!first) {\n <li aria-hidden=\"true\" [class]=\"styles.separator()\">\n <svg [size]=\"14\" lucideChevronRight></svg>\n </li>\n }\n <li class=\"inline-flex items-center\">\n @if (isCurrent($index)) {\n <span aria-current=\"page\" [class]=\"styles.current()\">{{ item.label | mnTranslate }}</span>\n } @else if (item.href) {\n <a [attr.href]=\"item.href\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</a>\n } @else {\n <button type=\"button\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</button>\n }\n </li>\n }\n </ol>\n } @else if (data.backHref) {\n <a [attr.href]=\"data.backHref\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel | mnTranslate }}\n </a>\n } @else {\n <button type=\"button\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel | mnTranslate }}\n </button>\n }\n</nav>\n" }]
13819
+ args: [{ selector: 'mn-breadcrumbs', standalone: true, imports: [MnTranslatePipe, LucideChevronLeft, LucideChevronRight], template: "<nav [attr.aria-label]=\"navLabel\" [class]=\"styles.root()\">\n @if (hasTrail) {\n <ol [class]=\"styles.list()\">\n @for (item of data.items; track $index; let first = $first) {\n @if (!first) {\n <li aria-hidden=\"true\" [class]=\"styles.separator()\">\n <svg [size]=\"14\" lucideChevronRight></svg>\n </li>\n }\n <li class=\"inline-flex items-center\">\n @if (isCurrent($index)) {\n <span aria-current=\"page\" [class]=\"styles.current()\">{{ item.label | mnTranslate }}</span>\n } @else if (item.href) {\n <a [attr.href]=\"item.href\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</a>\n } @else {\n <button type=\"button\" [class]=\"styles.link()\" (click)=\"onCrumb(item)\">{{ item.label | mnTranslate }}</button>\n }\n </li>\n }\n </ol>\n } @else if (data.backHref) {\n <a [attr.href]=\"data.backHref\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel }}\n </a>\n } @else {\n <button type=\"button\" [class]=\"styles.back()\" (click)=\"onBack()\">\n <svg [size]=\"16\" lucideChevronLeft></svg>\n {{ backLabel }}\n </button>\n }\n</nav>\n" }]
13722
13820
  }], propDecorators: { data: [{
13723
13821
  type: Input
13724
13822
  }], crumbClick: [{