@keenmate/web-multiselect 2.0.0-rc05 → 2.0.0-rc07

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/README.md CHANGED
@@ -21,38 +21,20 @@ Reads `--base-*` variables from the page if [`@keenmate/theme-designer`](https:/
21
21
  - Custom rendering callbacks for options, badges, and group headers.
22
22
  - Form integration via standard hidden inputs (FormData-compatible).
23
23
 
24
+ ## What's New in v2.0.0-rc06
25
+
26
+ - **Tree search — `search-mode="navigate"` now works on trees** — Navigate mode (keep the whole list on screen and jump focus between matches instead of filtering it down) was silently ignored on tree data: the tree search path always collapsed the hierarchy to matches + ancestors and cleared the match set *before* the navigate branch could run, so `search-mode="navigate"` on a `path-member` tree did nothing. It now honors the mode — the always-expanded tree stays fully visible, matching rows get the highlight, focus jumps to the first match, and the match set powers `Ctrl`+`ArrowUp`/`ArrowDown` on desktop plus the fullscreen `N of M` match navigator (count + prev/next) on touch, which now shows for trees too. Filter mode (the default) is unchanged.
27
+
28
+ - **Fullscreen overlay — no longer slips under the system bar on pages with horizontal overflow** — A host page that overflows sideways (e.g. an unbreakable-wide token in a heading) makes the mobile browser shrink-to-fit — it zooms the page out so the overflow fits, splitting the visual viewport from the layout viewport. Because the fullscreen dropdown and selected-items popover are `position: fixed` (anchored to the layout viewport), that zoom left them inset from the physical screen and their top edge slid under the status/nav bar — easily misread as a safe-area issue, but `env(safe-area-inset-*)` is `0` in that state. Opening a fullscreen sheet now clamps `overflow-x: hidden` on `<html>` (restored on close), which removes the overflow so the browser drops the zoom and the sheet sits flush — regardless of the host page. No-op when the page doesn't overflow, and touches only `<html>` so it doesn't fight core's body scroll-lock.
29
+
30
+ - **Fullscreen selected-items popover — Back gesture closes it instead of leaving the page** — The options dropdown already traps the Android Back gesture so it dismisses the fullscreen sheet rather than navigating away, but the selected-items popover — which also goes fullscreen on phones — skipped that guard, so a Back gesture fell through to a real `history(-1)` and left the page (losing the selection view and any unsaved state). The popover now pushes and consumes the same history entry as the dropdown, and the shared handler closes whichever overlay is open. Floating popovers are unaffected.
31
+
32
+ - **Search results — the current match no longer nudges the row sideways** — The leading accent bar on the current match was a real `border-inline-start` that existed only in the matched state, so the checkbox and label jumped ~3px inward the instant a row became the match — content visibly "walked" as you stepped between results. The bar is now an out-of-flow `::before` overlay pinned to the leading edge, so it paints the same accent without touching the row's box model. Still themeable via `--ms-option-border-matched` and RTL-mirrored.
33
+
24
34
  ## What's New in v2.0.0-rc05
25
35
 
26
36
  - **Full-screen overlays — content no longer clipped under landscape system bars** — Rotating a phone to landscape with the full-screen dropdown (or the selected-items popover) open used to push the search field and option rows *under* the Android navigation bar and camera cutout — the field looked "too wide for the screen" and each row's leading edge was hidden. Cause: the sheets are `width: 100vw` (the full physical width) with no safe-area handling. They now set `box-sizing: border-box` and inset their content box by `env(safe-area-inset-{top,right,bottom,left})` on all four edges — the background stays edge-to-edge (full-bleed, no corner gaps) while the header · list · actions column is pulled into the visible region, and the fullscreen toast lifts above the bottom safe area. `env()` resolves to `0` unless the host page opts into edge-to-edge, so this is a no-op on non-cutout / letterboxed pages — a fix where needed, never a regression. Consumer note: for the insets to engage the host page must set `<meta name="viewport" content="… viewport-fit=cover">` (the mobile examples/tests now do).
27
37
 
28
- ## What's New in v2.0.0-rc04
29
-
30
- **Phone search — one-tap clear and a keyboard that gets out of the way** — The
31
- full-screen search sheet gained the two things it was missing on touch. A clear button
32
- now sits at the trailing edge of the search field (a distinct Lucide `search-x` glyph,
33
- so it doesn't read as a second close ✕) to wipe the term in one tap and restore the full
34
- list. And the soft keyboard, which used to stay pinned open once you focused the field,
35
- now tucks away on the three natural "done typing" gestures — scrolling the list, tapping
36
- an option, or pressing Enter/Search — while never dismissing itself mid-type from a
37
- programmatic scroll-to-match.
38
-
39
- **A close button you can make your own** — The full-screen close ✕ is now a themeable
40
- chip: the new `--ms-fullscreen-close-bg`, `--ms-fullscreen-close-border`, and
41
- `--ms-fullscreen-close-border-radius` variables turn the bare glyph into a bordered
42
- button (à la a command-palette close) while the defaults keep it a plain round ✕. Its
43
- tap target was also fixed — the previously-dead padding around the button in the sheet's
44
- top-trailing corner (the natural place to reach) now dismisses the sheet, without ever
45
- stealing taps from the first option row or the search field.
46
-
47
- **Right presentation on every device, via core rc07** — This release pins
48
- `@keenmate/web-components-core` at `1.0.0-rc07` and adopts its reworked
49
- `resolvePresentation` / `classifyDevice` API. Behavior is unchanged — full-screen on
50
- phones, floating on tablet and desktop — with one refinement from the new capability
51
- gate: a narrowed desktop window (fine pointer, hover) now stays `desktop` and keeps its
52
- floating dropdown at any width, instead of ever flipping to the full-screen sheet.
53
-
54
- See `CHANGELOG.md` for the full list.
55
-
56
38
  ## Demos & docs
57
39
 
58
40
  - 🚀 [Live demo](https://web-multiselect.keenmate.dev)
@@ -250,7 +250,7 @@
250
250
  "text": "MultiSelectConfig<T>"
251
251
  },
252
252
  "privacy": "private",
253
- "default": "{ // String options searchHint: element.dataset.searchHint || '', searchPlaceholder: element.dataset.searchPlaceholder || 'Search...', selectPlaceholder: element.dataset.selectPlaceholder || 'Pick an option...', noDataPlaceholder: element.dataset.noDataPlaceholder || undefined, dropdownMinWidth: element.dataset.dropdownMinWidth || undefined, dropdownMaxWidth: element.dataset.dropdownMaxWidth || undefined, badgesDisplayMode: (element.dataset.badgesDisplayMode as any) || 'badges', badgesPosition: (element.dataset.badgesPosition as BadgesPosition) || 'bottom', badgesThresholdMode: (element.dataset.badgesThresholdMode as any) || 'count', maxHeight: element.dataset.maxHeight || '20rem', emptyMessage: element.dataset.emptyMessage || 'No results found', loadingMessage: element.dataset.loadingMessage || 'Loading...', searchInputMode: (element.dataset.searchInputMode as SearchInputMode) || 'normal', searchMode: (element.dataset.searchMode as SearchMode) || 'filter', // Number options badgesThreshold: element.dataset.badgesThreshold ? parseInt(element.dataset.badgesThreshold) : undefined, minSearchLength: parseInt(element.dataset.minSearchLength || '0') || 0, searchDebounce: parseInt(element.dataset.searchDebounce || '0') || 0, // Boolean options (internal names with 'is' prefix) isMultipleEnabled: element.dataset.multiple !== 'false', isGroupsAllowed: element.dataset.allowGroups !== 'false', isCheckboxesShown: element.dataset.showCheckboxes !== 'false', isActionsSticky: element.dataset.stickyActions !== 'false', isCloseOnSelect: element.dataset.closeOnSelect === 'true', isPlacementLocked: element.dataset.lockPlacement !== 'false', isSearchEnabled: element.dataset.enableSearch !== 'false', isAddNewAllowed: element.dataset.allowAddNew === 'true', isCounterShown: element.dataset.showCounter === 'true', isKeepOptionsOnSearch: element.dataset.keepOptionsOnSearch !== 'false', shouldKeepSearchOnClose: element.dataset.keepSearchOnClose !== 'false', // Data and callbacks options: [], container: undefined, // Override with provided options ...options }"
253
+ "default": "{ // String options searchHint: element.dataset.searchHint || '', searchPlaceholder: element.dataset.searchPlaceholder || undefined, selectPlaceholder: element.dataset.selectPlaceholder || 'Pick an option...', noDataPlaceholder: element.dataset.noDataPlaceholder || undefined, dropdownMinWidth: element.dataset.dropdownMinWidth || undefined, dropdownMaxWidth: element.dataset.dropdownMaxWidth || undefined, badgesDisplayMode: (element.dataset.badgesDisplayMode as any) || 'badges', badgesPosition: (element.dataset.badgesPosition as BadgesPosition) || 'bottom', badgesThresholdMode: (element.dataset.badgesThresholdMode as any) || 'count', maxHeight: element.dataset.maxHeight || '20rem', emptyMessage: element.dataset.emptyMessage || 'No results found', loadingMessage: element.dataset.loadingMessage || 'Loading...', searchInputMode: (element.dataset.searchInputMode as SearchInputMode) || 'normal', searchMode: (element.dataset.searchMode as SearchMode) || 'filter', // Number options badgesThreshold: element.dataset.badgesThreshold ? parseInt(element.dataset.badgesThreshold) : undefined, minSearchLength: parseInt(element.dataset.minSearchLength || '0') || 0, searchDebounce: parseInt(element.dataset.searchDebounce || '0') || 0, // Boolean options (internal names with 'is' prefix) isMultipleEnabled: element.dataset.multiple !== 'false', isGroupsAllowed: element.dataset.allowGroups !== 'false', isCheckboxesShown: element.dataset.showCheckboxes !== 'false', isActionsSticky: element.dataset.stickyActions !== 'false', isCloseOnSelect: element.dataset.closeOnSelect === 'true', isPlacementLocked: element.dataset.lockPlacement !== 'false', isSearchEnabled: element.dataset.enableSearch !== 'false', isAddNewAllowed: element.dataset.allowAddNew === 'true', isCounterShown: element.dataset.showCounter === 'true', isSearchModeToggleShown: element.dataset.showSearchModeToggle === 'true', isKeepOptionsOnSearch: element.dataset.keepOptionsOnSearch !== 'false', shouldKeepSearchOnClose: element.dataset.keepSearchOnClose !== 'false', // Data and callbacks options: [], container: undefined, // Override with provided options ...options }"
254
254
  },
255
255
  {
256
256
  "kind": "field",
@@ -496,6 +496,15 @@
496
496
  "privacy": "private",
497
497
  "default": "null"
498
498
  },
499
+ {
500
+ "kind": "field",
501
+ "name": "fullscreenModeToggle",
502
+ "type": {
503
+ "text": "HTMLButtonElement | null"
504
+ },
505
+ "privacy": "private",
506
+ "default": "null"
507
+ },
499
508
  {
500
509
  "kind": "field",
501
510
  "name": "fullscreenNav",
@@ -541,6 +550,15 @@
541
550
  "privacy": "private",
542
551
  "default": "null"
543
552
  },
553
+ {
554
+ "kind": "field",
555
+ "name": "overflowXClamp",
556
+ "type": {
557
+ "text": "{ html: string } | null"
558
+ },
559
+ "privacy": "private",
560
+ "default": "null"
561
+ },
544
562
  {
545
563
  "kind": "field",
546
564
  "name": "keyboardInsetCleanup",
@@ -1178,6 +1196,17 @@
1178
1196
  ],
1179
1197
  "description": "Tree mode: derive the visible list from an **external** set of matched\r\noptions — e.g. the results returned by `searchCallback` — keeping each\r\nmatch's ancestors so indentation stays coherent. This is the async-search\r\nanalogue of `rebuildTreeVisible`: the matching is done by the caller (their\r\nown index/engine) instead of a local substring test, but ancestor\r\npreservation and `treeNodes`/`filteredOptions` index-alignment still happen\r\nhere. Pass all options to show the whole tree."
1180
1198
  },
