mn-angular-lib 1.0.151 → 1.0.152

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.
@@ -1937,7 +1937,7 @@ class MnInputField {
1937
1937
  });
1938
1938
  }
1939
1939
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, deps: [], target: i0.ɵɵFactoryTarget.Component });
1940
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, host: { properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: 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"] }] });
1940
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, host: { properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n", styles: ["input::-webkit-search-cancel-button{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: 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"] }] });
1941
1941
  }
1942
1942
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, decorators: [{
1943
1943
  type: Component,
@@ -1950,7 +1950,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1950
1950
  // real width when fullWidth is requested so 100% has something to fill.
1951
1951
  '[style.display]': "props?.fullWidth ? 'block' : null",
1952
1952
  '[style.width]': "props?.fullWidth ? '100%' : null",
1953
- }, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n" }]
1953
+ }, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (search)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n\n }\n }\n }\n</div>\n", styles: ["input::-webkit-search-cancel-button{cursor:pointer}\n"] }]
1954
1954
  }], ctorParameters: () => [], propDecorators: { props: [{
1955
1955
  type: Input,
1956
1956
  args: [{ required: true }]
@@ -4641,6 +4641,14 @@ class MnDropdown {
4641
4641
  * or when the menu is not searchable, leaving the plain content-height popover untouched.
4642
4642
  */
4643
4643
  panelFloorPx = null;
4644
+ /**
4645
+ * The anchored popover's opened width, locked for the same reason as {@link panelFloorPx}:
4646
+ * the panel is content-sized between its `min-w`/`max-w` bounds, so a filtered list that
4647
+ * drops the widest item would otherwise shrink the popover mid-type. Captured on the same
4648
+ * next-frame pass as the height, so applying it is jump-free. Null while closed or when the
4649
+ * menu is not searchable, leaving the plain content-width popover untouched.
4650
+ */
4651
+ panelWidthPx = null;
4644
4652
  /**
4645
4653
  * The mobile sheet's opened height, applied as a `min-height` floor for the same reason
4646
4654
  * as {@link panelFloorPx} — mirroring mn-multi-select's sheet floor. Null while anchored,
@@ -4672,6 +4680,7 @@ class MnDropdown {
4672
4680
  }
4673
4681
  else if (!el) {
4674
4682
  this.panelFloorPx = null;
4683
+ this.panelWidthPx = null;
4675
4684
  }
4676
4685
  }
4677
4686
  /**
@@ -4755,6 +4764,7 @@ class MnDropdown {
4755
4764
  this.isOpen = false;
4756
4765
  this.searchTerm = '';
4757
4766
  this.panelFloorPx = null;
4767
+ this.panelWidthPx = null;
4758
4768
  this.sheetFloorPx = null;
4759
4769
  this.stopWatchingTrigger();
4760
4770
  this.unlockBodyScroll();
@@ -4898,15 +4908,17 @@ class MnDropdown {
4898
4908
  }
4899
4909
  // ── Height floors (searchable only) ──
4900
4910
  /**
4901
- * Records the anchored popover's opened height and locks it via {@link panelFloorPx}.
4902
- * Measured on the next frame so the read reflects the fully-rendered, unfiltered list
4903
- * (the search box is empty on open) and never forces a reflow mid change-detection. The
4904
- * value equals the current height, so applying it is jump-free it only stops a later,
4905
- * shorter filtered list from shrinking the panel.
4911
+ * Records the anchored popover's opened height and width, locking them via
4912
+ * {@link panelFloorPx} / {@link panelWidthPx}. Measured on the next frame so the read
4913
+ * reflects the fully-rendered, unfiltered list (the search box is empty on open) and never
4914
+ * forces a reflow mid change-detection. Both values equal the current dimensions, so
4915
+ * applying them is jump-free — they only stop a later, shorter/narrower filtered list from
4916
+ * shrinking the panel.
4906
4917
  */
4907
4918
  capturePanelFloor(panelEl) {
4908
4919
  if (typeof requestAnimationFrame !== 'function') {
4909
4920
  this.panelFloorPx = panelEl.offsetHeight;
4921
+ this.panelWidthPx = panelEl.offsetWidth;
4910
4922
  return;
4911
4923
  }
4912
4924
  requestAnimationFrame(() => {
@@ -4914,6 +4926,7 @@ class MnDropdown {
4914
4926
  if (!this.isOpen || this.movedPanel !== panelEl)
4915
4927
  return;
4916
4928
  this.panelFloorPx = panelEl.offsetHeight;
4929
+ this.panelWidthPx = panelEl.offsetWidth;
4917
4930
  this.cdr.markForCheck();
4918
4931
  });
4919
4932
  }
@@ -4972,7 +4985,7 @@ class MnDropdown {
4972
4985
  }
4973
4986
  /** Whether a list entry is a {@link MnDropdownSeparator} rather than a command. */
4974
4987
  isSeparator(item) {
4975
- return item.separator === true;
4988
+ return item.separator;
4976
4989
  }
4977
4990
  /**
4978
4991
  * Narrows a list entry to a command, or null for a separator. Used as `@if (asAction(item);
@@ -5093,11 +5106,11 @@ class MnDropdown {
5093
5106
  return this.datasource.id ?? this.autoId;
5094
5107
  }
5095
5108
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
5096
- 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 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"] }] });
5109
+ 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"] }] });
5097
5110
  }
5098
5111
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDropdown, decorators: [{
5099
5112
  type: Component,
5100
- 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 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" }]
5113
+ 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" }]
5101
5114
  }], propDecorators: { datasource: [{
5102
5115
  type: Input,
5103
5116
  args: [{ required: true }]