mn-angular-lib 1.0.157 → 1.0.159

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.
@@ -10,7 +10,7 @@ import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angu
10
10
  import * as i1$1 from '@angular/forms';
11
11
  import { NgControl, Validators, FormsModule, NG_VALUE_ACCESSOR, FormBuilder, ReactiveFormsModule } from '@angular/forms';
12
12
  import JSON5 from 'json5';
13
- import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideCalendarDays, LucideChevronLeft, LucideChevronRight, LucideChevronDown, LucideEllipsisVertical, LucideSearchX, LucideDynamicIcon, LucideArrowLeft, LucideArrowRight, LucideCheck, LucideInbox, LucideFilter, LucideFunnel, LucideTriangleAlert, LucideCircleAlert } from '@lucide/angular';
13
+ import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideCalendarDays, LucideChevronLeft, LucideChevronRight, LucideChevronDown, LucideCheck, LucideEllipsisVertical, LucideSearchX, LucideDynamicIcon, LucideArrowLeft, LucideArrowRight, LucideInbox, LucideFilter, LucideFunnel, LucideTriangleAlert, LucideCircleAlert } from '@lucide/angular';
14
14
  import { Router, ActivatedRoute } from '@angular/router';
15
15
  import { DomSanitizer } from '@angular/platform-browser';
16
16
 
@@ -4675,11 +4675,11 @@ class MnMultiSelect {
4675
4675
  });
4676
4676
  }
4677
4677
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
4678
- 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 needs a header to name the field and\n an explicit way out \u2014 tapping the trigger again is not reachable here. -->\n <div class=\"flex items-center justify-between gap-x-2 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 <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"uiConfig.closeLabel || 'Close'\"\n ><svg lucideX [size]=\"20\"></svg></button>\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]" }] });
4678
+ 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]" }] });
4679
4679
  }
4680
4680
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
4681
4681
  type: Component,
4682
- 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 needs a header to name the field and\n an explicit way out \u2014 tapping the trigger again is not reachable here. -->\n <div class=\"flex items-center justify-between gap-x-2 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 <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"uiConfig.closeLabel || 'Close'\"\n ><svg lucideX [size]=\"20\"></svg></button>\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" }]
4682
+ 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" }]
4683
4683
  }], ctorParameters: () => [], propDecorators: { props: [{
4684
4684
  type: Input,
4685
4685
  args: [{ required: true }]
@@ -4780,6 +4780,8 @@ const ACTION_COLOR_CLASS = {
4780
4780
  class MnDropdown {
4781
4781
  datasource;
4782
4782
  uiConfig = {};
4783
+ /** Lucide data for the trailing check shown on the {@link MnDropdownAction.active} row. */
4784
+ checkIcon = LucideCheck.icon;
4783
4785
  configService = inject(MnConfigService);
4784
4786
  sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
4785
4787
  explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
@@ -5241,10 +5243,6 @@ class MnDropdown {
5241
5243
  const translated = this.datasource.menuLabelKey ? this.lang.translateIfPresent(this.datasource.menuLabelKey) : undefined;
5242
5244
  return translated ?? this.datasource.menuLabel ?? this.uiConfig.menuLabel ?? null;
5243
5245
  }
5244
- /** Accessible label for the sheet's close button. */
5245
- get closeLabel() {
5246
- return this.uiConfig.closeLabel ?? 'Close';
5247
- }
5248
5246
  /** Placeholder shown in the search input, preferring a resolved translation key. */
5249
5247
  get searchPlaceholder() {
5250
5248
  const translated = this.datasource.searchPlaceholderKey ? this.lang.translateIfPresent(this.datasource.searchPlaceholderKey) : undefined;
@@ -5290,11 +5288,11 @@ class MnDropdown {
5290
5288
  return this.datasource.id ?? this.autoId;
5291
5289
  }
5292
5290
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
5293
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDropdown, isStandalone: true, selector: "mn-lib-dropdown", inputs: { datasource: "datasource" }, 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, read: ElementRef }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it needs a header and an explicit way out. -->\n <div class=\"flex items-center justify-between gap-x-2 px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n <button\n mnButton\n type=\"button\"\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeLabel\"\n ><svg lucideX [size]=\"20\"></svg></button>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\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 + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate\">{{ actionLabel(action) }}</span>\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? '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: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\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: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideSearchX, selector: "svg[lucideSearchX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }] });
5291
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDropdown, isStandalone: true, selector: "mn-lib-dropdown", inputs: { datasource: "datasource" }, 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, read: ElementRef }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it carries a header to name itself; 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-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\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 + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [attr.aria-current]=\"action.active ? 'true' : null\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', action.active ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate min-w-0\">{{ actionLabel(action) }}</span>\n <!-- The current choice's marker (e.g. the active language). Decorative: the\n state is conveyed to assistive tech by `aria-current` on the row. -->\n @if (action.active) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"sheet ? 18 : 16\"\n class=\"ml-auto shrink-0 text-primary\"\n aria-hidden=\"true\"\n ></svg>\n }\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? '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: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\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: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: LucideSearchX, selector: "svg[lucideSearchX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }] });
5294
5292
  }
5295
5293
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDropdown, decorators: [{
5296
5294
  type: Component,
5297
- args: [{ selector: 'mn-lib-dropdown', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnButton, MnBottomSheet, MnInputField, LucideX, LucideSearchX, LucideDynamicIcon], template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it needs a header and an explicit way out. -->\n <div class=\"flex items-center justify-between gap-x-2 px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n <button\n mnButton\n type=\"button\"\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeLabel\"\n ><svg lucideX [size]=\"20\"></svg></button>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\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 + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate\">{{ actionLabel(action) }}</span>\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? '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: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\n</div>\n" }]
5295
+ args: [{ selector: 'mn-lib-dropdown', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnButton, MnBottomSheet, MnInputField, LucideSearchX, LucideDynamicIcon], template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it carries a header to name itself; 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-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\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 + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [attr.aria-current]=\"action.active ? 'true' : null\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', action.active ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate min-w-0\">{{ actionLabel(action) }}</span>\n <!-- The current choice's marker (e.g. the active language). Decorative: the\n state is conveyed to assistive tech by `aria-current` on the row. -->\n @if (action.active) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"sheet ? 18 : 16\"\n class=\"ml-auto shrink-0 text-primary\"\n aria-hidden=\"true\"\n ></svg>\n }\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? '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: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\n</div>\n" }]
5298
5296
  }], propDecorators: { datasource: [{
5299
5297
  type: Input,
5300
5298
  args: [{ required: true }]
@@ -8425,10 +8423,10 @@ class MnTable extends MnSelectableCollectionBase {
8425
8423
  /**
8426
8424
  * The effective colour for an action, used identically by the inline button and the
8427
8425
  * collapsed ⋯-menu item so the two never diverge: an explicit `color`, else `'danger'`
8428
- * for a destructive action, else the default `'secondary'`.
8426
+ * for a destructive action, else the default `'primary'`.
8429
8427
  */
8430
8428
  rowActionColor(action, row) {
8431
- return this.resolveRowValue(action.color, row) ?? (action.danger ? 'danger' : 'secondary');
8429
+ return this.resolveRowValue(action.color, row) ?? (action.danger ? 'danger' : 'primary');
8432
8430
  }
8433
8431
  /** Invokes an action for a row. */
8434
8432
  runRowAction(action, row) {