1199
+ {
1200
+ "kind": "method",
1201
+ "name": "rebuildTreeVisibleForNavigate",
1202
+ "privacy": "private",
1203
+ "return": {
1204
+ "type": {
1205
+ "text": "number"
1206
+ }
1207
+ },
1208
+ "description": "Tree + `search-mode=\"navigate\"`: keep the ENTIRE tree visible (the tree is always\r\nfully expanded, so `flatNodes` is the whole thing) and record which visible rows\r\nmatch the term in `matchingIndices` — the flat-list navigate behavior, but over\r\n`treeNodes`. Filter mode collapses the hierarchy to matches + ancestors; navigate\r\nmode instead leaves the structure intact so the user can jump between matches\r\n(Ctrl+Arrow on desktop, the fullscreen navigator on touch). Returns the index of\r\nthe first match, or -1 (no term / no matches), so the caller can set focus."
1209
+ },
1181
1210
  {
1182
1211
  "kind": "method",
1183
1212
  "name": "detectRTL",
@@ -1414,6 +1443,28 @@
1414
1443
  },
1415
1444
  "description": "Resolve the closed-state input placeholder for the current data/search state.\r\nPriority: explicit no-data placeholder (when the list is empty) → \"pick\" prompt when\r\nsearch is unusable → the search placeholder."
1416
1445
  },
1446
+ {
1447
+ "kind": "method",
1448
+ "name": "getSearchPlaceholder",
1449
+ "privacy": "private",
1450
+ "return": {
1451
+ "type": {
1452
+ "text": "string"
1453
+ }
1454
+ },
1455
+ "description": "The search field placeholder. An explicit `searchPlaceholder` always wins and stays\r\nfixed. Otherwise the default is \"Search...\" — except when the in-overlay mode toggle\r\nis enabled (`isSearchModeToggleShown`), where it becomes mode-aware so the field labels\r\nthe current behavior: \"Search…\" in navigate mode, \"Filter…\" in filter mode. Refreshed\r\non a live mode switch (see setSearchModeLive → refreshSearchPlaceholder)."
1456
+ },
1457
+ {
1458
+ "kind": "method",
1459
+ "name": "refreshSearchPlaceholder",
1460
+ "privacy": "private",
1461
+ "return": {
1462
+ "type": {
1463
+ "text": "void"
1464
+ }
1465
+ },
1466
+ "description": "Re-apply the (possibly mode-aware) placeholder to the live inputs after a mode switch."
1467
+ },
1417
1468
  {
1418
1469
  "kind": "method",
1419
1470
  "name": "renderBadges",
@@ -1971,6 +2022,28 @@
1971
2022
  },
1972
2023
  "description": "Restore page scroll (no-op if it wasn't locked)."
1973
2024
  },
2025
+ {
2026
+ "kind": "method",
2027
+ "name": "clampDocumentOverflowX",
2028
+ "privacy": "private",
2029
+ "return": {
2030
+ "type": {
2031
+ "text": "void"
2032
+ }
2033
+ },
2034
+ "description": "Clip the host document's horizontal overflow while a fullscreen sheet is open.\r\n\r\nA page that overflows horizontally (e.g. an unbreakable-wide token in a heading)\r\nmakes the mobile browser SHRINK-TO-FIT: it zooms the page out so the overflow fits,\r\nwhich desyncs the visual viewport from the layout viewport. Our fullscreen sheet is\r\n`position: fixed` — anchored to the LAYOUT viewport — so under that zoom it no longer\r\nlands flush against the physical screen edges, and the top slips under the system bar\r\n(looks like \"the bar covers the sheet\"). This is NOT a safe-area problem; safe-area\r\ninsets are 0 in that state. Clamping `overflow-x: hidden` on <html>/<body> removes the\r\noverflow, so the browser drops the zoom and the sheet sits flush. Complements\r\nlockBodyScroll() (vertical axis); the saved inline value is restored on close.\r\n\r\nOnly <html> is touched (not <body>): clipping the root's horizontal overflow is\r\nenough to collapse the scrollWidth and cancel the shrink-to-fit, and it avoids\r\nconflicting with core's lockBodyScroll(), which owns <body>'s `overflow`. Idempotent."
2035
+ },
2036
+ {
2037
+ "kind": "method",
2038
+ "name": "releaseDocumentOverflowX",
2039
+ "privacy": "private",
2040
+ "return": {
2041
+ "type": {
2042
+ "text": "void"
2043
+ }
2044
+ },
2045
+ "description": "Restore the <html> `overflow-x` clamped by clampDocumentOverflowX() (no-op if unset)."
2046
+ },
1974
2047
  {
1975
2048
  "kind": "method",
1976
2049
  "name": "observeKeyboardInset",
@@ -2130,6 +2203,69 @@
2130
2203
  },
2131
2204
  "description": "Build the fullscreen overlay header: a search field (proxying to the same\r\n`handleSearch`/`handleKeydown` path as the main input, since the overlay covers\r\nit) plus a close button. Inserted before the scrolling list so it pins to the\r\ntop of the fixed panel. `renderDropdown()` only rewrites `dropdownInner`, so the\r\nheader survives re-renders."
2132
2205
  },
2206
+ {
2207
+ "kind": "method",
2208
+ "name": "ensureFullscreenNav",
2209
+ "privacy": "private",
2210
+ "return": {
2211
+ "type": {
2212
+ "text": "void"
2213
+ }
2214
+ },
2215
+ "description": "Build the navigate-mode match navigator (count + prev/next) and append it to the\r\nfullscreen header, once. No-op if already built or the header isn't present. The\r\nnav wraps onto its own full-width row under the search box (header is flex-wrap;\r\nthe nav takes 100% basis)."
2216
+ },
2217
+ {
2218
+ "kind": "method",
2219
+ "name": "removeFullscreenNav",
2220
+ "privacy": "private",
2221
+ "return": {
2222
+ "type": {
2223
+ "text": "void"
2224
+ }
2225
+ },
2226
+ "description": "Remove the match navigator (switching to filter mode, which has no jump UI)."
2227
+ },
2228
+ {
2229
+ "kind": "method",
2230
+ "name": "toggleSearchModeLive",
2231
+ "privacy": "private",
2232
+ "return": {
2233
+ "type": {
2234
+ "text": "void"
2235
+ }
2236
+ },
2237
+ "description": "Flip searchMode filter<->navigate from the in-overlay toggle."
2238
+ },
2239
+ {
2240
+ "kind": "method",
2241
+ "name": "setSearchModeLive",
2242
+ "privacy": "private",
2243
+ "return": {
2244
+ "type": {
2245
+ "text": "void"
2246
+ }
2247
+ },
2248
+ "parameters": [
2249
+ {
2250
+ "name": "mode",
2251
+ "type": {
2252
+ "text": "SearchMode"
2253
+ }
2254
+ }
2255
+ ],
2256
+ "description": "Switch searchMode in place — the overlay's toggle path. The `search-mode` attribute\r\nis reinit-on-change (it rebuilds and closes the overlay); this instead mutates the\r\nlive config, adds/removes the match navigator to match, and re-projects the current\r\nterm under the new mode (filter narrows the list / navigate keeps all + highlights),\r\nall without tearing the open sheet down. Focus stays on the search field."
2257
+ },
2258
+ {
2259
+ "kind": "method",
2260
+ "name": "updateFullscreenModeToggle",
2261
+ "privacy": "private",
2262
+ "return": {
2263
+ "type": {
2264
+ "text": "void"
2265
+ }
2266
+ },
2267
+ "description": "Sync the mode toggle's icon (via data-mode) and labels with the current searchMode.\r\nNo-op when the toggle isn't built (opt-out, floating panel, or search hidden)."
2268
+ },
2133
2269
  {
2134
2270
  "kind": "method",
2135
2271
  "name": "updateFullscreenNav",
@@ -2277,7 +2413,7 @@
2277
2413
  {
2278
2414
  "name": "ctx",
2279
2415
  "type": {
2280
- "text": "BadgeContentRenderContext"
2416
+ "text": "Omit<BadgeContentRenderContext, 'presentation' | 'isFullscreen' | 'isModal'>"
2281
2417
  }
2282
2418
  }
2283
2419
  ],
