mn-angular-lib 1.0.160 → 1.0.162

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mn-angular-lib",
3
- "version": "1.0.160",
3
+ "version": "1.0.162",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.1.3",
6
6
  "@angular/core": "^21.1.3",
@@ -3909,6 +3909,28 @@ type MnSelectProps<TValue = unknown> = {
3909
3909
  placeholder?: string;
3910
3910
  /** Available options to select from */
3911
3911
  options: MnSelectOption<TValue>[];
3912
+ /**
3913
+ * Whether to show a search/filter input at the top of the panel. When omitted, search
3914
+ * auto-enables once the number of options reaches `searchThreshold`, so long lists stay
3915
+ * filterable without every call site having to opt in. Set explicitly to force it on or off.
3916
+ */
3917
+ searchable?: boolean;
3918
+ /**
3919
+ * Number of options at which the search input auto-enables (default: 8).
3920
+ * Ignored when `searchable` is set explicitly.
3921
+ */
3922
+ searchThreshold?: number;
3923
+ /** Placeholder text for the search input */
3924
+ searchPlaceholder?: string;
3925
+ /**
3926
+ * Whether the option panel renders as a bottom sheet on small screens (< 640px).
3927
+ * Defaults to true. Set to false to keep the trigger-anchored panel on mobile.
3928
+ *
3929
+ * The anchored panel sits at the trigger's bottom edge, which puts it directly in
3930
+ * the path of the soft keyboard as soon as the search input takes focus. The sheet
3931
+ * is anchored to the viewport instead, so the list stays reachable.
3932
+ */
3933
+ mobileSheet?: boolean;
3912
3934
  /** Size variant of the select (default: 'md') */
3913
3935
  size?: MnSelectVariants['size'];
3914
3936
  /** Border radius variant (default: 'md') */
@@ -3940,45 +3962,222 @@ type MnSelectUIConfig = {
3940
3962
  * These override built-in error messages but are overridden by props.errorMessages.
3941
3963
  */
3942
3964
  errorMessages?: Record<string, string>;
3965
+ /** Text shown when no options match the search filter */
3966
+ noOptionsFound?: string;
3943
3967
  };
3944
3968
 
3945
3969
  declare const MN_SELECT_CONFIG: InjectionToken<MnSelectUIConfig>;
3970
+ /**
3971
+ * A single-value picker. The trigger opens a `role="listbox"` of {@link MnSelectOption}s;
3972
+ * choosing one sets the value and closes — this is the value-picker twin of the ⋯
3973
+ * command menu mn-dropdown, so it *is* a ControlValueAccessor.
3974
+ *
3975
+ * Presentation mirrors mn-multi-select: one custom field trigger at every size, an
3976
+ * anchored popover on desktop and the shared {@link MnBottomSheet} on mobile (< 640px) —
3977
+ * the same sheet mn-dropdown itself wraps. Both the popover and the sheet host are
3978
+ * portalled to `document.body` so their `position: fixed` anchors to the viewport rather
3979
+ * than any transformed/filtered ancestor (a table cell, a card) — the same root-cause fix
3980
+ * the multi-select applies.
3981
+ */
3946
3982
  declare class MnSelect implements OnInit {
3947
3983
  ngControl: NgControl | null;
3948
3984
  props: MnSelectProps;
3949
3985
  /** Currently selected value */
3950
3986
  selectedValue: unknown;
3987
+ isOpen: boolean;
3951
3988
  isDisabled: boolean;
3989
+ searchTerm: string;
3952
3990
  protected uiConfig: MnSelectUIConfig;
3953
3991
  private readonly configService;
3954
3992
  private readonly sectionPath;
3955
3993
  private readonly explicitInstanceId;
3994
+ private readonly elRef;
3956
3995
  private readonly lang;
3957
3996
  private readonly destroyRef;
3997
+ private readonly renderer;
3998
+ private readonly cdr;
3999
+ /** Lucide data for the trailing check shown on the selected row. */
4000
+ protected readonly checkIcon: mn_angular_lib.LucideIconData;
4001
+ /** Reference to the trigger element for positioning the dropdown. */
4002
+ triggerRef: ElementRef<HTMLElement>;
4003
+ /** Layout classes for the anchored popover panel. The mobile sheet is rendered by
4004
+ * mn-bottom-sheet instead, so it no longer needs a branch here. */
4005
+ readonly panelClasses = "fixed z-9999 bg-base-100 border border-base-300 rounded-md shadow-lg max-h-60 overflow-auto";
4006
+ /** Layout classes for the invisible click shield rendered under the anchored panel.
4007
+ * One step below the panel's z-index so the panel itself stays clickable, and above
4008
+ * any modal/drawer chrome (which tops out well under 9998). */
4009
+ readonly shieldClasses = "fixed inset-0 z-9998";
4010
+ /** Option count at which the search input auto-enables when `searchable` is unset. */
4011
+ private static readonly DEFAULT_SEARCH_THRESHOLD;
4012
+ /** Tailwind's `sm` breakpoint — below this the panel renders as a bottom sheet.
4013
+ * Kept in step with the same constant in mn-bottom-sheet / mn-multi-select. */
4014
+ private static readonly SHEET_MAX_WIDTH;
4015
+ /** The anchored popover panel currently moved into `document.body`, if any. */
4016
+ private movedPanel;
4017
+ /** The click shield currently moved into `document.body`, if any. */
4018
+ private movedShield;
4019
+ /** The bottom-sheet host, for outside-click tests. The sheet owns its own placement. */
4020
+ private sheetHost;
4021
+ /** Whether the viewport is currently narrow enough for the sheet layout. */
4022
+ private isNarrowViewport;
4023
+ /** Live breakpoint match, so rotating the device re-evaluates the layout. */
4024
+ private sheetMedia;
4025
+ /** The listener registered on `sheetMedia`, retained for teardown. */
4026
+ private sheetMediaListener;
4027
+ /** `document.body`'s inline `overflow` before the sheet locked it, restored on close. */
4028
+ private previousBodyOverflow;
4029
+ /**
4030
+ * The sheet's height (px) captured the moment it opened, before any search. Re-applied
4031
+ * as a `min-height` floor so filtering the option list shorter cannot shrink the sheet
4032
+ * mid-type. Null while anchored or closed, so the popover and desktop path are untouched.
4033
+ */
4034
+ sheetFloorPx: number | null;
4035
+ /**
4036
+ * Watches the trigger while the panel is open. The panel lives in `document.body`, so it
4037
+ * survives its own trigger being hidden by an ancestor — a wizard step or a tab switched
4038
+ * away with `display: none`. When the trigger stops being visible the panel goes with it.
4039
+ */
4040
+ private visibilityObserver;
4041
+ /**
4042
+ * Capture-phase scroll listener installed while open. `window:scroll` only fires for the
4043
+ * document scroller, so scrolling an inner container (a modal body, a scrollable card)
4044
+ * would otherwise leave the portalled panel floating at its stale coordinates.
4045
+ */
4046
+ private scrollCapture;
4047
+ /** Dropdown position calculated from the trigger's bounding rect. */
4048
+ dropdownStyle: {
4049
+ top: string;
4050
+ left: string;
4051
+ width: string;
4052
+ };
4053
+ private onChange;
4054
+ private onTouched;
3958
4055
  private readonly builtInErrorMessages;
3959
4056
  constructor();
3960
- get selectedOption(): MnSelectOption | undefined;
4057
+ /**
4058
+ * The dropdown panel element, queried while it is rendered by the `@if` block. The setter
4059
+ * relocates the panel to `document.body` so that its `position: fixed` coordinates resolve
4060
+ * against the viewport rather than any transformed/filtered ancestor (which would otherwise
4061
+ * become the containing block and push the panel to the middle of the screen — also broken
4062
+ * on iOS). Cleanup is handled when the query clears on close/destroy.
4063
+ */
4064
+ set dropdownRef(ref: ElementRef<HTMLElement> | undefined);
4065
+ /**
4066
+ * The click shield sitting under the anchored panel, portalled alongside it for the same
4067
+ * reason: `position: fixed` must resolve against the viewport, not a transformed ancestor.
4068
+ */
4069
+ set shieldRef(ref: ElementRef<HTMLElement> | undefined);
4070
+ /**
4071
+ * The bottom-sheet host, kept as a reference for outside-click tests. The sheet relocates
4072
+ * itself to `document.body`, so nothing is moved here. On open its container height is
4073
+ * captured as the sheet's `min-height` floor.
4074
+ */
4075
+ set sheetRef(ref: ElementRef<HTMLElement> | undefined);
3961
4076
  get control(): _angular_forms.AbstractControl<any, any, any> | null;
4077
+ get selectedOption(): MnSelectOption | undefined;
4078
+ /** The label shown in the trigger: the selected option, else the placeholder. */
4079
+ get displayText(): string;
3962
4080
  get showError(): boolean;
3963
4081
  get errorMessages(): string[];
3964
4082
  get errorMessage(): string | null;
3965
4083
  get resolvedId(): string;
3966
4084
  get resolvedName(): string | null;
3967
- get selectClasses(): string;
4085
+ get triggerClasses(): string;
4086
+ /** Whether the panel should currently render as a bottom sheet. */
4087
+ get isSheet(): boolean;
4088
+ /**
4089
+ * Whether the search input is shown: the explicit `searchable` prop when set, otherwise
4090
+ * auto-enabled once the option count reaches the threshold.
4091
+ */
4092
+ get isSearchable(): boolean;
4093
+ get filteredOptions(): MnSelectOption[];
3968
4094
  ngOnInit(): void;
3969
4095
  writeValue(val: unknown): void;
3970
4096
  registerOnChange(fn: (val: unknown) => void): void;
3971
4097
  registerOnTouched(fn: () => void): void;
3972
4098
  setDisabledState(isDisabled: boolean): void;
3973
- /** Returns the index of an option, used as the <option> value attribute */
3974
- optionIndex(option: MnSelectOption): string;
3975
- /** Handles native select change event */
3976
- onSelectChange(event: Event): void;
4099
+ toggle(): void;
4100
+ /** Selects an option, notifies the form and closes — a single choice ends the interaction. */
4101
+ selectOption(option: MnSelectOption): void;
3977
4102
  isSelected(option: MnSelectOption): boolean;
4103
+ onSearch(term: string | null): void;
4104
+ /**
4105
+ * The single close path. Every trigger (outside click, Escape, scroll, resize, the trigger
4106
+ * being hidden, a choice) funnels through here so the open-only listeners are always torn
4107
+ * down with the panel and never leak.
4108
+ */
4109
+ close(): void;
3978
4110
  handleBlur(): void;
4111
+ /**
4112
+ * Dismisses the anchored panel from a shield click, and stops the event there.
4113
+ *
4114
+ * Swallowing it is the point: the shield spans the viewport, so the click would otherwise
4115
+ * land on whatever the panel was floating over. Inside a modal that is the modal's own
4116
+ * backdrop, and "close the dropdown" would double as "throw away the modal". A first click
4117
+ * that only dismisses the overlay is also how native selects and menus behave.
4118
+ */
4119
+ onShieldClick(event: Event): void;
4120
+ onDocumentClick(event: Event): void;
4121
+ /** Closes the dropdown on Escape for keyboard accessibility. */
4122
+ onEscape(): void;
4123
+ /**
4124
+ * Closes the dropdown when the page or a scrollable parent is scrolled.
4125
+ *
4126
+ * Skipped for a sheet: it is anchored to the viewport, not to the trigger, so it has no
4127
+ * stale position to escape. Crucially, opening the soft keyboard fires a `resize` on
4128
+ * Android — closing on that would dismiss the sheet the instant search is focused. A
4129
+ * genuine layout switch is handled by the `matchMedia` listener instead.
4130
+ */
4131
+ onWindowScrollOrResize(): void;
3979
4132
  protected isRequired(): boolean;
3980
- private onChange;
3981
- private onTouched;
4133
+ /**
4134
+ * Tracks the sheet breakpoint through `matchMedia` rather than reading `innerWidth` once,
4135
+ * so rotating the device switches layout instead of leaving a panel positioned for the
4136
+ * previous orientation. An open panel is closed on the switch — its anchored coordinates
4137
+ * and its sheet layout are not interchangeable.
4138
+ */
4139
+ private startWatchingViewport;
4140
+ /** Tears down the breakpoint listener. Idempotent. */
4141
+ private stopWatchingViewport;
4142
+ /**
4143
+ * Freezes the page behind an open sheet. The previous inline value is captured and restored
4144
+ * verbatim so a surrounding modal that set its own lock is left intact.
4145
+ */
4146
+ private lockBodyScroll;
4147
+ /** Restores the pre-lock `overflow`. Idempotent. */
4148
+ private unlockBodyScroll;
4149
+ /** Calculates the fixed position for the dropdown based on the trigger element. */
4150
+ private updateDropdownPosition;
4151
+ /**
4152
+ * Starts the open-only watchers: an `IntersectionObserver` on the trigger (closes the panel
4153
+ * as soon as the trigger stops being rendered/visible) and a capture-phase `scroll` listener
4154
+ * (closes it when any ancestor scroller moves under it). Scrolls that originate inside the
4155
+ * panel's own option list are ignored.
4156
+ */
4157
+ private startWatchingTrigger;
4158
+ /** Tears down the watchers installed by `startWatchingTrigger`. Idempotent. */
4159
+ private stopWatchingTrigger;
4160
+ /**
4161
+ * Records the sheet's opened height as its `min-height` floor. Measured on the next frame
4162
+ * so the read reflects the fully-rendered, unfiltered list (the search box is empty on
4163
+ * open) and never forces a reflow mid change-detection. The floor equals the content height
4164
+ * at that instant, so applying it triggers no resize — it only stops a later, shorter
4165
+ * filtered list from pulling the sheet down.
4166
+ *
4167
+ * `hostEl` is the portalled mn-bottom-sheet host (`display: contents`), so the height is
4168
+ * read from its `.mn-sheet-container` child rather than the host itself.
4169
+ */
4170
+ private captureSheetFloor;
4171
+ /**
4172
+ * Move an overlay element to `document.body` when it appears, and detach it when the query
4173
+ * clears. Appending to the body root makes the element immune to ancestor
4174
+ * `transform`/`filter`/`will-change`, so `position: fixed` anchors to the viewport — without
4175
+ * this the panel lands mid-screen (and breaks outright on iOS).
4176
+ *
4177
+ * Returns the element now portalled, so the caller can store it. Idempotent and safe to
4178
+ * call with `null`.
4179
+ */
4180
+ private portal;
3982
4181
  private resolveConfig;
3983
4182
  private pickErrorKey;
3984
4183
  private resolveErrorMessageForKey;
@@ -4239,10 +4438,14 @@ type MnTranslatable = {
4239
4438
  */
4240
4439
  type MnConfigValue<T = string> = T | MnTranslatable;
4241
4440
  /**
4242
- * A flat key-value map of translations for a single locale.
4243
- * Supports nested keys via dot notation: "form.email.label"
4441
+ * Translations for a single locale, as a tree of keys.
4442
+ *
4443
+ * Both shapes resolve through the same dotted lookup: a bundle may nest
4444
+ * (`{ form: { email: { label } } }`), flatten (`{ "form.email.label": … }`), or mix the two.
4244
4445
  */
4245
- type MnTranslationMap = Record<string, string>;
4446
+ type MnTranslationMap = {
4447
+ [key: string]: string | MnTranslationMap;
4448
+ };
4246
4449
  /**
4247
4450
  * All loaded translations keyed by locale code (e.g. "en", "nl", "de").
4248
4451
  */
@@ -4278,6 +4481,11 @@ declare class MnLanguageService {
4278
4481
  private _locale$;
4279
4482
  private _urlPattern;
4280
4483
  private _debug;
4484
+ /**
4485
+ * `Intl.PluralRules` per locale. Cached because {@link translate} runs on every change
4486
+ * detection through the impure `mnTranslate` pipe, and constructing one is not cheap.
4487
+ */
4488
+ private readonly _pluralRules;
4281
4489
  /** Observable of the current active locale. */
4282
4490
  readonly locale$: Observable<string>;
4283
4491
  /** Current active locale. */
@@ -4309,8 +4517,42 @@ declare class MnLanguageService {
4309
4517
  * Falls back to the key itself if no translation is found.
4310
4518
  *
4311
4519
  * Interpolation replaces `{{paramName}}` with the provided value.
4520
+ *
4521
+ * A `count` param additionally selects the wording that agrees with it: the key is
4522
+ * resolved against its CLDR plural category first (`key` + `One`/`Two`/`Few`/`Many`/
4523
+ * `Zero`), falling back to `key` when that sibling is undefined. Nothing has to opt in —
4524
+ * a key with no sibling behaves exactly as before.
4525
+ *
4526
+ * ```ts
4527
+ * // 'shift.asked' → '{{count}} members are notified'
4528
+ * // 'shift.askedOne' → '{{count}} member is notified'
4529
+ * lang.translate('shift.asked', { count: 3 }); // 3 members are notified
4530
+ * lang.translate('shift.asked', { count: 1 }); // 1 member is notified
4531
+ * ```
4312
4532
  */
4313
4533
  translate(key: string, params?: Record<string, string | number>): string;
4534
+ /**
4535
+ * Picks the wording that agrees with a `count` param.
4536
+ *
4537
+ * A key carrying a count resolves against its CLDR plural category first, so
4538
+ * `askedMessage` + `askedMessageOne` render "3 leden krijgen bericht" and "1 lid krijgt
4539
+ * bericht" off the same call. Both languages change the verb as well as the noun, which
4540
+ * is why each form is a whole sentence under its own key rather than a swapped noun.
4541
+ *
4542
+ * Falls back to `key` whenever the sibling is undefined, so a key that never needed a
4543
+ * plural — or an app that has not written one yet — behaves exactly as it did before.
4544
+ * @param map The active locale's translations.
4545
+ * @param key The dot-notated translation key.
4546
+ * @param params The interpolation values, inspected for `count`.
4547
+ * @returns The key to look up: the plural sibling, or `key` itself.
4548
+ */
4549
+ private resolvePluralKey;
4550
+ /**
4551
+ * The CLDR plural category of a count in the active locale.
4552
+ * @param count The count being quoted.
4553
+ * @returns The category, falling back to English rules for an unusable locale.
4554
+ */
4555
+ private pluralCategory;
4314
4556
  /**
4315
4557
  * Helper to retrieve a value from a potentially nested translation map using a dot-notated key.
4316
4558
  */
@@ -5047,8 +5289,10 @@ declare class MnTable<T = object> extends MnSelectableCollectionBase<T, TableDat
5047
5289
  * button and a stacked filter panel.
5048
5290
  */
5049
5291
  protected filtersCollapsed: boolean;
5050
- /** Whether the small-screen filter panel is currently expanded. */
5292
+ /** Whether the small-screen filter bottom sheet is currently open. */
5051
5293
  protected filtersPanelOpen: boolean;
5294
+ /** Small-screen filter sheet, held so the close button can play its exit. */
5295
+ protected filtersSheet?: MnBottomSheet;
5052
5296
  protected readonly componentName = "MnTable";
5053
5297
  protected get trackedToolbarTemplate(): TemplateRef<unknown> | undefined;
5054
5298
  protected collectionBody?: ElementRef<HTMLElement>;
@@ -5107,12 +5351,16 @@ declare class MnTable<T = object> extends MnSelectableCollectionBase<T, TableDat
5107
5351
  get hasColumnFilters(): boolean;
5108
5352
  /** Label for the "clear all filters" action in the small-screen panel. */
5109
5353
  get clearFiltersButtonLabel(): string;
5354
+ /** Accessible label for the filter sheet's close button. */
5355
+ get filtersCloseLabel(): string;
5110
5356
  /** Heading for the selection summary, with the count filled in. */
5111
5357
  get selectionSummaryTitle(): string;
5112
5358
  /** Label for the summary's clear-everything action. */
5113
5359
  get selectionClearAllLabel(): string;
5114
- /** Opens/closes the stacked filter panel shown on small screens. */
5115
- toggleFiltersPanel(): void;
5360
+ /** Opens the small-screen filter bottom sheet. */
5361
+ openFiltersPanel(): void;
5362
+ /** Plays the sheet's slide-down exit, then unmounts it. */
5363
+ closeFiltersPanel(): Promise<void>;
5116
5364
  private readonly baseTableClasses;
5117
5365
  /**
5118
5366
  * Column widths measured from the automatic layout and pinned, keyed by column