@@ -3157,7 +3293,7 @@
3157
3293
  "name": "inputs",
3158
3294
  "privacy": "protected",
3159
3295
  "static": true,
3160
- "default": "[ // ── Strings (cosmetic → update). Optional ones are nullable: absent → null ─ { configKey: 'searchHint', attribute: 'search-hint', converter: toText({ isNullable: true }), on: 'update', description: 'Small hint text shown beneath the search input.' }, { configKey: 'searchPlaceholder', attribute: 'search-placeholder', converter: toText({ default: 'Search...' }), on: 'update', description: 'Placeholder text for the search input.' }, { configKey: 'selectPlaceholder', attribute: 'select-placeholder', converter: toText({ default: 'Pick an option...' }), on: 'update', description: 'Placeholder shown on the control when nothing is selected.' }, { configKey: 'noDataPlaceholder', attribute: 'no-data-placeholder', converter: toText({ isNullable: true }), on: 'update', description: 'Text shown when there are no options at all.' }, { configKey: 'dropdownMinWidth', attribute: 'dropdown-min-width', converter: toText({ isNullable: true }), on: 'update', description: 'Minimum width of the dropdown panel (any CSS length).' }, { configKey: 'dropdownMaxWidth', attribute: 'dropdown-max-width', converter: toText({ isNullable: true }), on: 'update', description: 'Maximum width of the dropdown panel (any CSS length).' }, { configKey: 'maxHeight', attribute: 'max-height', converter: toText({ default: '20rem' }), on: 'update', description: 'Maximum height of the dropdown list before it scrolls.' }, { configKey: 'emptyMessage', attribute: 'empty-message', converter: toText({ default: 'No results found' }), on: 'update', description: 'Message shown when a search yields no matches.' }, { configKey: 'loadingMessage', attribute: 'loading-message', converter: toText({ default: 'Loading...' }), on: 'update', description: 'Message shown while options are loading.' }, { configKey: 'removeButtonTooltipText', attribute: 'remove-button-tooltip-text', converter: toText({ isNullable: true }), on: 'update', description: 'Tooltip text for a badge remove (×) button.' }, { configKey: 'formFieldId', attribute: 'name', converter: toText({ isNullable: true }), on: 'reinit', description: 'HTML form field name/id used for the hidden input(s).' }, // ── CSS-var sugar (mirrored to a host style prop in reinit()/update()) ──── { configKey: 'dropdownWidth', attribute: 'dropdown-width', converter: toText({ isNullable: true }), on: 'update', description: 'Fixed dropdown width; mirrored to the `--ms-dropdown-width` CSS variable.' }, { configKey: 'selectedPopoverWidth', attribute: 'selected-popover-width', converter: toText({ isNullable: true }), on: 'update', description: 'Selected-items popover width; mirrored to `--ms-selected-popover-width`.' }, // ── Member properties (structural → reinit; optional → nullable) ───────── { configKey: 'valueMember', attribute: 'value-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name on an option object that holds its value.' }, { configKey: 'displayValueMember', attribute: 'display-value-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option display label.' }, { configKey: 'searchValueMember', attribute: 'search-value-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name searched against (falls back to the display value).' }, { configKey: 'iconMember', attribute: 'icon-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option icon.' }, { configKey: 'subtitleMember', attribute: 'subtitle-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option subtitle.' }, { configKey: 'fullTitleMember', attribute: 'full-title-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option full/long title.' }, { configKey: 'groupMember', attribute: 'group-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name used to group options under headers.' }, { configKey: 'disabledMember', attribute: 'disabled-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that marks an option disabled.' }, // ── Tree of options (structural → reinit; optional → nullable) ─────────── { configKey: 'pathMember', attribute: 'path-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name holding a node materialized tree path.' }, { configKey: 'parentPathMember', attribute: 'parent-path-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name holding a node parent path.' }, { configKey: 'levelMember', attribute: 'level-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name holding a node depth level.' }, { configKey: 'hasChildrenMember', attribute: 'has-children-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name flagging that a node has children.' }, { configKey: 'isSelectableMember', attribute: 'is-selectable-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name marking whether a node can be selected.' }, { configKey: 'treePathSeparator', attribute: 'tree-path-separator', converter: toText({ default: '.' }), reflect: true, on: 'reinit', description: 'Separator between segments in a materialized tree path.' }, { configKey: 'isTreeEnabled', converter: toBool('tristate'), on: 'reinit', type: 'boolean', description: 'Force tree mode on/off. Property-only; when unset (null) tree mode auto-enables if a path source (path-member / getPathCallback) is present.' }, { configKey: 'checkboxMode', attribute: 'checkbox-mode', converter: toEnum(['independent', 'cascade'] as const, { default: 'independent' }), reflect: true, on: 'update', description: `Tree checkbox interaction. - \\`independent\\` (default) — toggles only the clicked node. - \\`cascade\\` — checks a node whole subtree and shows a tristate (checked / indeterminate / unchecked) box on branches. Tree + multiple only.` }, { configKey: 'cascadeSelectPolicy', attribute: 'cascade-select-policy', converter: toEnum(['rolled-up', 'leaves', 'all'] as const, { default: 'rolled-up' }), reflect: true, on: 'update', description: `In \\`cascade\\` mode, which values a selection emits (badges / form / change): - \\`rolled-up\\` (default) — minimal cover: a fully-selected subtree collapses to its root; partially-selected branches emit their individually-checked descendants. - \\`leaves\\` — only the checked leaf-level nodes. - \\`all\\` — every fully-checked node (branches and leaves).` }, // ── Enums ──────────────────────────────────────────────────────────────── { configKey: 'badgesDisplayMode', attribute: 'badges-display-mode', converter: toEnum(['badges', 'count', 'compact', 'partial', 'none'] as const, { default: 'badges' }), on: 'reinit', description: 'How the current selection is shown in the control.' }, { configKey: 'badgesPosition', attribute: 'badges-position', converter: toEnum(['top', 'bottom', 'left', 'right'] as const, { default: 'bottom' }), on: 'reinit', description: 'Where the badges/selection appear relative to the input.' }, { configKey: 'badgesThresholdMode', attribute: 'badges-threshold-mode', converter: toEnum(['count', 'partial'] as const, { default: 'count' }), on: 'update', description: 'How `badgesThreshold` is interpreted: collapse to a count badge, or keep partial badges + a \"more\" badge.' }, { configKey: 'searchInputMode', attribute: 'search-input-mode', converter: toEnum(['normal', 'readonly', 'hidden'] as const, { default: 'normal' }), on: 'reinit', description: 'Search field mode: editable, read-only, or hidden.' }, { configKey: 'searchMode', attribute: 'search-mode', converter: toEnum(['filter', 'navigate'] as const, { default: 'filter' }), on: 'reinit', description: 'Whether typing filters the list or navigates it.' }, { configKey: 'actionsLayout', attribute: 'actions-layout', converter: toEnum(['nowrap', 'wrap'] as const, { default: 'nowrap' }), on: 'reinit', description: 'Whether the action bar wraps or stays on one line.' }, { configKey: 'actionsPosition', attribute: 'actions-position', converter: toEnum(['top', 'bottom'] as const, { default: 'top' }), on: 'reinit', description: 'Whether the action bar sits above or below the list.' }, { configKey: 'actionsAlign', attribute: 'actions-align', converter: toEnum(['stretch', 'left', 'right', 'center', 'space-between'] as const, { default: 'stretch' }), on: 'update', description: 'Horizontal alignment of the action buttons.' }, { configKey: 'checkboxAlign', attribute: 'checkbox-align', converter: toEnum(['top', 'center', 'bottom'] as const, { default: 'center' }), on: 'update', description: 'Vertical alignment of an option checkbox.' }, { configKey: 'valueFormat', attribute: 'value-format', converter: toEnum(['json', 'csv', 'array'] as const, { default: 'json' }), on: 'reinit', description: 'Serialization format the control emits its value in.' }, { configKey: 'badgeTooltipPlacement', attribute: 'badge-tooltip-placement', converter: toEnum(PLACEMENTS, { default: 'top' }), on: 'update', description: 'Preferred placement of a badge tooltip relative to its badge (floating-ui placement).' }, { configKey: 'optionTooltipPlacement', attribute: 'option-tooltip-placement', converter: toEnum(PLACEMENTS, { default: 'top-start' }), on: 'update', description: 'Preferred placement of an option tooltip (floating-ui placement).' }, { configKey: 'mobilePresentation', attribute: 'mobile-presentation', converter: toEnum(['auto', 'floating', 'fullscreen'] as const, { default: 'auto' }), reflect: true, on: 'update', description: 'How the open dropdown is presented on phones. `auto` (default) keeps the floating panel on desktop/tablet and switches to a full-screen overlay on phone-sized touch devices (touch primary + shorter viewport side < 600px, orientation-robust); `floating` forces the anchored panel everywhere; `fullscreen` forces the full-screen overlay on any device (handy for previews/testing). Resolved reactively from the device/viewport environment.' }, { configKey: 'fullscreenAutofocus', attribute: 'fullscreen-autofocus', converter: toBool('default-false'), on: 'update', description: 'In the phone fullscreen overlay, auto-focus the search field on open (pops the soft keyboard immediately). Default `false`: the sheet opens with the list visible and the keyboard closed, appearing only when the user taps the search. Set `true` to type-to-filter right away. No effect in the floating presentation.' }, // ── Numbers ────────────────────────────────────────────────────────────── { configKey: 'badgesThreshold', attribute: 'badges-threshold', converter: toInt(), on: 'update', description: 'Threshold at which badges collapse to a count/compact view.' }, { configKey: 'badgesMaxVisible', attribute: 'badges-max-visible', converter: toInt(), on: 'update', description: 'Maximum number of badges rendered before overflow.' }, { configKey: 'minSearchLength', attribute: 'min-search-length', converter: toInt({ default: 0 }), on: 'update', description: 'Minimum characters before searching/filtering starts.' }, { configKey: 'searchDebounce', attribute: 'search-debounce', converter: toInt({ default: 0 }), on: 'update', description: 'Debounce delay in ms applied to the search input.' }, { configKey: 'virtualScrollThreshold', attribute: 'virtual-scroll-threshold', converter: toInt({ default: 100 }), on: 'reinit', description: 'Option count above which virtual scrolling turns on.' }, { configKey: 'optionHeight', attribute: 'option-height', converter: toInt({ default: 50 }), on: 'update', description: 'Fixed row height in px used by virtual scrolling.' }, { configKey: 'badgeHeight', attribute: 'badge-height', converter: toInt({ default: 36 }), on: 'update', description: 'Fixed badge height in px used for layout/virtualization.' }, { configKey: 'virtualScrollBuffer', attribute: 'virtual-scroll-buffer', converter: toInt({ default: 10 }), on: 'update', description: 'Extra rows rendered above/below the viewport when virtualizing.' }, { configKey: 'badgeTooltipDelay', attribute: 'badge-tooltip-delay', converter: toInt({ default: 100 }), on: 'update', description: 'Delay in ms before a badge tooltip appears.' }, { configKey: 'badgeTooltipOffset', attribute: 'badge-tooltip-offset', converter: toInt({ default: 8 }), on: 'update', description: 'Gap in px between a badge and its tooltip.' }, { configKey: 'optionTooltipDelay', attribute: 'option-tooltip-delay', converter: toInt(), on: 'update', description: 'Delay in ms before an option tooltip appears (falls back to badgeTooltipDelay).' }, { configKey: 'optionTooltipOffset', attribute: 'option-tooltip-offset', converter: toInt(), on: 'update', description: 'Gap in px between an option and its tooltip.' }, // ── Booleans (default true) ────────────────────────────────────────────── { configKey: 'isMultipleEnabled', attribute: 'multiple', converter: toBool('default-true'), on: 'reinit', description: 'Allow selecting multiple options. When off, selecting one replaces the previous.' }, { configKey: 'isGroupsAllowed', attribute: 'allow-groups', converter: toBool('default-true'), on: 'reinit', description: 'Allow grouping options under group headers.' }, { configKey: 'isCheckboxesShown', attribute: 'show-checkboxes', converter: toBool('default-true'), on: 'reinit', description: 'Show a checkbox on each option.' }, { configKey: 'isActionsSticky', attribute: 'sticky-actions', converter: toBool('default-true'), on: 'update', description: 'Keep the action bar pinned while the list scrolls.' }, { configKey: 'isPlacementLocked', attribute: 'lock-placement', converter: toBool('default-true'), on: 'update', description: 'Keep the dropdown initial placement instead of flipping when it fits.' }, { configKey: 'isSearchEnabled', attribute: 'enable-search', converter: toBool('default-true'), on: 'reinit', description: 'Show the search input.' }, { configKey: 'isKeepOptionsOnSearch', attribute: 'keep-options-on-search', converter: toBool('default-true'), on: 'update', description: 'Keep already-selected options visible while filtering.' }, { configKey: 'shouldKeepSearchOnClose', attribute: 'should-keep-search-on-close', converter: toBool('default-true'), on: 'update', description: 'Preserve the search text after the dropdown closes.' }, // ── Booleans (default false) ───────────────────────────────────────────── { configKey: 'isCloseOnSelect', attribute: 'close-on-select', converter: toBool('default-false'), on: 'update', description: 'Close the dropdown immediately after a selection.' }, { configKey: 'isAddNewAllowed', attribute: 'allow-add-new', converter: toBool('default-false'), on: 'reinit', description: 'Allow adding a new option from the search text.' }, { configKey: 'isCounterShown', attribute: 'show-counter', converter: toBool('default-false'), on: 'update', description: 'Show a selected-count indicator.' }, { configKey: 'isBadgeFullTitleShown', attribute: 'show-badge-full-title', converter: toBool('default-false'), on: 'update', description: 'Show the full title on badges instead of the short label.' }, { configKey: 'isVirtualScrollEnabled', attribute: 'enable-virtual-scroll', converter: toBool('default-false'), on: 'reinit', description: 'Force virtual scrolling on regardless of the threshold.' }, { configKey: 'isBadgeTooltipsEnabled', attribute: 'enable-badge-tooltips', converter: toBool('default-false'), on: 'update', description: 'Enable tooltips on badges.' }, { configKey: 'isOptionTooltipsEnabled', attribute: 'enable-option-tooltips', converter: toBool('default-false'), on: 'update', description: 'Enable tooltips on options.' }, { configKey: 'isOptionTooltipFollowCursor', attribute: 'option-tooltip-follow-cursor', converter: toBool('default-false'), on: 'update', description: 'Make option tooltips follow the pointer.' }, // ── Special attributes ─────────────────────────────────────────────────── { configKey: 'initialValues', attribute: 'initial-values', converter: toInitialValues(), default: [], on: 'reinit', type: 'Array<string | number>', description: 'Values selected on first render. Accepts a JSON array (`[\"a\",\"b\"]`) or a bare CSV (`a,b,c`).' }, { configKey: 'showDebugInfo', attribute: 'show-debug-info', converter: toBool('default-false'), on: 'update', description: 'Render an in-component debug panel.', deprecated: 'Use per-instance logging (el.enableLogging()) instead.' }, // ── Complex property (data) ────────────────────────────────────────────── { configKey: 'options', converter: toObjectArray(), on: 'reinit', type: 'ReadonlyArray<Record<string, unknown>>', description: 'The array of option objects to render. The JS API — assign `el.options` directly. For HTML authoring use the `data-options` attribute (parsed per `data-options-format`) or declarative <option> children; both feed the same list and take precedence over this property in the order: <option> children > property > data-options.' }, { configKey: 'optionsSource', attribute: 'data-options', converter: toText({ isNullable: true }), on: 'reinit', type: 'string', description: 'HTML-authoring source for the option list, parsed per `data-options-format`. Reactive: changing either attribute re-renders. Prefer the `options` property in JS; a set `options` property and declarative <option> children both win over this.' }, { configKey: 'optionsFormat', attribute: 'data-options-format', converter: toEnum(OPTIONS_FORMATS, { default: 'json' }), on: 'reinit', type: \"'json' | 'csv' | 'plain'\", description: 'How to parse the `data-options` attribute: `json` (a JSON array of objects or [value, label] tuples), `csv` (rows split on `data-options-row-splitter`, cells on `data-options-splitter`; the first row is a header — map columns via *-member), or `plain` (bare values split on both splitters -> [value, label] tuples, value === label). Default `json`.' }, { configKey: 'optionsSplitter', attribute: 'data-options-splitter', converter: toText({ default: ',' }), on: 'reinit', type: 'string', description: 'Field/cell delimiter for the `csv` and `plain` `data-options` formats. Default `,`. Escapes `\\\\t` `\\\\n` `\\\\r` are honoured (e.g. `data-options-splitter=\"\\\\t\"` for TSV). Ignored for `json`.' }, { configKey: 'optionsRowSplitter', attribute: 'data-options-row-splitter', converter: toText({ default: '\\n' }), on: 'reinit', type: 'string', description: 'Row/record delimiter for the `csv` and `plain` `data-options` formats. Default newline. Escapes honoured (e.g. `data-options-row-splitter=\";\"` for single-line data). Ignored for `json`.' }, { configKey: 'actionButtons', converter: toValue({ validate: (v): v is unknown[] => Array.isArray(v) }), on: 'reinit', type: 'Array<Record<string, unknown>>', description: 'Custom action buttons for the dropdown footer/header. Property-only; when unset the default Select-All / Clear buttons apply.' }, // ── Callbacks: data shape (structural → reinit) ────────────────────────── { configKey: 'getValueCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string | number', description: 'Extract an option value (overrides valueMember).' }, { configKey: 'getPathCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string', description: 'Extract a node tree path (enables tree mode; overrides pathMember).' }, { configKey: 'getGroupCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string', description: 'Extract the group name from an option (overrides groupMember).' }, { configKey: 'getDisabledCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => boolean', description: 'Whether an option is disabled (overrides disabledMember).' }, { configKey: 'getIsSelectableCallback', converter: cb(), on: 'reinit', type: '(node: unknown) => boolean', description: 'Whether a tree node can be selected (overrides is-selectable-member).' }, { configKey: 'getSearchValueCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string', description: 'Text an option is searched against (overrides searchValueMember).' }, { configKey: 'searchCallback', converter: cb(), on: 'reinit', type: '(searchTerm: string, signal?: AbortSignal) => Promise<unknown[]>', description: 'Custom / async search; return the filtered options.' }, // ── Callbacks: display / render (cosmetic → update) ────────────────────── { configKey: 'getDisplayValueCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Compute the display label for an option (overrides displayValueMember).' }, { configKey: 'getBadgeDisplayCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Compute the text shown on an option badge.' }, { configKey: 'getBadgeClassCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | string[]', description: 'Extra CSS class(es) for an option badge.' }, { configKey: 'getIconCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Icon for an option (overrides iconMember).' }, { configKey: 'getSubtitleCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Subtitle for an option (overrides subtitleMember).' }, { configKey: 'getFullTitleCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Full title for an option (used by badges when show-badge-full-title is on).' }, { configKey: 'getCounterCallback', converter: cb(), on: 'update', type: '(count: number, moreCount?: number) => string', description: 'Render the selected-count label.' }, { configKey: 'getValueFormatCallback', converter: cb(), on: 'update', type: '(selectedValues: (string | number)[]) => string', description: 'Serialize the selected values for form submission.' }, { configKey: 'getBadgeTooltipCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | HTMLElement', description: 'Tooltip content for an option badge.' }, { configKey: 'getOptionTooltipCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | HTMLElement', description: 'Tooltip content for an option row.' }, { configKey: 'getRemoveButtonTooltipCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Tooltip text for a badge remove button.' }, { configKey: 'getSelectedItemClassCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | string[]', description: 'Extra CSS class(es) for a selected item.' }, { configKey: 'renderOptionContentCallback', converter: cb(), on: 'update', type: '(item: unknown, context: OptionContentRenderContext) => string | HTMLElement', description: 'Custom render for an option row; may return HTML or an element.' }, { configKey: 'renderBadgeContentCallback', converter: cb(), on: 'update', type: '(item: unknown, context: BadgeContentRenderContext) => string | HTMLElement', description: 'Custom render for a badge; may return HTML or an element.' }, { configKey: 'renderGroupLabelContentCallback', converter: cb(), on: 'update', type: '(groupName: string) => string | HTMLElement', description: 'Customize a group label; may return an HTML string or element.' }, { configKey: 'renderSelectedContentCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Custom render for the whole selected area.' }, { configKey: 'renderSelectedItemContentCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | HTMLElement', description: 'Custom render for one selected item.' }, { configKey: 'customStylesCallback', converter: cb(), on: 'update', type: '() => string', description: 'Returns a CSS string injected into the component via a replaceable style slot (§12.8).' }, // ── Callbacks: before-hooks (behavior-shaping) ─────────────────────────── { configKey: 'beforeSearchCallback', converter: cb(), on: 'update', type: '(searchTerm: string) => string | null', description: 'Runs before a search; return a rewritten term or null to veto.' }, { configKey: 'beforeSelectCallback', converter: cb(), on: 'update', type: '(option: unknown, selectedOptions: unknown[]) => boolean | string | void', description: 'Runs before selecting; return false to veto, or a string to veto and show it as a message.' }, { configKey: 'beforeDeselectCallback', converter: cb(), on: 'update', type: '(option: unknown, selectedOptions: unknown[]) => boolean | string | void', description: 'Runs before deselecting; return false to veto, or a string to veto and show it as a message.' }, { configKey: 'addNewCallback', converter: cb(), on: 'update', type: '(value: string) => unknown | Promise<unknown>', description: 'Create a new option from the typed text.' }, ]",
3296
+ "default": "[ // ── Strings (cosmetic → update). Optional ones are nullable: absent → null ─ { configKey: 'searchHint', attribute: 'search-hint', converter: toText({ isNullable: true }), on: 'update', description: 'Small hint text shown beneath the search input.' }, { configKey: 'searchPlaceholder', attribute: 'search-placeholder', converter: toText({ isNullable: true }), on: 'update', description: 'Placeholder text for the search input. When unset it defaults to \"Search...\"; if `show-search-mode-toggle` is on, the default instead becomes mode-aware (\"Search…\" in navigate, \"Filter…\" in filter). An explicit value always wins and stays fixed.' }, { configKey: 'selectPlaceholder', attribute: 'select-placeholder', converter: toText({ default: 'Pick an option...' }), on: 'update', description: 'Placeholder shown on the control when nothing is selected.' }, { configKey: 'noDataPlaceholder', attribute: 'no-data-placeholder', converter: toText({ isNullable: true }), on: 'update', description: 'Text shown when there are no options at all.' }, { configKey: 'dropdownMinWidth', attribute: 'dropdown-min-width', converter: toText({ isNullable: true }), on: 'update', description: 'Minimum width of the dropdown panel (any CSS length).' }, { configKey: 'dropdownMaxWidth', attribute: 'dropdown-max-width', converter: toText({ isNullable: true }), on: 'update', description: 'Maximum width of the dropdown panel (any CSS length).' }, { configKey: 'maxHeight', attribute: 'max-height', converter: toText({ default: '20rem' }), on: 'update', description: 'Maximum height of the dropdown list before it scrolls.' }, { configKey: 'emptyMessage', attribute: 'empty-message', converter: toText({ default: 'No results found' }), on: 'update', description: 'Message shown when a search yields no matches.' }, { configKey: 'loadingMessage', attribute: 'loading-message', converter: toText({ default: 'Loading...' }), on: 'update', description: 'Message shown while options are loading.' }, { configKey: 'removeButtonTooltipText', attribute: 'remove-button-tooltip-text', converter: toText({ isNullable: true }), on: 'update', description: 'Tooltip text for a badge remove (×) button.' }, { configKey: 'formFieldId', attribute: 'name', converter: toText({ isNullable: true }), on: 'reinit', description: 'HTML form field name/id used for the hidden input(s).' }, // ── CSS-var sugar (mirrored to a host style prop in reinit()/update()) ──── { configKey: 'dropdownWidth', attribute: 'dropdown-width', converter: toText({ isNullable: true }), on: 'update', description: 'Fixed dropdown width; mirrored to the `--ms-dropdown-width` CSS variable.' }, { configKey: 'selectedPopoverWidth', attribute: 'selected-popover-width', converter: toText({ isNullable: true }), on: 'update', description: 'Selected-items popover width; mirrored to `--ms-selected-popover-width`.' }, // ── Member properties (structural → reinit; optional → nullable) ───────── { configKey: 'valueMember', attribute: 'value-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name on an option object that holds its value.' }, { configKey: 'displayValueMember', attribute: 'display-value-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option display label.' }, { configKey: 'searchValueMember', attribute: 'search-value-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name searched against (falls back to the display value).' }, { configKey: 'iconMember', attribute: 'icon-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option icon.' }, { configKey: 'subtitleMember', attribute: 'subtitle-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option subtitle.' }, { configKey: 'fullTitleMember', attribute: 'full-title-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that holds an option full/long title.' }, { configKey: 'groupMember', attribute: 'group-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name used to group options under headers.' }, { configKey: 'disabledMember', attribute: 'disabled-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name that marks an option disabled.' }, // ── Tree of options (structural → reinit; optional → nullable) ─────────── { configKey: 'pathMember', attribute: 'path-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name holding a node materialized tree path.' }, { configKey: 'parentPathMember', attribute: 'parent-path-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name holding a node parent path.' }, { configKey: 'levelMember', attribute: 'level-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name holding a node depth level.' }, { configKey: 'hasChildrenMember', attribute: 'has-children-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name flagging that a node has children.' }, { configKey: 'isSelectableMember', attribute: 'is-selectable-member', converter: toText({ isNullable: true }), reflect: true, on: 'reinit', description: 'Property name marking whether a node can be selected.' }, { configKey: 'treePathSeparator', attribute: 'tree-path-separator', converter: toText({ default: '.' }), reflect: true, on: 'reinit', description: 'Separator between segments in a materialized tree path.' }, { configKey: 'isTreeEnabled', converter: toBool('tristate'), on: 'reinit', type: 'boolean', description: 'Force tree mode on/off. Property-only; when unset (null) tree mode auto-enables if a path source (path-member / getPathCallback) is present.' }, { configKey: 'checkboxMode', attribute: 'checkbox-mode', converter: toEnum(['independent', 'cascade'] as const, { default: 'independent' }), reflect: true, on: 'update', description: `Tree checkbox interaction. - \\`independent\\` (default) — toggles only the clicked node. - \\`cascade\\` — checks a node whole subtree and shows a tristate (checked / indeterminate / unchecked) box on branches. Tree + multiple only.` }, { configKey: 'cascadeSelectPolicy', attribute: 'cascade-select-policy', converter: toEnum(['rolled-up', 'leaves', 'all'] as const, { default: 'rolled-up' }), reflect: true, on: 'update', description: `In \\`cascade\\` mode, which values a selection emits (badges / form / change): - \\`rolled-up\\` (default) — minimal cover: a fully-selected subtree collapses to its root; partially-selected branches emit their individually-checked descendants. - \\`leaves\\` — only the checked leaf-level nodes. - \\`all\\` — every fully-checked node (branches and leaves).` }, // ── Enums ──────────────────────────────────────────────────────────────── { configKey: 'badgesDisplayMode', attribute: 'badges-display-mode', converter: toEnum(['badges', 'count', 'compact', 'partial', 'none'] as const, { default: 'badges' }), on: 'reinit', description: 'How the current selection is shown in the control.' }, { configKey: 'badgesPosition', attribute: 'badges-position', converter: toEnum(['top', 'bottom', 'left', 'right'] as const, { default: 'bottom' }), on: 'reinit', description: 'Where the badges/selection appear relative to the input.' }, { configKey: 'badgesThresholdMode', attribute: 'badges-threshold-mode', converter: toEnum(['count', 'partial'] as const, { default: 'count' }), on: 'update', description: 'How `badgesThreshold` is interpreted: collapse to a count badge, or keep partial badges + a \"more\" badge.' }, { configKey: 'searchInputMode', attribute: 'search-input-mode', converter: toEnum(['normal', 'readonly', 'hidden'] as const, { default: 'normal' }), on: 'reinit', description: 'Search field mode: editable, read-only, or hidden.' }, { configKey: 'searchMode', attribute: 'search-mode', converter: toEnum(['filter', 'navigate'] as const, { default: 'filter' }), on: 'reinit', description: 'Whether typing filters the list or navigates it.' }, { configKey: 'actionsLayout', attribute: 'actions-layout', converter: toEnum(['nowrap', 'wrap'] as const, { default: 'nowrap' }), on: 'reinit', description: 'Whether the action bar wraps or stays on one line.' }, { configKey: 'actionsPosition', attribute: 'actions-position', converter: toEnum(['top', 'bottom'] as const, { default: 'top' }), on: 'reinit', description: 'Whether the action bar sits above or below the list.' }, { configKey: 'actionsAlign', attribute: 'actions-align', converter: toEnum(['stretch', 'left', 'right', 'center', 'space-between'] as const, { default: 'stretch' }), on: 'update', description: 'Horizontal alignment of the action buttons.' }, { configKey: 'checkboxAlign', attribute: 'checkbox-align', converter: toEnum(['top', 'center', 'bottom'] as const, { default: 'center' }), on: 'update', description: 'Vertical alignment of an option checkbox.' }, { configKey: 'valueFormat', attribute: 'value-format', converter: toEnum(['json', 'csv', 'array'] as const, { default: 'json' }), on: 'reinit', description: 'Serialization format the control emits its value in.' }, { configKey: 'badgeTooltipPlacement', attribute: 'badge-tooltip-placement', converter: toEnum(PLACEMENTS, { default: 'top' }), on: 'update', description: 'Preferred placement of a badge tooltip relative to its badge (floating-ui placement).' }, { configKey: 'optionTooltipPlacement', attribute: 'option-tooltip-placement', converter: toEnum(PLACEMENTS, { default: 'top-start' }), on: 'update', description: 'Preferred placement of an option tooltip (floating-ui placement).' }, { configKey: 'mobilePresentation', attribute: 'mobile-presentation', converter: toEnum(['auto', 'floating', 'fullscreen'] as const, { default: 'auto' }), reflect: true, on: 'update', description: 'How the open dropdown is presented on phones. `auto` (default) keeps the floating panel on desktop/tablet and switches to a full-screen overlay on phone-sized touch devices (touch primary + shorter viewport side < 600px, orientation-robust); `floating` forces the anchored panel everywhere; `fullscreen` forces the full-screen overlay on any device (handy for previews/testing). Resolved reactively from the device/viewport environment.' }, { configKey: 'fullscreenAutofocus', attribute: 'fullscreen-autofocus', converter: toBool('default-false'), on: 'update', description: 'In the phone fullscreen overlay, auto-focus the search field on open (pops the soft keyboard immediately). Default `false`: the sheet opens with the list visible and the keyboard closed, appearing only when the user taps the search. Set `true` to type-to-filter right away. No effect in the floating presentation.' }, // ── Numbers ────────────────────────────────────────────────────────────── { configKey: 'badgesThreshold', attribute: 'badges-threshold', converter: toInt(), on: 'update', description: 'Threshold at which badges collapse to a count/compact view.' }, { configKey: 'badgesMaxVisible', attribute: 'badges-max-visible', converter: toInt(), on: 'update', description: 'Maximum number of badges rendered before overflow.' }, { configKey: 'minSearchLength', attribute: 'min-search-length', converter: toInt({ default: 0 }), on: 'update', description: 'Minimum characters before searching/filtering starts.' }, { configKey: 'searchDebounce', attribute: 'search-debounce', converter: toInt({ default: 0 }), on: 'update', description: 'Debounce delay in ms applied to the search input.' }, { configKey: 'virtualScrollThreshold', attribute: 'virtual-scroll-threshold', converter: toInt({ default: 100 }), on: 'reinit', description: 'Option count above which virtual scrolling turns on.' }, { configKey: 'optionHeight', attribute: 'option-height', converter: toInt({ default: 50 }), on: 'update', description: 'Fixed row height in px used by virtual scrolling.' }, { configKey: 'badgeHeight', attribute: 'badge-height', converter: toInt({ default: 36 }), on: 'update', description: 'Fixed badge height in px used for layout/virtualization.' }, { configKey: 'virtualScrollBuffer', attribute: 'virtual-scroll-buffer', converter: toInt({ default: 10 }), on: 'update', description: 'Extra rows rendered above/below the viewport when virtualizing.' }, { configKey: 'badgeTooltipDelay', attribute: 'badge-tooltip-delay', converter: toInt({ default: 100 }), on: 'update', description: 'Delay in ms before a badge tooltip appears.' }, { configKey: 'badgeTooltipOffset', attribute: 'badge-tooltip-offset', converter: toInt({ default: 8 }), on: 'update', description: 'Gap in px between a badge and its tooltip.' }, { configKey: 'optionTooltipDelay', attribute: 'option-tooltip-delay', converter: toInt(), on: 'update', description: 'Delay in ms before an option tooltip appears (falls back to badgeTooltipDelay).' }, { configKey: 'optionTooltipOffset', attribute: 'option-tooltip-offset', converter: toInt(), on: 'update', description: 'Gap in px between an option and its tooltip.' }, // ── Booleans (default true) ────────────────────────────────────────────── { configKey: 'isMultipleEnabled', attribute: 'multiple', converter: toBool('default-true'), on: 'reinit', description: 'Allow selecting multiple options. When off, selecting one replaces the previous.' }, { configKey: 'isGroupsAllowed', attribute: 'allow-groups', converter: toBool('default-true'), on: 'reinit', description: 'Allow grouping options under group headers.' }, { configKey: 'isCheckboxesShown', attribute: 'show-checkboxes', converter: toBool('default-true'), on: 'reinit', description: 'Show a checkbox on each option.' }, { configKey: 'isActionsSticky', attribute: 'sticky-actions', converter: toBool('default-true'), on: 'update', description: 'Keep the action bar pinned while the list scrolls.' }, { configKey: 'isPlacementLocked', attribute: 'lock-placement', converter: toBool('default-true'), on: 'update', description: 'Keep the dropdown initial placement instead of flipping when it fits.' }, { configKey: 'isSearchEnabled', attribute: 'enable-search', converter: toBool('default-true'), on: 'reinit', description: 'Show the search input.' }, { configKey: 'isKeepOptionsOnSearch', attribute: 'keep-options-on-search', converter: toBool('default-true'), on: 'update', description: 'Keep already-selected options visible while filtering.' }, { configKey: 'shouldKeepSearchOnClose', attribute: 'should-keep-search-on-close', converter: toBool('default-true'), on: 'update', description: 'Preserve the search text after the dropdown closes.' }, // ── Booleans (default false) ───────────────────────────────────────────── { configKey: 'isCloseOnSelect', attribute: 'close-on-select', converter: toBool('default-false'), on: 'update', description: 'Close the dropdown immediately after a selection.' }, { configKey: 'isAddNewAllowed', attribute: 'allow-add-new', converter: toBool('default-false'), on: 'reinit', description: 'Allow adding a new option from the search text.' }, { configKey: 'isCounterShown', attribute: 'show-counter', converter: toBool('default-false'), on: 'update', description: 'Show a selected-count indicator.' }, { configKey: 'isBadgeFullTitleShown', attribute: 'show-badge-full-title', converter: toBool('default-false'), on: 'update', description: 'Show the full title on badges instead of the short label.' }, { configKey: 'isVirtualScrollEnabled', attribute: 'enable-virtual-scroll', converter: toBool('default-false'), on: 'reinit', description: 'Force virtual scrolling on regardless of the threshold.' }, { configKey: 'isBadgeTooltipsEnabled', attribute: 'enable-badge-tooltips', converter: toBool('default-false'), on: 'update', description: 'Enable tooltips on badges.' }, { configKey: 'isOptionTooltipsEnabled', attribute: 'enable-option-tooltips', converter: toBool('default-false'), on: 'update', description: 'Enable tooltips on options.' }, { configKey: 'isOptionTooltipFollowCursor', attribute: 'option-tooltip-follow-cursor', converter: toBool('default-false'), on: 'update', description: 'Make option tooltips follow the pointer.' }, { configKey: 'isSearchModeToggleShown', attribute: 'show-search-mode-toggle', converter: toBool('default-false'), on: 'update', description: 'Show a clickable toggle in the phone fullscreen overlay search header that flips `search-mode` between `filter` and `navigate` live. Fullscreen-only; no effect in the floating presentation or when search is disabled.' }, // ── Special attributes ─────────────────────────────────────────────────── { configKey: 'initialValues', attribute: 'initial-values', converter: toInitialValues(), default: [], on: 'reinit', type: 'Array<string | number>', description: 'Values selected on first render. Accepts a JSON array (`[\"a\",\"b\"]`) or a bare CSV (`a,b,c`).' }, { configKey: 'showDebugInfo', attribute: 'show-debug-info', converter: toBool('default-false'), on: 'update', description: 'Render an in-component debug panel.', deprecated: 'Use per-instance logging (el.enableLogging()) instead.' }, // ── Complex property (data) ────────────────────────────────────────────── { configKey: 'options', converter: toObjectArray(), on: 'reinit', type: 'ReadonlyArray<Record<string, unknown>>', description: 'The array of option objects to render. The JS API — assign `el.options` directly. For HTML authoring use the `data-options` attribute (parsed per `data-options-format`) or declarative <option> children; both feed the same list and take precedence over this property in the order: <option> children > property > data-options.' }, { configKey: 'optionsSource', attribute: 'data-options', converter: toText({ isNullable: true }), on: 'reinit', type: 'string', description: 'HTML-authoring source for the option list, parsed per `data-options-format`. Reactive: changing either attribute re-renders. Prefer the `options` property in JS; a set `options` property and declarative <option> children both win over this.' }, { configKey: 'optionsFormat', attribute: 'data-options-format', converter: toEnum(OPTIONS_FORMATS, { default: 'json' }), on: 'reinit', type: \"'json' | 'csv' | 'plain'\", description: 'How to parse the `data-options` attribute: `json` (a JSON array of objects or [value, label] tuples), `csv` (rows split on `data-options-row-splitter`, cells on `data-options-splitter`; the first row is a header — map columns via *-member), or `plain` (bare values split on both splitters -> [value, label] tuples, value === label). Default `json`.' }, { configKey: 'optionsSplitter', attribute: 'data-options-splitter', converter: toText({ default: ',' }), on: 'reinit', type: 'string', description: 'Field/cell delimiter for the `csv` and `plain` `data-options` formats. Default `,`. Escapes `\\\\t` `\\\\n` `\\\\r` are honoured (e.g. `data-options-splitter=\"\\\\t\"` for TSV). Ignored for `json`.' }, { configKey: 'optionsRowSplitter', attribute: 'data-options-row-splitter', converter: toText({ default: '\\n' }), on: 'reinit', type: 'string', description: 'Row/record delimiter for the `csv` and `plain` `data-options` formats. Default newline. Escapes honoured (e.g. `data-options-row-splitter=\";\"` for single-line data). Ignored for `json`.' }, { configKey: 'actionButtons', converter: toValue({ validate: (v): v is unknown[] => Array.isArray(v) }), on: 'reinit', type: 'Array<Record<string, unknown>>', description: 'Custom action buttons for the dropdown footer/header. Property-only; when unset the default Select-All / Clear buttons apply.' }, // ── Callbacks: data shape (structural → reinit) ────────────────────────── { configKey: 'getValueCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string | number', description: 'Extract an option value (overrides valueMember).' }, { configKey: 'getPathCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string', description: 'Extract a node tree path (enables tree mode; overrides pathMember).' }, { configKey: 'getGroupCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string', description: 'Extract the group name from an option (overrides groupMember).' }, { configKey: 'getDisabledCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => boolean', description: 'Whether an option is disabled (overrides disabledMember).' }, { configKey: 'getIsSelectableCallback', converter: cb(), on: 'reinit', type: '(node: unknown) => boolean', description: 'Whether a tree node can be selected (overrides is-selectable-member).' }, { configKey: 'getSearchValueCallback', converter: cb(), on: 'reinit', type: '(item: unknown) => string', description: 'Text an option is searched against (overrides searchValueMember).' }, { configKey: 'searchCallback', converter: cb(), on: 'reinit', type: '(searchTerm: string, signal?: AbortSignal) => Promise<unknown[]>', description: 'Custom / async search; return the filtered options.' }, // ── Callbacks: display / render (cosmetic → update) ────────────────────── { configKey: 'getDisplayValueCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Compute the display label for an option (overrides displayValueMember).' }, { configKey: 'getBadgeDisplayCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Compute the text shown on an option badge.' }, { configKey: 'getBadgeClassCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | string[]', description: 'Extra CSS class(es) for an option badge.' }, { configKey: 'getIconCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Icon for an option (overrides iconMember).' }, { configKey: 'getSubtitleCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Subtitle for an option (overrides subtitleMember).' }, { configKey: 'getFullTitleCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Full title for an option (used by badges when show-badge-full-title is on).' }, { configKey: 'getCounterCallback', converter: cb(), on: 'update', type: '(count: number, moreCount?: number) => string', description: 'Render the selected-count label.' }, { configKey: 'getValueFormatCallback', converter: cb(), on: 'update', type: '(selectedValues: (string | number)[]) => string', description: 'Serialize the selected values for form submission.' }, { configKey: 'getBadgeTooltipCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | HTMLElement', description: 'Tooltip content for an option badge.' }, { configKey: 'getOptionTooltipCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | HTMLElement', description: 'Tooltip content for an option row.' }, { configKey: 'getRemoveButtonTooltipCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Tooltip text for a badge remove button.' }, { configKey: 'getSelectedItemClassCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | string[]', description: 'Extra CSS class(es) for a selected item.' }, { configKey: 'renderOptionContentCallback', converter: cb(), on: 'update', type: '(item: unknown, context: OptionContentRenderContext) => string | HTMLElement', description: 'Custom render for an option row; may return HTML or an element.' }, { configKey: 'renderBadgeContentCallback', converter: cb(), on: 'update', type: '(item: unknown, context: BadgeContentRenderContext) => string | HTMLElement', description: 'Custom render for a badge; may return HTML or an element.' }, { configKey: 'renderGroupLabelContentCallback', converter: cb(), on: 'update', type: '(groupName: string) => string | HTMLElement', description: 'Customize a group label; may return an HTML string or element.' }, { configKey: 'renderSelectedContentCallback', converter: cb(), on: 'update', type: '(item: unknown) => string', description: 'Custom render for the whole selected area.' }, { configKey: 'renderSelectedItemContentCallback', converter: cb(), on: 'update', type: '(item: unknown) => string | HTMLElement', description: 'Custom render for one selected item.' }, { configKey: 'customStylesCallback', converter: cb(), on: 'update', type: '() => string', description: 'Returns a CSS string injected into the component via a replaceable style slot (§12.8).' }, // ── Callbacks: before-hooks (behavior-shaping) ─────────────────────────── { configKey: 'beforeSearchCallback', converter: cb(), on: 'update', type: '(searchTerm: string) => string | null', description: 'Runs before a search; return a rewritten term or null to veto.' }, { configKey: 'beforeSelectCallback', converter: cb(), on: 'update', type: '(option: unknown, selectedOptions: unknown[]) => boolean | string | void', description: 'Runs before selecting; return false to veto, or a string to veto and show it as a message.' }, { configKey: 'beforeDeselectCallback', converter: cb(), on: 'update', type: '(option: unknown, selectedOptions: unknown[]) => boolean | string | void', description: 'Runs before deselecting; return false to veto, or a string to veto and show it as a message.' }, { configKey: 'addNewCallback', converter: cb(), on: 'update', type: '(value: string) => unknown | Promise<unknown>', description: 'Create a new option from the typed text.' }, ]",
3161
3297
  "type": {
3162
3298
  "text": "readonly InputDef[]"
3163
3299
  }
@@ -3692,11 +3828,10 @@
3692
3828
  "name": "searchPlaceholder",
3693
3829
  "privacy": "public",
3694
3830
  "type": {
3695
- "text": "string"
3831
+ "text": "string | null"
3696
3832
  },
3697
- "default": "'Search...'",
3698
3833
  "attribute": "search-placeholder",
3699
- "description": "Placeholder text for the search input."
3834
+ "description": "Placeholder text for the search input. When unset it defaults to \"Search...\"; if `show-search-mode-toggle` is on, the default instead becomes mode-aware (\"Search…\" in navigate, \"Filter…\" in filter). An explicit value always wins and stays fixed."
3700
3835
  },
3701
3836
  {
3702
3837
  "kind": "field",
@@ -4442,6 +4577,16 @@
4442
4577
  "attribute": "option-tooltip-follow-cursor",
4443
4578
  "description": "Make option tooltips follow the pointer."
4444
4579
  },
4580
+ {
4581
+ "kind": "field",
4582
+ "name": "isSearchModeToggleShown",
4583
+ "privacy": "public",
4584
+ "type": {
4585
+ "text": "boolean"
4586
+ },
4587
+ "attribute": "show-search-mode-toggle",
4588
+ "description": "Show a clickable toggle in the phone fullscreen overlay search header that flips `search-mode` between `filter` and `navigate` live. Fullscreen-only; no effect in the floating presentation or when search is disabled."
4589
+ },
4445
4590
  {
4446
4591
  "kind": "field",
4447
4592
  "name": "initialValues",
@@ -4804,10 +4949,9 @@
4804
4949
  "name": "search-placeholder",
4805
4950
  "fieldName": "searchPlaceholder",
4806
4951
  "type": {
4807
- "text": "string"
4952
+ "text": "string | null"
4808
4953
  },
4809
- "default": "'Search...'",
4810
- "description": "Placeholder text for the search input."
4954
+ "description": "Placeholder text for the search input. When unset it defaults to \"Search...\"; if `show-search-mode-toggle` is on, the default instead becomes mode-aware (\"Search…\" in navigate, \"Filter…\" in filter). An explicit value always wins and stays fixed."
4811
4955
  },
4812
4956
  {
4813
4957
  "name": "select-placeholder",
@@ -5389,6 +5533,14 @@
5389
5533
  },
5390
5534
  "description": "Make option tooltips follow the pointer."
5391
5535
  },
5536
+ {
5537
+ "name": "show-search-mode-toggle",
5538
+ "fieldName": "isSearchModeToggleShown",
5539
+ "type": {
5540
+ "text": "boolean"
5541
+ },
5542
+ "description": "Show a clickable toggle in the phone fullscreen overlay search header that flips `search-mode` between `filter` and `navigate` live. Fullscreen-only; no effect in the floating presentation or when search is disabled."
5543
+ },
5392
5544
  {
5393
5545
  "name": "initial-values",
5394
5546
  "fieldName": "initialValues",
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { InputDef } from '@keenmate/web-components-core';
4
4
  import { Logger } from '@keenmate/web-components-core';
5
5
  import { LogLevelDesc } from '@keenmate/web-components-core';
6
6
  import { Placement } from '@keenmate/web-components-core/positioning';
7
+ import { PresentationContext } from '@keenmate/web-components-core';
7
8
 
8
9
  /**
9
10
  * Action button configuration for dropdown actions (Select All, Clear All, custom actions)
@@ -59,7 +60,7 @@ declare type ActionsPosition = 'top' | 'bottom';
59
60
  /**
60
61
  * Context provided to renderBadgeContentCallback
61
62
  */
62
- declare interface BadgeContentRenderContext {
63
+ declare interface BadgeContentRenderContext extends PresentationContext {
63
64
  /** Current badges display mode */
64
65
  displayMode: BadgesDisplayMode;
65
66
  /** Whether the badge is being rendered in the selected items popover */
@@ -387,6 +388,17 @@ declare interface MultiSelectConfig<T = any> {
387
388
  * - `navigate` — keep all options visible and jump focus to matches
388
389
  */
389
390
  searchMode?: SearchMode;
391
+ /**
392
+ * Show a clickable mode toggle in the phone fullscreen overlay's search header that
393
+ * flips `searchMode` between `filter` and `navigate` live (no reopen). Default `false`.
394
+ *
395
+ * The overlay has room for the affordance and touch users can't reach the desktop
396
+ * `Ctrl`+`Arrow` match-stepping, so this exposes both modes on the device where it
397
+ * matters most. The toggle sits at the leading edge of the search field; its icon
398
+ * reflects the current mode (magnifier = navigate, funnel = filter). No effect in the
399
+ * floating presentation or when search is disabled/hidden.
400
+ */
401
+ isSearchModeToggleShown?: boolean;
390
402
  /**
391
403
  * Layout mode for the action buttons. Default: `nowrap`.
392
404
  *
@@ -647,9 +659,15 @@ export declare interface MultiSelectOptions extends MultiSelectConfig<MultiSelec
647
659
  declare type NodeId = string | number;
648
660
 
649
661
  /**
650
- * Context provided to renderOptionContentCallback
662
+ * Context provided to renderOptionContentCallback.
663
+ *
664
+ * Extends the shared {@link PresentationContext} from `@keenmate/web-components-core`, so it
665
+ * also carries `presentation` (`'floating' | 'modal' | 'fullscreen'` — this component only ever
666
+ * emits `floating`/`fullscreen`), `isFullscreen`, and `isModal`. Branch on `isFullscreen` to
667
+ * render leaner content in the phone overlay. Reactive: swapping presentation re-renders and
668
+ * re-invokes the callback with the new value.
651
669
  */
652
- declare interface OptionContentRenderContext {
670
+ declare interface OptionContentRenderContext extends PresentationContext {
653
671
  /** Index of the option in the filtered list */
654
672
  index: number;
655
673
  /** Whether the option is currently selected */
@@ -778,11 +796,13 @@ export declare class WebMultiSelect<T = any> {
778
796
  private fullscreenHeader;
779
797
  private fullscreenSearchInput;
780
798
  private fullscreenSearchClear;
799
+ private fullscreenModeToggle;
781
800
  private fullscreenNav;
782
801
  private fullscreenNavCount;
783
802
  private fullscreenNavPrev;
784
803
  private fullscreenNavNext;
785
804
  private bodyScrollUnlock;
805
+ private overflowXClamp;
786
806
  private keyboardInsetCleanup;
787
807
  private overlayHistoryActive;
788
808
  private readonly onOverlayPopstate;
@@ -916,6 +936,16 @@ export declare class WebMultiSelect<T = any> {
916
936
  * here. Pass all options to show the whole tree.
917
937
  */
918
938
  private rebuildTreeVisibleFromMatches;
939
+ /**
940
+ * Tree + `search-mode="navigate"`: keep the ENTIRE tree visible (the tree is always
941
+ * fully expanded, so `flatNodes` is the whole thing) and record which visible rows
942
+ * match the term in `matchingIndices` — the flat-list navigate behavior, but over
943
+ * `treeNodes`. Filter mode collapses the hierarchy to matches + ancestors; navigate
944
+ * mode instead leaves the structure intact so the user can jump between matches
945
+ * (Ctrl+Arrow on desktop, the fullscreen navigator on touch). Returns the index of
946
+ * the first match, or -1 (no term / no matches), so the caller can set focus.
947
+ */
948
+ private rebuildTreeVisibleForNavigate;
919
949
  /**
920
950
  * (Re)compute `isRTL` from the host's `dir` (or an RTL ancestor) and derive the
921
951
  * direction-mirrored badges position. Pure state — callers apply the DOM effects
@@ -988,6 +1018,16 @@ export declare class WebMultiSelect<T = any> {
988
1018
  * search is unusable → the search placeholder.
989
1019
  */
990
1020
  private getPlaceholderText;
1021
+ /**
1022
+ * The search field placeholder. An explicit `searchPlaceholder` always wins and stays
1023
+ * fixed. Otherwise the default is "Search..." — except when the in-overlay mode toggle
1024
+ * is enabled (`isSearchModeToggleShown`), where it becomes mode-aware so the field labels
1025
+ * the current behavior: "Search…" in navigate mode, "Filter…" in filter mode. Refreshed
1026
+ * on a live mode switch (see setSearchModeLive → refreshSearchPlaceholder).
1027
+ */
1028
+ private getSearchPlaceholder;
1029
+ /** Re-apply the (possibly mode-aware) placeholder to the live inputs after a mode switch. */
1030
+ private refreshSearchPlaceholder;
991
1031
  private renderBadges;
992
1032
  private attachEvents;
993
1033
  private handleSearch;
@@ -1098,6 +1138,26 @@ export declare class WebMultiSelect<T = any> {
1098
1138
  private lockBodyScroll;
1099
1139
  /** Restore page scroll (no-op if it wasn't locked). */
1100
1140
  private unlockBodyScroll;
1141
+ /**
1142
+ * Clip the host document's horizontal overflow while a fullscreen sheet is open.
1143
+ *
1144
+ * A page that overflows horizontally (e.g. an unbreakable-wide token in a heading)
1145
+ * makes the mobile browser SHRINK-TO-FIT: it zooms the page out so the overflow fits,
1146
+ * which desyncs the visual viewport from the layout viewport. Our fullscreen sheet is
1147
+ * `position: fixed` — anchored to the LAYOUT viewport — so under that zoom it no longer
1148
+ * lands flush against the physical screen edges, and the top slips under the system bar
1149
+ * (looks like "the bar covers the sheet"). This is NOT a safe-area problem; safe-area
1150
+ * insets are 0 in that state. Clamping `overflow-x: hidden` on <html>/<body> removes the
1151
+ * overflow, so the browser drops the zoom and the sheet sits flush. Complements
1152
+ * lockBodyScroll() (vertical axis); the saved inline value is restored on close.
1153
+ *
1154
+ * Only <html> is touched (not <body>): clipping the root's horizontal overflow is
1155
+ * enough to collapse the scrollWidth and cancel the shrink-to-fit, and it avoids
1156
+ * conflicting with core's lockBodyScroll(), which owns <body>'s `overflow`. Idempotent.
1157
+ */
1158
+ private clampDocumentOverflowX;
1159
+ /** Restore the <html> `overflow-x` clamped by clampDocumentOverflowX() (no-op if unset). */
1160
+ private releaseDocumentOverflowX;
1101
1161
  /**
1102
1162
  * While the fullscreen dropdown is open, keep it sitting above the soft keyboard.
1103
1163
  * Delegates to core's `observeKeyboardInset` (which tracks `window.visualViewport`
@@ -1168,6 +1228,26 @@ export declare class WebMultiSelect<T = any> {
1168
1228
  * header survives re-renders.
1169
1229
  */
1170
1230
  private buildFullscreenHeader;
1231
+ /** Build the navigate-mode match navigator (count + prev/next) and append it to the
1232
+ * fullscreen header, once. No-op if already built or the header isn't present. The
1233
+ * nav wraps onto its own full-width row under the search box (header is flex-wrap;
1234
+ * the nav takes 100% basis). */
1235
+ private ensureFullscreenNav;
1236
+ /** Remove the match navigator (switching to filter mode, which has no jump UI). */
1237
+ private removeFullscreenNav;
1238
+ /** Flip searchMode filter<->navigate from the in-overlay toggle. */
1239
+ private toggleSearchModeLive;
1240
+ /**
1241
+ * Switch searchMode in place — the overlay's toggle path. The `search-mode` attribute
1242
+ * is reinit-on-change (it rebuilds and closes the overlay); this instead mutates the
1243
+ * live config, adds/removes the match navigator to match, and re-projects the current
1244
+ * term under the new mode (filter narrows the list / navigate keeps all + highlights),
1245
+ * all without tearing the open sheet down. Focus stays on the search field.
1246
+ */
1247
+ private setSearchModeLive;
1248
+ /** Sync the mode toggle's icon (via data-mode) and labels with the current searchMode.
1249
+ * No-op when the toggle isn't built (opt-out, floating panel, or search hidden). */
1250
+ private updateFullscreenModeToggle;
1171
1251
  /**
1172
1252
  * Sync the fullscreen match navigator (navigate mode only) with the current search
1173
1253
  * state: hide it until there's a term, then show "N of M" while a match is focused