@ixfx/components 0.7.4 → 0.7.5

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.
Files changed (73) hide show
  1. package/bundle/index.d.ts +536 -178
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +1324 -442
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-token.js +1 -1
  6. package/dist/crumbs.d.ts +5 -1
  7. package/dist/crumbs.d.ts.map +1 -1
  8. package/dist/crumbs.js +10 -0
  9. package/dist/crumbs.js.map +1 -1
  10. package/dist/{data-grid-BddUjAti.d.ts → data-grid-CtJ35v8L.d.ts} +26 -1
  11. package/dist/data-grid-CtJ35v8L.d.ts.map +1 -0
  12. package/dist/data-grid.d.ts +1 -1
  13. package/dist/data-grid.js +88 -7
  14. package/dist/data-grid.js.map +1 -1
  15. package/dist/{detail-list-Br0WAnry.d.ts → detail-list-BS5dRDda.d.ts} +20 -12
  16. package/dist/detail-list-BS5dRDda.d.ts.map +1 -0
  17. package/dist/detail-list.d.ts +2 -2
  18. package/dist/detail-list.js +51 -135
  19. package/dist/detail-list.js.map +1 -1
  20. package/dist/{grid-list-B_orPsot.d.ts → grid-list-D-GbghSH.d.ts} +18 -1
  21. package/dist/grid-list-D-GbghSH.d.ts.map +1 -0
  22. package/dist/grid-list.d.ts +1 -1
  23. package/dist/grid-list.js +76 -15
  24. package/dist/grid-list.js.map +1 -1
  25. package/dist/incr-search-DpbSJtgj.js +962 -0
  26. package/dist/incr-search-DpbSJtgj.js.map +1 -0
  27. package/dist/incr-search.d.ts +2 -2
  28. package/dist/incr-search.js +2 -3
  29. package/dist/index-CBS4z1v-.d.ts +365 -0
  30. package/dist/index-CBS4z1v-.d.ts.map +1 -0
  31. package/dist/index.d.ts +8 -8
  32. package/dist/index.js +4 -5
  33. package/dist/index.js.map +1 -1
  34. package/dist/{list-selection-controller-CUo-Hhcm.js → list-selection-controller-DTFI40li.js} +2 -76
  35. package/dist/list-selection-controller-DTFI40li.js.map +1 -0
  36. package/dist/miller.d.ts +81 -3
  37. package/dist/miller.d.ts.map +1 -1
  38. package/dist/miller.js +327 -68
  39. package/dist/miller.js.map +1 -1
  40. package/dist/narrowed-text.js +1 -2
  41. package/dist/narrowed-text.js.map +1 -1
  42. package/dist/{tree-B74zdI60.js → tree-DAii2A1g.js} +77 -15
  43. package/dist/tree-DAii2A1g.js.map +1 -0
  44. package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
  45. package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
  46. package/dist/tree.d.ts +22 -1
  47. package/dist/tree.d.ts.map +1 -1
  48. package/dist/tree.js +1 -1
  49. package/dist/{vertical-list-CQSEVeOT.d.ts → vertical-list-DwtCPy1b.d.ts} +17 -14
  50. package/dist/vertical-list-DwtCPy1b.d.ts.map +1 -0
  51. package/dist/vertical-list.d.ts +1 -1
  52. package/dist/vertical-list.js +59 -127
  53. package/dist/vertical-list.js.map +1 -1
  54. package/docs-user/data-grid.md +19 -0
  55. package/docs-user/detail-list.md +1 -1
  56. package/docs-user/grid-list.md +24 -0
  57. package/docs-user/incr-search.md +12 -2
  58. package/docs-user/index.json +2 -2
  59. package/docs-user/tree.md +22 -1
  60. package/docs-user/vertical-list.md +7 -1
  61. package/package.json +1 -1
  62. package/dist/data-grid-BddUjAti.d.ts.map +0 -1
  63. package/dist/detail-list-Br0WAnry.d.ts.map +0 -1
  64. package/dist/element-search-Y8SRfPGM.js +0 -185
  65. package/dist/element-search-Y8SRfPGM.js.map +0 -1
  66. package/dist/grid-list-B_orPsot.d.ts.map +0 -1
  67. package/dist/incr-search-vCzuao-2.js +0 -137
  68. package/dist/incr-search-vCzuao-2.js.map +0 -1
  69. package/dist/index-Cu5i8zZY.d.ts +0 -160
  70. package/dist/index-Cu5i8zZY.d.ts.map +0 -1
  71. package/dist/list-selection-controller-CUo-Hhcm.js.map +0 -1
  72. package/dist/tree-B74zdI60.js.map +0 -1
  73. package/dist/vertical-list-CQSEVeOT.d.ts.map +0 -1
package/bundle/index.js CHANGED
@@ -2400,14 +2400,15 @@ function createIncrSearch(options = {}) {
2400
2400
  }
2401
2401
  //#endregion
2402
2402
  //#region src/incr-search/controller-tree.ts
2403
- const DEFAULT_HIGHLIGHT_KEY = `incr-search-highlight`;
2403
+ const DEFAULT_HIGHLIGHT_KEY$1 = `incr-search-highlight`;
2404
2404
  /**
2405
- * Incremental-search controller for any component implementing `TreeSearchHost`
2406
- * (i.e. `ixfx-tree-list`, `ixfx-miller-list`, `ixfx-crumb-navigation`).
2405
+ * External-input filter-search controller for `TreeSearchHost` components.
2407
2406
  *
2408
- * Replaces the previous separate `IncrSearchTreeController` / `IncrSearchMillerController`
2409
- * classes the unified `TreeFilterPredicate` (with depth) means one implementation works
2410
- * for all tree-like components.
2407
+ * @deprecated `ixfx-tree-list` / `ixfx-miller-list` now have a built-in
2408
+ * incremental-search popover with navigate / select / filter modes invoke
2409
+ * `el.registry.invoke('list.search.filter')` (see `docs/infra-incremental.md`)
2410
+ * instead of wiring an external `<input>`. This class remains for existing
2411
+ * consumers and covers filter mode only.
2411
2412
  *
2412
2413
  * @example Using with ixfx-tree-list:
2413
2414
  * ```typescript
@@ -2461,7 +2462,7 @@ var IncrSearchTreeController = class {
2461
2462
  this.#opts.input.removeEventListener(`keydown`, this.#onKeydown);
2462
2463
  }
2463
2464
  this.#el.filterPredicate = void 0;
2464
- clearHighlights({ key: this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY });
2465
+ clearHighlights({ key: this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY$1 });
2465
2466
  this.#labelMap.clear();
2466
2467
  }
2467
2468
  async setQuery(query) {
@@ -2484,7 +2485,7 @@ var IncrSearchTreeController = class {
2484
2485
  const matched = new Set(state.results.map((r) => r.item.node.item.key));
2485
2486
  const hasQuery = state.query.trim().length > 0;
2486
2487
  this.#el.filterPredicate = hasQuery && matched.size > 0 ? (node, _depth) => matched.has(node.item.key) : void 0;
2487
- const key = this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY;
2488
+ const key = this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY$1;
2488
2489
  if (!this.#el.shadowRoot) return;
2489
2490
  if (!state.query.trim()) {
2490
2491
  clearHighlights({ key });
@@ -2498,11 +2499,149 @@ var IncrSearchTreeController = class {
2498
2499
  }
2499
2500
  };
2500
2501
  //#endregion
2502
+ //#region src/interaction/keyed-list.ts
2503
+ /**
2504
+ * Helpers for key-based selection on the Element-flavoured list components
2505
+ * (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
2506
+ *
2507
+ * A "key" is a stable string the host puts on each item element — `data-key`
2508
+ * (preferred) or `data-value`. It survives the host rebuilding the item set
2509
+ * (re-fetch, re-sort, filter) where raw `Element` references do not.
2510
+ */
2511
+ /**
2512
+ * The stable key for an item element: `data-key`, then `data-value`, else
2513
+ * `undefined`.
2514
+ */
2515
+ function keyForElement(el) {
2516
+ return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
2517
+ }
2518
+ /**
2519
+ * A lazily-built key → element index over a container. Rebuilt on first use
2520
+ * and after any child-list mutation under `root`; `invalidate()` forces a
2521
+ * rebuild on the next `get()`.
2522
+ *
2523
+ * ```ts
2524
+ * #keyIndex = new KeyedElementIndex(this);
2525
+ * // ...
2526
+ * const el = this.#keyIndex.get(`row-42`);
2527
+ * // on disconnect:
2528
+ * this.#keyIndex.dispose();
2529
+ * ```
2530
+ */
2531
+ var KeyedElementIndex = class {
2532
+ #root;
2533
+ #selector;
2534
+ #map = /* @__PURE__ */ new Map();
2535
+ #dirty = true;
2536
+ #observer;
2537
+ /**
2538
+ * @param root Container whose descendants are indexed.
2539
+ * @param itemSelector Which descendants count as items (default: any `[data-key]`).
2540
+ */
2541
+ constructor(root, itemSelector = `[data-key]`) {
2542
+ this.#root = root;
2543
+ this.#selector = itemSelector;
2544
+ this.#observer = new MutationObserver(() => {
2545
+ this.#dirty = true;
2546
+ });
2547
+ this.#observer.observe(root, {
2548
+ childList: true,
2549
+ subtree: true
2550
+ });
2551
+ }
2552
+ /** The element with this key, or `undefined`. First match wins on duplicates. */
2553
+ get(key) {
2554
+ if (this.#dirty) this.#rebuild();
2555
+ return this.#map.get(key);
2556
+ }
2557
+ /** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
2558
+ invalidate() {
2559
+ this.#dirty = true;
2560
+ }
2561
+ /** Stop observing and drop the cache. */
2562
+ dispose() {
2563
+ this.#observer.disconnect();
2564
+ this.#map.clear();
2565
+ }
2566
+ #rebuild() {
2567
+ this.#map.clear();
2568
+ for (const el of this.#root.querySelectorAll(this.#selector)) {
2569
+ const key = keyForElement(el);
2570
+ if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
2571
+ }
2572
+ this.#dirty = false;
2573
+ }
2574
+ };
2575
+ //#endregion
2576
+ //#region src/incr-search/element-list-search-adapter.ts
2577
+ function defaultLabel(el) {
2578
+ return el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``;
2579
+ }
2580
+ /**
2581
+ * A {@link ListSearchAdapter} over the light-DOM item elements of a list
2582
+ * component. Filter mode toggles `hidden` (only on elements it hid itself, so a
2583
+ * consumer's own `hidden` items are left alone). Highlights are painted on the
2584
+ * item elements in the light DOM — the consuming page styles
2585
+ * `::highlight(<highlightKey>)`. See `docs/infra-incremental.md`.
2586
+ */
2587
+ function createElementListSearchAdapter(host, options = {}) {
2588
+ const itemSelector = options.itemSelector ?? `:scope > *`;
2589
+ const getLabel = options.getLabel ?? defaultLabel;
2590
+ const getKey = options.getKey ?? ((el) => keyForElement(el) ?? defaultLabel(el));
2591
+ const hidden = /* @__PURE__ */ new Set();
2592
+ const items = () => Array.from(host.querySelectorAll(itemSelector));
2593
+ return {
2594
+ getItems: items,
2595
+ getLabel,
2596
+ getKey,
2597
+ focusItem: (el) => {
2598
+ if (el) host.focusKey(getKey(el));
2599
+ },
2600
+ activateItem: (el) => {
2601
+ el.dispatchEvent(new MouseEvent(`click`, {
2602
+ bubbles: true,
2603
+ composed: true
2604
+ }));
2605
+ },
2606
+ selectItems: (els) => {
2607
+ host.selectKeys(els.map(getKey));
2608
+ },
2609
+ applyFilter: (matched) => {
2610
+ if (matched === void 0) {
2611
+ for (const el of hidden) el.hidden = false;
2612
+ hidden.clear();
2613
+ return;
2614
+ }
2615
+ for (const el of items()) if (matched.has(getKey(el))) {
2616
+ if (hidden.delete(el)) el.hidden = false;
2617
+ } else {
2618
+ el.hidden = true;
2619
+ hidden.add(el);
2620
+ }
2621
+ },
2622
+ highlightRoot: () => host,
2623
+ highlightSelector: itemSelector,
2624
+ highlightKeyOf: (el) => getKey(el),
2625
+ highlightText: getLabel,
2626
+ highlightKey: options.highlightKey,
2627
+ container: () => host
2628
+ };
2629
+ }
2630
+ //#endregion
2501
2631
  //#region src/incr-search/element-search.ts
2502
2632
  const defaultGetLabel = (el) => el.textContent?.trim() ?? ``;
2503
2633
  function defaultGetKey(el) {
2504
2634
  return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? el.textContent?.trim() ?? ``;
2505
2635
  }
2636
+ /**
2637
+ * DOM filter-search over a container's item elements (sets `hidden` on
2638
+ * non-matches, applies highlights).
2639
+ *
2640
+ * @deprecated The list components (`ixfx-grid-list`, `ixfx-vertical-list`,
2641
+ * `ixfx-detail-list`, …) now have a built-in navigate / select / filter search
2642
+ * popover — see `docs/infra-incremental.md` and `ListSearchController`. Kept for
2643
+ * standalone / non-component use.
2644
+ */
2506
2645
  function createElementIncrSearch(opts) {
2507
2646
  const { container, itemSelector, getLabel = defaultGetLabel, getKey = defaultGetKey, labelSelector, highlightKey } = opts;
2508
2647
  let elementByKey = /* @__PURE__ */ new Map();
@@ -2576,6 +2715,512 @@ function createElementIncrSearch(opts) {
2576
2715
  };
2577
2716
  }
2578
2717
  //#endregion
2718
+ //#region src/incr-search/list-search-commands.ts
2719
+ /** Command ids registered by {@link registerListSearchCommands}, keyed by mode. */
2720
+ const LIST_SEARCH_COMMAND_IDS = {
2721
+ navigate: `list.search.navigate`,
2722
+ select: `list.search.select`,
2723
+ filter: `list.search.filter`
2724
+ };
2725
+ const LABELS = {
2726
+ navigate: {
2727
+ label: `Find`,
2728
+ description: `Type to move the cursor to the best match`
2729
+ },
2730
+ select: {
2731
+ label: `Find & Select`,
2732
+ description: `Type to select every matching item`
2733
+ },
2734
+ filter: {
2735
+ label: `Filter`,
2736
+ description: `Type to hide items that don't match`
2737
+ }
2738
+ };
2739
+ /**
2740
+ * Register one command per mode the controller offers (see
2741
+ * `controller.registeredModes`). Each command opens the search popover in its
2742
+ * mode. Returns a function that unregisters them again — call it from
2743
+ * `disconnectedCallback`. See `docs/infra-incremental.md`.
2744
+ */
2745
+ function registerListSearchCommands(registry, controller, options = {}) {
2746
+ const id = (mode) => options.idPrefix ? `${options.idPrefix}.search.${mode}` : LIST_SEARCH_COMMAND_IDS[mode];
2747
+ const registered = [];
2748
+ for (const mode of controller.registeredModes) {
2749
+ const commandId = id(mode);
2750
+ if (registry.has(commandId)) continue;
2751
+ registry.register({
2752
+ id: commandId,
2753
+ label: LABELS[mode].label,
2754
+ description: LABELS[mode].description,
2755
+ execute: () => controller.open(mode)
2756
+ });
2757
+ registered.push(commandId);
2758
+ }
2759
+ return () => {
2760
+ for (const commandId of registered) registry.unregister(commandId);
2761
+ };
2762
+ }
2763
+ //#endregion
2764
+ //#region src/incr-search/list-search-controller.ts
2765
+ /** Canonical order — used for the switch-row and to normalise a requested set. */
2766
+ const LIST_SEARCH_MODES = [
2767
+ `navigate`,
2768
+ `select`,
2769
+ `filter`
2770
+ ];
2771
+ const DEFAULT_HIGHLIGHT_KEY = `incr-search-highlight`;
2772
+ const INPUT_CLASS = `list-search-input`;
2773
+ /**
2774
+ * Shared brain for the three-mode incremental-search popover. A Lit
2775
+ * `ReactiveController`: the host adds `listSearchStyles` to `static styles`,
2776
+ * drops `${controller.renderField()}` into `render()`, and registers the
2777
+ * commands with {@link registerListSearchCommands}. See `docs/infra-incremental.md`.
2778
+ */
2779
+ var ListSearchController = class {
2780
+ #host;
2781
+ #adapter;
2782
+ #modes;
2783
+ #placeholder;
2784
+ #search;
2785
+ #unsub;
2786
+ #observer;
2787
+ #itemByKey = /* @__PURE__ */ new Map();
2788
+ #open = false;
2789
+ #mode = `filter`;
2790
+ #query = ``;
2791
+ #matchIndex = 0;
2792
+ #matches = [];
2793
+ /** Whether a filter has been pushed to the adapter and still needs undoing. */
2794
+ #filterActive = false;
2795
+ constructor(host, options) {
2796
+ this.#host = host;
2797
+ host.addController(this);
2798
+ this.#adapter = options.adapter;
2799
+ const requested = new Set(options.modes ?? LIST_SEARCH_MODES);
2800
+ this.#modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
2801
+ this.#mode = this.#modes[0] ?? `filter`;
2802
+ this.#placeholder = options.placeholder ?? `Search…`;
2803
+ this.#search = createIncrSearch({
2804
+ selector: (i) => i.label,
2805
+ fzf: options.fzf
2806
+ });
2807
+ this.#unsub = this.#search.onChange((state) => this.#onState(state));
2808
+ }
2809
+ /** `false` when no modes are available — the host should register no commands and render no field. */
2810
+ get enabled() {
2811
+ return this.#modes.length > 0;
2812
+ }
2813
+ /** The modes actually offered (component capability ∩ consumer request). */
2814
+ get registeredModes() {
2815
+ return this.#modes;
2816
+ }
2817
+ /**
2818
+ * Replace the offered mode set (normalised to {@link LIST_SEARCH_MODES}
2819
+ * order). If the current mode is dropped the popover closes and falls back to
2820
+ * the first remaining mode. Callers should re-run
2821
+ * {@link registerListSearchCommands} afterwards.
2822
+ */
2823
+ setModes(modes) {
2824
+ const set = new Set(modes);
2825
+ this.#modes = LIST_SEARCH_MODES.filter((m) => set.has(m));
2826
+ if (!this.#modes.includes(this.#mode)) {
2827
+ if (this.#open) this.close();
2828
+ this.#mode = this.#modes[0] ?? `filter`;
2829
+ }
2830
+ this.#host.requestUpdate();
2831
+ }
2832
+ get isOpen() {
2833
+ return this.#open;
2834
+ }
2835
+ get mode() {
2836
+ return this.#mode;
2837
+ }
2838
+ get query() {
2839
+ return this.#query;
2840
+ }
2841
+ get matchCount() {
2842
+ return this.#matches.length;
2843
+ }
2844
+ get matchIndex() {
2845
+ return this.#matchIndex;
2846
+ }
2847
+ hostConnected() {
2848
+ const container = this.#adapter.container?.();
2849
+ if (container) {
2850
+ this.#observer = new MutationObserver(() => this.refresh());
2851
+ this.#observer.observe(container, {
2852
+ childList: true,
2853
+ subtree: false
2854
+ });
2855
+ }
2856
+ this.refresh();
2857
+ }
2858
+ hostDisconnected() {
2859
+ this.#observer?.disconnect();
2860
+ this.#observer = void 0;
2861
+ if (this.#open) this.#teardownEffects();
2862
+ this.#open = false;
2863
+ this.#unsub?.();
2864
+ this.#unsub = void 0;
2865
+ this.#search.dispose();
2866
+ }
2867
+ /** Open the popover in `mode` (clamped to the available set). */
2868
+ open(mode) {
2869
+ if (!this.enabled) return;
2870
+ this.#mode = this.#modes.includes(mode) ? mode : this.#modes[0];
2871
+ this.#open = true;
2872
+ this.#query = ``;
2873
+ this.#matchIndex = 0;
2874
+ this.#matches = [];
2875
+ this.refresh();
2876
+ this.#search.setQuery(``);
2877
+ this.#host.requestUpdate();
2878
+ this.#host.updateComplete.then(() => this.#focusInput());
2879
+ }
2880
+ /** Close the popover, restoring any live filter. Selection / cursor stay put. */
2881
+ close() {
2882
+ if (!this.#open) return;
2883
+ this.#open = false;
2884
+ this.#teardownEffects();
2885
+ this.#query = ``;
2886
+ this.#matches = [];
2887
+ this.#search.clearQuery();
2888
+ this.#host.requestUpdate();
2889
+ this.#host.focus();
2890
+ }
2891
+ setQuery(query) {
2892
+ this.#query = query;
2893
+ this.#matchIndex = 0;
2894
+ this.#search.setQuery(query);
2895
+ }
2896
+ /** Re-interpret the current query under a new mode (undoing the old mode's effect). */
2897
+ switchMode(next) {
2898
+ if (!this.#modes.includes(next) || next === this.#mode) return;
2899
+ if (this.#mode === `filter` && this.#filterActive) {
2900
+ this.#adapter.applyFilter(void 0);
2901
+ this.#filterActive = false;
2902
+ }
2903
+ this.#mode = next;
2904
+ this.#applyMode();
2905
+ this.#host.requestUpdate();
2906
+ this.#host.updateComplete.then(() => this.#focusInput());
2907
+ }
2908
+ /** One-shot: turn the current match set into the selection, staying in the current mode. */
2909
+ selectMatches() {
2910
+ if (!this.#modes.includes(`select`)) return;
2911
+ this.#adapter.selectItems(this.#currentItems());
2912
+ this.#host.requestUpdate();
2913
+ }
2914
+ nextMatch() {
2915
+ this.#cycle(1);
2916
+ }
2917
+ prevMatch() {
2918
+ this.#cycle(-1);
2919
+ }
2920
+ /** Enter: in `navigate` activate the current match; then close. */
2921
+ commit() {
2922
+ if (this.#mode === `navigate`) {
2923
+ const item = this.#itemFor(this.#matches[this.#matchIndex]);
2924
+ if (item) {
2925
+ if (this.#adapter.activateItem) this.#adapter.activateItem(item);
2926
+ else this.#adapter.focusItem(item);
2927
+ }
2928
+ }
2929
+ this.close();
2930
+ }
2931
+ /** Rebuild the searchable item set from the adapter. Called on open and on container mutation. */
2932
+ refresh() {
2933
+ this.#itemByKey.clear();
2934
+ const items = [];
2935
+ for (const it of this.#adapter.getItems()) {
2936
+ const key = this.#adapter.getKey(it);
2937
+ this.#itemByKey.set(key, it);
2938
+ items.push({
2939
+ key,
2940
+ label: this.#adapter.getLabel(it)
2941
+ });
2942
+ }
2943
+ this.#search.setItems(items);
2944
+ }
2945
+ /** Popover markup. Pair with `listSearchStyles` in the host's `static styles`. */
2946
+ renderField() {
2947
+ if (!this.enabled) return A;
2948
+ const hasQuery = this.#query.trim().length > 0;
2949
+ const others = this.#modes.filter((m) => m !== this.#mode);
2950
+ const showSelectMatches = this.#modes.includes(`select`) && this.#mode !== `select`;
2951
+ return b`
2952
+ <div class="list-search" part="search" ?hidden=${!this.#open}>
2953
+ <div class="list-search-row">
2954
+ <span class="list-search-mode">${this.#mode}</span>
2955
+ <input
2956
+ class=${INPUT_CLASS}
2957
+ type="text"
2958
+ part="search-input"
2959
+ placeholder=${this.#placeholder}
2960
+ .value=${this.#query}
2961
+ @input=${this.#onInput}
2962
+ @keydown=${this.#onKeydown}
2963
+ />
2964
+ ${hasQuery ? b`<span class="list-search-count">${this.#matches.length === 0 ? `0` : this.#matchIndex + 1} / ${this.#matches.length}</span>` : A}
2965
+ ${this.#mode === `navigate` && this.#matches.length > 1 ? b`
2966
+ <button class="list-search-icon-btn" title="Previous match (↑)" @click=${() => this.prevMatch()}>‹</button>
2967
+ <button class="list-search-icon-btn" title="Next match (↓)" @click=${() => this.nextMatch()}>›</button>
2968
+ ` : A}
2969
+ <button class="list-search-icon-btn" title="Close (Esc)" @click=${() => this.close()}>✕</button>
2970
+ </div>
2971
+ ${others.length > 0 || showSelectMatches ? b`
2972
+ <div class="list-search-switch">
2973
+ ${others.map((m) => b`<button class="list-search-btn" @click=${() => this.switchMode(m)}>${m}</button>`)}
2974
+ ${showSelectMatches ? b`<button class="list-search-btn" @click=${() => this.selectMatches()}>Select matches</button>` : A}
2975
+ </div>
2976
+ ` : A}
2977
+ </div>
2978
+ `;
2979
+ }
2980
+ #focusInput() {
2981
+ this.#host.renderRoot?.querySelector(`.${INPUT_CLASS}`)?.focus();
2982
+ }
2983
+ #onInput = (e) => {
2984
+ this.setQuery(e.target.value);
2985
+ };
2986
+ #onKeydown = (e) => {
2987
+ switch (e.key) {
2988
+ case `Escape`:
2989
+ e.preventDefault();
2990
+ e.stopPropagation();
2991
+ this.close();
2992
+ break;
2993
+ case `Enter`:
2994
+ e.preventDefault();
2995
+ e.stopPropagation();
2996
+ this.commit();
2997
+ break;
2998
+ case `ArrowDown`:
2999
+ if (this.#mode === `navigate`) {
3000
+ e.preventDefault();
3001
+ this.nextMatch();
3002
+ }
3003
+ break;
3004
+ case `ArrowUp`: if (this.#mode === `navigate`) {
3005
+ e.preventDefault();
3006
+ this.prevMatch();
3007
+ }
3008
+ }
3009
+ };
3010
+ #cycle(delta) {
3011
+ if (this.#matches.length === 0) return;
3012
+ const n = this.#matches.length;
3013
+ this.#matchIndex = (this.#matchIndex + delta + n) % n;
3014
+ if (this.#mode === `navigate`) this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
3015
+ this.#host.requestUpdate();
3016
+ }
3017
+ #itemFor(match) {
3018
+ return match ? this.#itemByKey.get(match.item.key) : void 0;
3019
+ }
3020
+ #currentItems() {
3021
+ return this.#matches.map((m) => this.#itemFor(m)).filter((x) => x !== void 0);
3022
+ }
3023
+ get #highlightKey() {
3024
+ return this.#adapter.highlightKey ?? DEFAULT_HIGHLIGHT_KEY;
3025
+ }
3026
+ #onState(state) {
3027
+ if (!this.#open) return;
3028
+ const hasQuery = state.query.trim().length > 0;
3029
+ this.#matches = hasQuery ? [...state.results] : [];
3030
+ if (this.#matchIndex >= this.#matches.length) this.#matchIndex = 0;
3031
+ this.#applyMode();
3032
+ this.#host.requestUpdate();
3033
+ }
3034
+ #applyMode() {
3035
+ if (!(this.#query.trim().length > 0)) {
3036
+ if (this.#filterActive) {
3037
+ this.#adapter.applyFilter(void 0);
3038
+ this.#filterActive = false;
3039
+ }
3040
+ this.#applyHighlights();
3041
+ return;
3042
+ }
3043
+ switch (this.#mode) {
3044
+ case `navigate`:
3045
+ this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
3046
+ break;
3047
+ case `select`:
3048
+ this.#adapter.selectItems(this.#currentItems());
3049
+ break;
3050
+ case `filter`:
3051
+ this.#adapter.applyFilter(new Set(this.#matches.map((m) => m.item.key)));
3052
+ this.#filterActive = true;
3053
+ }
3054
+ this.#applyHighlights();
3055
+ }
3056
+ #applyHighlights() {
3057
+ const root = this.#adapter.highlightRoot?.();
3058
+ const selector = this.#adapter.highlightSelector;
3059
+ const keyOf = this.#adapter.highlightKeyOf;
3060
+ if (!root || !selector || !keyOf) return;
3061
+ if (this.#query.trim().length === 0) {
3062
+ clearHighlights({ key: this.#highlightKey });
3063
+ return;
3064
+ }
3065
+ const state = this.#search.state;
3066
+ const targets = buildHighlightTargets(root, selector, state.positionsByItem, (el) => {
3067
+ const key = keyOf(el);
3068
+ return key === void 0 ? void 0 : state.results.find((r) => r.item.key === key)?.item;
3069
+ }, this.#adapter.highlightText ?? ((el) => el.textContent ?? ``));
3070
+ try {
3071
+ applyHighlights(targets, { key: this.#highlightKey });
3072
+ } catch {
3073
+ clearHighlights({ key: this.#highlightKey });
3074
+ }
3075
+ }
3076
+ #teardownEffects() {
3077
+ if (this.#filterActive) {
3078
+ this.#adapter.applyFilter(void 0);
3079
+ this.#filterActive = false;
3080
+ }
3081
+ clearHighlights({ key: this.#highlightKey });
3082
+ }
3083
+ };
3084
+ //#endregion
3085
+ //#region src/incr-search/list-search-styles.ts
3086
+ /**
3087
+ * Styles for the incremental-search popover rendered by
3088
+ * `ListSearchController.renderField()`. Add to a component's `static styles`;
3089
+ * the class names are the contract between this fragment and `renderField()`.
3090
+ *
3091
+ * The host must give the popover a positioned ancestor (a wrapper with
3092
+ * `position: relative`, or `:host { position: relative }`). Tunables:
3093
+ * `--list-search-inset` (default `8px`) offsets it from the top-right corner.
3094
+ *
3095
+ * See `docs/infra-incremental.md`.
3096
+ */
3097
+ const listSearchStyles = i$3`
3098
+ .list-search {
3099
+ position: absolute;
3100
+ top: var(--list-search-inset, 8px);
3101
+ right: var(--list-search-inset, 8px);
3102
+ z-index: 20;
3103
+ display: flex;
3104
+ flex-direction: column;
3105
+ gap: 4px;
3106
+ padding: 6px;
3107
+ box-sizing: border-box;
3108
+ background: var(--surface-4, #2a2a2a);
3109
+ color: var(--surface-3-text, inherit);
3110
+ border: 1px solid var(--border, #444);
3111
+ border-radius: var(--radius-m, 6px);
3112
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
3113
+ font-size: var(--text-s, 0.85em);
3114
+ }
3115
+
3116
+ .list-search[hidden] {
3117
+ display: none;
3118
+ }
3119
+
3120
+ .list-search-row {
3121
+ display: flex;
3122
+ align-items: center;
3123
+ gap: 4px;
3124
+ }
3125
+
3126
+ .list-search-mode {
3127
+ flex: none;
3128
+ padding: 1px 7px;
3129
+ border-radius: 999px;
3130
+ font-weight: 600;
3131
+ text-transform: capitalize;
3132
+ background: var(--accent, #60a5fa);
3133
+ color: var(--accent-text, #000);
3134
+ }
3135
+
3136
+ .list-search-input {
3137
+ flex: 1;
3138
+ min-width: 140px;
3139
+ background: transparent;
3140
+ border: none;
3141
+ outline: none;
3142
+ color: inherit;
3143
+ font: inherit;
3144
+ }
3145
+
3146
+ .list-search-count {
3147
+ flex: none;
3148
+ opacity: 0.6;
3149
+ font-variant-numeric: tabular-nums;
3150
+ white-space: nowrap;
3151
+ }
3152
+
3153
+ .list-search-switch {
3154
+ display: flex;
3155
+ flex-wrap: wrap;
3156
+ gap: 4px;
3157
+ }
3158
+
3159
+ .list-search-btn {
3160
+ padding: 1px 7px;
3161
+ background: var(--surface-3, #333);
3162
+ color: inherit;
3163
+ border: 1px solid var(--border, #444);
3164
+ border-radius: var(--radius-s, 4px);
3165
+ cursor: pointer;
3166
+ font: inherit;
3167
+ text-transform: capitalize;
3168
+ }
3169
+
3170
+ .list-search-btn:hover {
3171
+ background: var(--item-bg-tickled, #444);
3172
+ }
3173
+
3174
+ .list-search-icon-btn {
3175
+ flex: none;
3176
+ padding: 0 2px;
3177
+ background: transparent;
3178
+ border: none;
3179
+ color: var(--surface-muted-text, #888);
3180
+ cursor: pointer;
3181
+ font-size: 1.1em;
3182
+ line-height: 1;
3183
+ }
3184
+
3185
+ .list-search-icon-btn:hover {
3186
+ color: inherit;
3187
+ }
3188
+ `;
3189
+ //#endregion
3190
+ //#region src/incr-search/tree-list-search-adapter.ts
3191
+ /**
3192
+ * A {@link ListSearchAdapter} over any `TreeSearchHost` (`ixfx-tree-list`,
3193
+ * `ixfx-miller-list`, `ixfx-crumb-navigation`). Navigate reveals the match
3194
+ * (expands its ancestors, moves the cursor); select drives `selectMany`; filter
3195
+ * sets `filterPredicate`. The item set is the whole tree — `getNodes()` returns
3196
+ * every node regardless of expansion or an active filter — so refining a query
3197
+ * can always re-surface a node. See `docs/infra-incremental.md`.
3198
+ */
3199
+ function createTreeListSearchAdapter(host, options = {}) {
3200
+ return {
3201
+ getItems: () => host.getNodes(),
3202
+ getLabel: options.getLabel ?? ((item) => item.node.item.label),
3203
+ getKey: (item) => item.node.item.key,
3204
+ focusItem: (item) => {
3205
+ if (item) host.revealKey(item.node.item.key);
3206
+ },
3207
+ activateItem: (item) => {
3208
+ host.revealKey(item.node.item.key);
3209
+ host.select(item.node);
3210
+ },
3211
+ selectItems: (items) => {
3212
+ host.selectMany(items.map((i) => i.node));
3213
+ },
3214
+ applyFilter: (matched) => {
3215
+ host.filterPredicate = matched === void 0 ? void 0 : (node) => matched.has(node.item.key);
3216
+ },
3217
+ highlightRoot: () => host.shadowRoot,
3218
+ highlightSelector: options.labelSelector ?? `.tree-label`,
3219
+ highlightKeyOf: (el) => el.closest(`[data-key]`)?.dataset.key,
3220
+ highlightKey: options.highlightKey
3221
+ };
3222
+ }
3223
+ //#endregion
2579
3224
  //#region src/ac-token/ac-token.ts
2580
3225
  const DEFAULT_TOKENIZER = (text) => text.split(/[\s,;]+/);
2581
3226
  function makeLruCache(maxSize) {
@@ -3877,80 +4522,6 @@ __decorate([state()], RadioGroup.prototype, "_icons", void 0);
3877
4522
  __decorate([query(`slot`)], RadioGroup.prototype, "_slot", void 0);
3878
4523
  RadioGroup = __decorate([safeCustomElement(`ixfx-radio-group`)], RadioGroup);
3879
4524
  //#endregion
3880
- //#region src/interaction/keyed-list.ts
3881
- /**
3882
- * Helpers for key-based selection on the Element-flavoured list components
3883
- * (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
3884
- *
3885
- * A "key" is a stable string the host puts on each item element — `data-key`
3886
- * (preferred) or `data-value`. It survives the host rebuilding the item set
3887
- * (re-fetch, re-sort, filter) where raw `Element` references do not.
3888
- */
3889
- /**
3890
- * The stable key for an item element: `data-key`, then `data-value`, else
3891
- * `undefined`.
3892
- */
3893
- function keyForElement(el) {
3894
- return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
3895
- }
3896
- /**
3897
- * A lazily-built key → element index over a container. Rebuilt on first use
3898
- * and after any child-list mutation under `root`; `invalidate()` forces a
3899
- * rebuild on the next `get()`.
3900
- *
3901
- * ```ts
3902
- * #keyIndex = new KeyedElementIndex(this);
3903
- * // ...
3904
- * const el = this.#keyIndex.get(`row-42`);
3905
- * // on disconnect:
3906
- * this.#keyIndex.dispose();
3907
- * ```
3908
- */
3909
- var KeyedElementIndex = class {
3910
- #root;
3911
- #selector;
3912
- #map = /* @__PURE__ */ new Map();
3913
- #dirty = true;
3914
- #observer;
3915
- /**
3916
- * @param root Container whose descendants are indexed.
3917
- * @param itemSelector Which descendants count as items (default: any `[data-key]`).
3918
- */
3919
- constructor(root, itemSelector = `[data-key]`) {
3920
- this.#root = root;
3921
- this.#selector = itemSelector;
3922
- this.#observer = new MutationObserver(() => {
3923
- this.#dirty = true;
3924
- });
3925
- this.#observer.observe(root, {
3926
- childList: true,
3927
- subtree: true
3928
- });
3929
- }
3930
- /** The element with this key, or `undefined`. First match wins on duplicates. */
3931
- get(key) {
3932
- if (this.#dirty) this.#rebuild();
3933
- return this.#map.get(key);
3934
- }
3935
- /** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
3936
- invalidate() {
3937
- this.#dirty = true;
3938
- }
3939
- /** Stop observing and drop the cache. */
3940
- dispose() {
3941
- this.#observer.disconnect();
3942
- this.#map.clear();
3943
- }
3944
- #rebuild() {
3945
- this.#map.clear();
3946
- for (const el of this.#root.querySelectorAll(this.#selector)) {
3947
- const key = keyForElement(el);
3948
- if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
3949
- }
3950
- this.#dirty = false;
3951
- }
3952
- };
3953
- //#endregion
3954
4525
  //#region src/interaction/list-item-animation.ts
3955
4526
  /**
3956
4527
  * Shared insertion / removal animation for the list & tree components.
@@ -8557,6 +9128,16 @@ let CrumbNavigationElement = class CrumbNavigationElement extends i {
8557
9128
  if (!keyPath) return false;
8558
9129
  return this.navigateTo(keyPath);
8559
9130
  }
9131
+ /** `TreeComponent` parity — a crumb path always follows the selection, so this just navigates to `key`. */
9132
+ revealKey(key) {
9133
+ return this.navigateToNode((n) => n.item.key === key);
9134
+ }
9135
+ /** `TreeComponent` parity — a crumb path is a single selection, so only the last node is kept. */
9136
+ selectMany(nodes) {
9137
+ const last = [...nodes].at(-1);
9138
+ if (last) this.select(last);
9139
+ else this.clearSelection();
9140
+ }
8560
9141
  #findNodePath(nodes, predicate, prefix = []) {
8561
9142
  for (const node of nodes) {
8562
9143
  const path = [...prefix, node.item.key];
@@ -11067,6 +11648,8 @@ let DataGridElement = class DataGridElement extends i {
11067
11648
  this._sortedRows = [];
11068
11649
  this._flat = [];
11069
11650
  this._flatKeys = [];
11651
+ this.#searchKeyByRow = /* @__PURE__ */ new Map();
11652
+ this.search = new ListSearchController(this, { adapter: this.#createSearchAdapter() });
11070
11653
  this.#prevFlatKeys = [];
11071
11654
  this.#enteringRowKeys = /* @__PURE__ */ new Set();
11072
11655
  this.#rowReflowTimer = 0;
@@ -11272,6 +11855,7 @@ let DataGridElement = class DataGridElement extends i {
11272
11855
  tickledStyles,
11273
11856
  tickledItemStyles,
11274
11857
  listItemFadeKeyframes,
11858
+ listSearchStyles,
11275
11859
  i$3`
11276
11860
  :host {
11277
11861
  --data-grid-row-height: 28px;
@@ -11280,6 +11864,7 @@ let DataGridElement = class DataGridElement extends i {
11280
11864
  --data-grid-header-bg: var(--surface-2);
11281
11865
  --data-grid-border: var(--border);
11282
11866
  --data-grid-header-text: var(--surface-3-text);
11867
+ position: relative;
11283
11868
  display: block;
11284
11869
  overflow: hidden;
11285
11870
  border: var(--data-grid-border);
@@ -11373,6 +11958,55 @@ let DataGridElement = class DataGridElement extends i {
11373
11958
  `
11374
11959
  ];
11375
11960
  }
11961
+ #searchRegistry;
11962
+ #unregisterSearchCommands;
11963
+ #searchKeyByRow;
11964
+ /** Command registry carrying this grid's `list.search.*` commands. */
11965
+ get registry() {
11966
+ return this.#searchRegistry;
11967
+ }
11968
+ #rowSearchText(row) {
11969
+ if (this.rowSearchText) return this.rowSearchText(row);
11970
+ const rec = row;
11971
+ return this._visibleColumns.map((c) => String(rec[c.key] ?? ``)).join(` `);
11972
+ }
11973
+ #createSearchAdapter() {
11974
+ return {
11975
+ getItems: () => {
11976
+ this.#searchKeyByRow.clear();
11977
+ this._sortedRows.forEach((r, i) => this.#searchKeyByRow.set(r, this.getRowKey(r, i)));
11978
+ return this._sortedRows;
11979
+ },
11980
+ getLabel: (row) => this.#rowSearchText(row),
11981
+ getKey: (row) => this.#searchKeyByRow.get(row) ?? String(this._sortedRows.indexOf(row)),
11982
+ focusItem: (row) => {
11983
+ if (row === void 0) return;
11984
+ const key = this.#searchKeyByRow.get(row);
11985
+ if (key !== void 0) this.tickleRow(key);
11986
+ },
11987
+ selectItems: (rows) => {
11988
+ this.selectKeys(rows.map((r) => this.#searchKeyByRow.get(r)).filter((k) => k !== void 0));
11989
+ },
11990
+ applyFilter: (matched) => {
11991
+ this.filterPredicate = matched === void 0 ? void 0 : (row) => {
11992
+ const k = this.#searchKeyByRow.get(row);
11993
+ return k === void 0 || matched.has(k);
11994
+ };
11995
+ }
11996
+ };
11997
+ }
11998
+ #effectiveSearchModes() {
11999
+ const raw = this.searchModes?.trim();
12000
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
12001
+ const requested = new Set(raw.split(/\s+/));
12002
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
12003
+ }
12004
+ #syncSearchCommands() {
12005
+ if (!this.#searchRegistry) return;
12006
+ this.search.setModes(this.#effectiveSearchModes());
12007
+ this.#unregisterSearchCommands?.();
12008
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
12009
+ }
11376
12010
  /** Row-key snapshot from the previous update, for the insertion / removal animation. */
11377
12011
  #prevFlatKeys;
11378
12012
  #enteringRowKeys;
@@ -11385,17 +12019,29 @@ let DataGridElement = class DataGridElement extends i {
11385
12019
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
11386
12020
  this.addEventListener(`keydown`, this._onKeyDown);
11387
12021
  this.addEventListener(`focusout`, this._onFocusOut);
12022
+ this.#searchRegistry ??= createCommandRegistry();
12023
+ this.#syncSearchCommands();
11388
12024
  }
11389
12025
  disconnectedCallback() {
11390
12026
  super.disconnectedCallback();
11391
12027
  this.removeEventListener(`keydown`, this._onKeyDown);
11392
12028
  this.removeEventListener(`focusout`, this._onFocusOut);
11393
12029
  cancelAnimationFrame(this._raf);
12030
+ this.#unregisterSearchCommands?.();
12031
+ this.#unregisterSearchCommands = void 0;
12032
+ this.#searchRegistry = void 0;
11394
12033
  if (this.#rowReflowTimer) {
11395
12034
  clearTimeout(this.#rowReflowTimer);
11396
12035
  this.#rowReflowTimer = 0;
11397
12036
  }
11398
12037
  }
12038
+ /**
12039
+ * Dispatch a command to this grid — routes through the command registry so
12040
+ * `list.search.*` (and any other registered commands) work.
12041
+ */
12042
+ dispatch(commandId, args) {
12043
+ this.#searchRegistry?.invoke(commandId, args);
12044
+ }
11399
12045
  firstUpdated() {
11400
12046
  new ResizeObserver(() => {
11401
12047
  if (!this._viewportEl) return;
@@ -11421,6 +12067,7 @@ let DataGridElement = class DataGridElement extends i {
11421
12067
  willUpdate(changed) {
11422
12068
  super.willUpdate(changed);
11423
12069
  this.setAttribute(`data-input-mode`, this._tickled.inputMode);
12070
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
11424
12071
  if (!this.#appendInProgress) {
11425
12072
  if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`)) this._sortedRows = this._applySort([...this.rows]);
11426
12073
  this._rebuildFlat();
@@ -11475,18 +12122,23 @@ let DataGridElement = class DataGridElement extends i {
11475
12122
  return String(this.groupBy(row));
11476
12123
  }
11477
12124
  _rebuildFlat() {
12125
+ const filtered = this.filterPredicate !== void 0;
12126
+ const source = filtered ? this._sortedRows.filter((r) => this.filterPredicate(r)) : this._sortedRows;
11478
12127
  if (!this.groupBy) {
11479
- this._flat = this._sortedRows.map((r, i) => ({
11480
- type: `row`,
11481
- key: this.getRowKey(r, i),
11482
- row: r,
11483
- idx: i
11484
- }));
12128
+ this._flat = source.map((r, i) => {
12129
+ const idx = filtered ? this._sortedRows.indexOf(r) : i;
12130
+ return {
12131
+ type: `row`,
12132
+ key: this.getRowKey(r, idx),
12133
+ row: r,
12134
+ idx
12135
+ };
12136
+ });
11485
12137
  this._flatKeys = this._flat.map((f) => f.key);
11486
12138
  return;
11487
12139
  }
11488
12140
  const groups = /* @__PURE__ */ new Map();
11489
- for (const r of this._sortedRows) {
12141
+ for (const r of source) {
11490
12142
  const k = this._getGroupKey(r) ?? ``;
11491
12143
  const arr = groups.get(k);
11492
12144
  if (arr) arr.push(r);
@@ -12356,6 +13008,7 @@ let DataGridElement = class DataGridElement extends i {
12356
13008
  })}
12357
13009
  </div>
12358
13010
  </div>
13011
+ ${this.search.renderField()}
12359
13012
  </div>
12360
13013
  `;
12361
13014
  }
@@ -12399,6 +13052,13 @@ __decorate([property({
12399
13052
  attribute: `column-menu`
12400
13053
  })], DataGridElement.prototype, "columnMenu", void 0);
12401
13054
  __decorate([property({ attribute: false })], DataGridElement.prototype, "globalFormatter", void 0);
13055
+ __decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
13056
+ __decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
13057
+ __decorate([property({
13058
+ type: String,
13059
+ attribute: `search-modes`,
13060
+ reflect: true
13061
+ })], DataGridElement.prototype, "searchModes", void 0);
12402
13062
  __decorate([state()], DataGridElement.prototype, "_selectedKeys", void 0);
12403
13063
  __decorate([state()], DataGridElement.prototype, "_scrollTop", void 0);
12404
13064
  __decorate([state()], DataGridElement.prototype, "_scrollLeft", void 0);
@@ -12684,8 +13344,8 @@ const ITEM_SELECTOR$2 = `:scope > li`;
12684
13344
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
12685
13345
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
12686
13346
  let DetailListElement = class DetailListElement extends i {
12687
- constructor(..._args2) {
12688
- super(..._args2);
13347
+ constructor(..._args) {
13348
+ super(..._args);
12689
13349
  this.selectionMode = `single`;
12690
13350
  this.interactionMode = `standard`;
12691
13351
  this.sort = `none`;
@@ -12695,8 +13355,6 @@ let DetailListElement = class DetailListElement extends i {
12695
13355
  this.iconProperty = ``;
12696
13356
  this.keyProperty = ``;
12697
13357
  this.itemWrap = `stop`;
12698
- this._searchVisible = false;
12699
- this._searchQuery = ``;
12700
13358
  this.#itemData = /* @__PURE__ */ new WeakMap();
12701
13359
  this.#grid = [];
12702
13360
  this.#positions = /* @__PURE__ */ new Map();
@@ -12733,6 +13391,11 @@ let DetailListElement = class DetailListElement extends i {
12733
13391
  onStateChange: () => this._syncItemStates(),
12734
13392
  itemWrap: this.itemWrap
12735
13393
  });
13394
+ this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
13395
+ itemSelector: ITEM_SELECTOR$2,
13396
+ getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
13397
+ highlightKey: SEARCH_HIGHLIGHT_KEY
13398
+ }) });
12736
13399
  this._onPointerOver = (e) => {
12737
13400
  const item = this._findListItem(e.composedPath());
12738
13401
  if (item) this.tickled.handlePointerEnter(item);
@@ -12741,11 +13404,7 @@ let DetailListElement = class DetailListElement extends i {
12741
13404
  this.tickled.handlePointerLeave(e);
12742
13405
  };
12743
13406
  this._onKeyDown = (e) => {
12744
- if ((e.ctrlKey || e.metaKey) && e.key === `f`) {
12745
- e.preventDefault();
12746
- this._openSearch();
12747
- return;
12748
- }
13407
+ if (this.search.isOpen) return;
12749
13408
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
12750
13409
  if (this.selection.canSelectAll()) {
12751
13410
  e.preventDefault();
@@ -12753,11 +13412,6 @@ let DetailListElement = class DetailListElement extends i {
12753
13412
  }
12754
13413
  return;
12755
13414
  }
12756
- if (this._searchVisible && e.key === `Escape`) {
12757
- e.preventDefault();
12758
- this._closeSearch();
12759
- return;
12760
- }
12761
13415
  switch (e.key) {
12762
13416
  case `ArrowDown`:
12763
13417
  e.preventDefault();
@@ -12826,44 +13480,7 @@ let DetailListElement = class DetailListElement extends i {
12826
13480
  e.preventDefault();
12827
13481
  this._listEl.scrollLeft += e.deltaY;
12828
13482
  };
12829
- this._onSearchInput = (e) => {
12830
- const input = e.target;
12831
- this._searchQuery = input.value;
12832
- this.#search?.setQuery(this._searchQuery).then(() => this.#recomputeGeometry());
12833
- };
12834
- this._onSearchKeydown = (e) => {
12835
- if (e.key === `Escape`) {
12836
- e.stopPropagation();
12837
- this._closeSearch();
12838
- } else if (e.key === `ArrowDown`) {
12839
- e.preventDefault();
12840
- this.focus();
12841
- this.#navigateDown();
12842
- } else if (e.key === `ArrowUp`) {
12843
- e.preventDefault();
12844
- this.focus();
12845
- this.#navigateUp();
12846
- } else if (e.key === `ArrowRight`) e.preventDefault();
12847
- else if (e.key === `ArrowLeft`) e.preventDefault();
12848
- else if (e.key === `Enter`) {
12849
- e.preventDefault();
12850
- e.stopPropagation();
12851
- const id = this.tickled.tickledId ?? this.#findFirstCheckable();
12852
- if (id) {
12853
- this._dispatch(`list-item-click`, { item: id });
12854
- this._activateItem(id);
12855
- this._closeSearch();
12856
- }
12857
- } else if (e.key === ` `) {
12858
- const id = this.tickled.tickledId;
12859
- if (id) {
12860
- e.preventDefault();
12861
- this.selection.handleSpace(id);
12862
- }
12863
- }
12864
- };
12865
13483
  }
12866
- #search;
12867
13484
  #itemData;
12868
13485
  #grid;
12869
13486
  #positions;
@@ -12875,6 +13492,24 @@ let DetailListElement = class DetailListElement extends i {
12875
13492
  #builtFromProvider;
12876
13493
  #providerBuilt;
12877
13494
  #data;
13495
+ #searchRegistry;
13496
+ #unregisterSearchCommands;
13497
+ /** Command registry carrying this list's `list.search.*` commands. */
13498
+ get registry() {
13499
+ return this.#searchRegistry;
13500
+ }
13501
+ #effectiveSearchModes() {
13502
+ const raw = this.searchModes?.trim();
13503
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
13504
+ const requested = new Set(raw.split(/\s+/));
13505
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
13506
+ }
13507
+ #syncSearchCommands() {
13508
+ if (!this.#searchRegistry) return;
13509
+ this.search.setModes(this.#effectiveSearchModes());
13510
+ this.#unregisterSearchCommands?.();
13511
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
13512
+ }
12878
13513
  connectedCallback() {
12879
13514
  super.connectedCallback();
12880
13515
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -12897,6 +13532,8 @@ let DetailListElement = class DetailListElement extends i {
12897
13532
  attributeFilter: [`hidden`]
12898
13533
  });
12899
13534
  for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#animatedItems.add(el);
13535
+ this.#searchRegistry ??= createCommandRegistry();
13536
+ this.#syncSearchCommands();
12900
13537
  }
12901
13538
  /**
12902
13539
  * Collapse-in items appended to the light DOM (however they got there). A bulk
@@ -12930,23 +13567,18 @@ let DetailListElement = class DetailListElement extends i {
12930
13567
  this.#resizeObserver = void 0;
12931
13568
  this.#mutationObserver?.disconnect();
12932
13569
  this.#mutationObserver = void 0;
12933
- this.#search?.dispose();
12934
- this.#search = void 0;
13570
+ this.#unregisterSearchCommands?.();
13571
+ this.#unregisterSearchCommands = void 0;
13572
+ this.#searchRegistry = void 0;
12935
13573
  }
12936
13574
  firstUpdated() {
12937
13575
  this.#processChildren();
12938
13576
  if (this.filterPredicate) this.#applyFilterPredicate();
12939
- this.#search = createElementIncrSearch({
12940
- container: this,
12941
- itemSelector: ITEM_SELECTOR$2,
12942
- getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
12943
- getKey: (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
12944
- highlightKey: SEARCH_HIGHLIGHT_KEY
12945
- });
12946
13577
  this.#recomputeGeometry();
12947
13578
  }
12948
13579
  updated(changed) {
12949
13580
  super.updated(changed);
13581
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
12950
13582
  if (changed.has(`provider`)) {
12951
13583
  this.#builtFromProvider = [];
12952
13584
  this.#providerBuilt = 0;
@@ -13040,8 +13672,14 @@ let DetailListElement = class DetailListElement extends i {
13040
13672
  getItemData(item) {
13041
13673
  return this.#itemData.get(item);
13042
13674
  }
13043
- /** Dispatch a command to this list. No-op by default (grouped-lister parity). */
13044
- dispatch(_commandId, _args) {}
13675
+ /**
13676
+ * Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
13677
+ * forward commands. Routes through the command registry (so `list.search.*`
13678
+ * and any other registered commands work).
13679
+ */
13680
+ dispatch(commandId, args) {
13681
+ this.#searchRegistry?.invoke(commandId, args);
13682
+ }
13045
13683
  select(item) {
13046
13684
  this.selection.replace(item);
13047
13685
  }
@@ -13525,16 +14163,6 @@ let DetailListElement = class DetailListElement extends i {
13525
14163
  composed: true
13526
14164
  }));
13527
14165
  }
13528
- _openSearch() {
13529
- this._searchVisible = true;
13530
- this.updateComplete.then(() => this._searchInputEl?.focus());
13531
- }
13532
- _closeSearch() {
13533
- this._searchVisible = false;
13534
- this._searchQuery = ``;
13535
- this.#search?.clearQuery().then(() => this.#recomputeGeometry());
13536
- this.focus();
13537
- }
13538
14166
  _activateItem(item) {
13539
14167
  if (this.interactionMode !== `manual`) this.selection.replace(item);
13540
14168
  this._dispatch(`list-activate`, { item });
@@ -13543,6 +14171,7 @@ let DetailListElement = class DetailListElement extends i {
13543
14171
  this.styles = [
13544
14172
  themeFallbacks,
13545
14173
  scrollFadeStyles(`x`),
14174
+ listSearchStyles,
13546
14175
  i$3`
13547
14176
  :host {
13548
14177
  display: block;
@@ -13664,48 +14293,6 @@ let DetailListElement = class DetailListElement extends i {
13664
14293
  .loading[hidden] {
13665
14294
  display: none;
13666
14295
  }
13667
-
13668
- .search-overlay {
13669
- position: absolute;
13670
- top: 8px;
13671
- right: 8px;
13672
- z-index: 10;
13673
- display: flex;
13674
- align-items: center;
13675
- gap: 4px;
13676
- background: var(--surface-4, #2a2a2a);
13677
- border: 1px solid var(--border, #444);
13678
- border-radius: var(--radius-m, 4px);
13679
- padding: 4px 6px;
13680
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
13681
- }
13682
-
13683
- .search-overlay[hidden] {
13684
- display: none;
13685
- }
13686
-
13687
- .search-input {
13688
- background: transparent;
13689
- border: none;
13690
- outline: none;
13691
- color: inherit;
13692
- font-size: var(--text-m, 0.875em);
13693
- min-width: 140px;
13694
- }
13695
-
13696
- .search-close {
13697
- background: transparent;
13698
- border: none;
13699
- color: var(--surface-muted-text, #888);
13700
- cursor: pointer;
13701
- font-size: 1.1em;
13702
- line-height: 1;
13703
- padding: 0 2px;
13704
- }
13705
-
13706
- .search-close:hover {
13707
- color: inherit;
13708
- }
13709
14296
  `
13710
14297
  ];
13711
14298
  }
@@ -13720,21 +14307,7 @@ let DetailListElement = class DetailListElement extends i {
13720
14307
  <div class="loading" part="loading" ?hidden=${!loading}>
13721
14308
  ${loading ? progress !== void 0 ? `${Math.round(progress * 100)}%` : `loading…` : A}
13722
14309
  </div>
13723
- <div
13724
- class="search-overlay"
13725
- part="search-overlay"
13726
- ?hidden=${!this._searchVisible}
13727
- >
13728
- <input
13729
- type="text"
13730
- class="search-input"
13731
- placeholder="Search…"
13732
- .value=${this._searchQuery}
13733
- @input=${this._onSearchInput}
13734
- @keydown=${this._onSearchKeydown}
13735
- />
13736
- <button class="search-close" @click=${this._closeSearch} title="Close search (Esc)">×</button>
13737
- </div>
14310
+ ${this.search.renderField()}
13738
14311
  </div>
13739
14312
  `;
13740
14313
  }
@@ -13774,9 +14347,11 @@ __decorate([property({
13774
14347
  attribute: `item-wrap`
13775
14348
  })], DetailListElement.prototype, "itemWrap", void 0);
13776
14349
  __decorate([property({ attribute: false })], DetailListElement.prototype, "checkboxRenderer", void 0);
13777
- __decorate([state()], DetailListElement.prototype, "_searchVisible", void 0);
13778
- __decorate([state()], DetailListElement.prototype, "_searchQuery", void 0);
13779
- __decorate([query(`.search-input`)], DetailListElement.prototype, "_searchInputEl", void 0);
14350
+ __decorate([property({
14351
+ type: String,
14352
+ attribute: `search-modes`,
14353
+ reflect: true
14354
+ })], DetailListElement.prototype, "searchModes", void 0);
13780
14355
  __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
13781
14356
  DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
13782
14357
  //#endregion
@@ -19619,6 +20194,11 @@ let GridListElement = class GridListElement extends i {
19619
20194
  onStateChange: () => this._syncItemStates(),
19620
20195
  itemWrap: this.#itemWrap
19621
20196
  });
20197
+ this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
20198
+ itemSelector: ITEM_SELECTOR$1,
20199
+ getLabel: (el) => el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
20200
+ highlightKey: `grid-list-search`
20201
+ }) });
19622
20202
  this.#onScroll = (e) => {
19623
20203
  const el = e.target;
19624
20204
  this.#cancelScrollRaf();
@@ -19645,7 +20225,10 @@ let GridListElement = class GridListElement extends i {
19645
20225
  const from = this.tickled.tickledId ? this.#items.indexOf(this.tickled.tickledId) : -1;
19646
20226
  const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
19647
20227
  const next = this.#items[nextIndex];
19648
- if (next) this.tickled.handleKeyboardNavigation(next);
20228
+ if (next) {
20229
+ this.tickled.handleKeyboardNavigation(next);
20230
+ this.#scrollItemIntoView(next);
20231
+ }
19649
20232
  };
19650
20233
  const perLine = this.orientation === `grid` ? win.cols : 1;
19651
20234
  switch (e.key) {
@@ -19663,9 +20246,11 @@ let GridListElement = class GridListElement extends i {
19663
20246
  break;
19664
20247
  case `Home`:
19665
20248
  this.tickled.handleKeyboardNavigation(`first`);
20249
+ if (this.tickled.tickledId) this.#scrollItemIntoView(this.tickled.tickledId);
19666
20250
  break;
19667
20251
  case `End`:
19668
20252
  this.tickled.handleKeyboardNavigation(`last`);
20253
+ if (this.tickled.tickledId) this.#scrollItemIntoView(this.tickled.tickledId);
19669
20254
  break;
19670
20255
  case `PageUp`:
19671
20256
  case `PageDown`: {
@@ -19708,11 +20293,13 @@ let GridListElement = class GridListElement extends i {
19708
20293
  scrollFadeStyles(`x`),
19709
20294
  scrollFadeStyles(`y`),
19710
20295
  listItemFadeKeyframes,
20296
+ listSearchStyles,
19711
20297
  i$3`
19712
20298
  :host {
19713
20299
  --grid-list-item-bg: var(--item-bg);
19714
20300
  --grid-list-radius: var(--radius-m);
19715
20301
  --grid-list-gap: var(--space-xs);
20302
+ position: relative;
19716
20303
  display: block;
19717
20304
  height: 100%;
19718
20305
  min-height: 2em;
@@ -19825,16 +20412,19 @@ let GridListElement = class GridListElement extends i {
19825
20412
  overflow: hidden;
19826
20413
  transition: background var(--transition), border-color var(--transition);
19827
20414
  }
19828
- :host(:not([caption-style="separate"])) .cell:hover {
20415
+ :host(:not([caption-style="separate"])) .cell:hover,
20416
+ :host(:not([caption-style="separate"])) .cell.tickled {
19829
20417
  background: var(--item-bg-tickled);
19830
- border-color: var(--item-border-tickled);
20418
+ border: var(--item-border-tickled);
19831
20419
  }
19832
20420
  :host(:not([caption-style="separate"])) .cell.selected {
19833
20421
  outline: 2px solid var(--accent);
19834
20422
  outline-offset: -2px;
19835
20423
  }
19836
- :host(:not([caption-style="separate"])) .cell.tickled {
19837
- box-shadow: inset 0 0 0 2px var(--accent);
20424
+ :host(:not([caption-style="separate"])) .cell.selected.tickled {
20425
+ background: var(--item-bg-selected-tickled);
20426
+ outline: 2px dashed var(--surface-3);
20427
+ outline-offset: -2px;
19838
20428
  }
19839
20429
 
19840
20430
  /* under: caption is a strip inside the box, below the content */
@@ -19893,9 +20483,10 @@ let GridListElement = class GridListElement extends i {
19893
20483
  background: var(--grid-list-item-bg);
19894
20484
  border: 1px solid transparent;
19895
20485
  }
19896
- :host([caption-style="separate"]) .cell:hover .thumb {
20486
+ :host([caption-style="separate"]) .cell:hover .thumb,
20487
+ :host([caption-style="separate"]) .cell.tickled .thumb {
19897
20488
  background: var(--item-bg-tickled);
19898
- border-color: var(--item-border-tickled);
20489
+ border: var(--item-border-tickled);
19899
20490
  }
19900
20491
  :host([caption-style="separate"]) .caption {
19901
20492
  display: flex;
@@ -19907,20 +20498,27 @@ let GridListElement = class GridListElement extends i {
19907
20498
  }
19908
20499
  :host([caption-style="separate"][caption-align="left"]) .caption { align-self: flex-start; }
19909
20500
  :host([caption-style="separate"][caption-align="right"]) .caption { align-self: flex-end; }
19910
- :host([caption-style="separate"]) .cell.selected .thumb,
19911
- :host([caption-style="separate"]) .cell.tickled .thumb {
20501
+ :host([caption-style="separate"]) .cell.selected .thumb {
19912
20502
  outline: 2px solid var(--accent);
19913
20503
  outline-offset: 2px;
19914
20504
  }
19915
- :host([caption-style="separate"]) .cell.tickled .thumb {
19916
- outline-style: dashed;
20505
+ :host([caption-style="separate"]) .cell.selected.tickled .thumb {
20506
+ background: var(--item-bg-selected-tickled);
20507
+ outline: 2px dashed var(--surface-3);
20508
+ outline-offset: 2px;
19917
20509
  }
19918
20510
  :host([caption-style="separate"]) .cell.selected .caption {
19919
20511
  background: var(--accent);
19920
20512
  color: var(--accent-text, #000);
19921
20513
  }
19922
20514
  :host([caption-style="separate"]) .cell.tickled .caption {
19923
- outline: 1px dashed var(--accent);
20515
+ background: var(--item-bg-tickled);
20516
+ outline: var(--item-border-tickled);
20517
+ }
20518
+ :host([caption-style="separate"]) .cell.selected.tickled .caption {
20519
+ background: var(--item-bg-selected-tickled);
20520
+ outline: 2px dashed var(--surface-3);
20521
+ outline-offset: -2px;
19924
20522
  }
19925
20523
 
19926
20524
  /* ---- Insertion / removal animation ----
@@ -19973,6 +20571,25 @@ let GridListElement = class GridListElement extends i {
19973
20571
  */
19974
20572
  #slotNames;
19975
20573
  #slotSeq;
20574
+ #searchRegistry;
20575
+ #unregisterSearchCommands;
20576
+ /** Command registry carrying this grid's `list.search.*` commands. */
20577
+ get registry() {
20578
+ return this.#searchRegistry;
20579
+ }
20580
+ /** Effective search modes: this component's capability (all three) ∩ the `search-modes` attribute. */
20581
+ #effectiveSearchModes() {
20582
+ const raw = this.searchModes?.trim();
20583
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
20584
+ const requested = new Set(raw.split(/\s+/));
20585
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
20586
+ }
20587
+ #syncSearchCommands() {
20588
+ if (!this.#searchRegistry) return;
20589
+ this.search.setModes(this.#effectiveSearchModes());
20590
+ this.#unregisterSearchCommands?.();
20591
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
20592
+ }
19976
20593
  connectedCallback() {
19977
20594
  super.connectedCallback();
19978
20595
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -19984,7 +20601,12 @@ let GridListElement = class GridListElement extends i {
19984
20601
  ]) if (!this.hasAttribute(attr)) this.setAttribute(attr, value);
19985
20602
  this.addEventListener(`keydown`, this._onKeyDown);
19986
20603
  this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
19987
- this.#mutationObserver.observe(this, { childList: true });
20604
+ this.#mutationObserver.observe(this, {
20605
+ childList: true,
20606
+ attributeFilter: [`hidden`]
20607
+ });
20608
+ this.#searchRegistry ??= createCommandRegistry();
20609
+ this.#syncSearchCommands();
19988
20610
  }
19989
20611
  disconnectedCallback() {
19990
20612
  super.disconnectedCallback();
@@ -19993,6 +20615,9 @@ let GridListElement = class GridListElement extends i {
19993
20615
  this.#resizeObserver = void 0;
19994
20616
  this.#mutationObserver?.disconnect();
19995
20617
  this.#cancelScrollRaf();
20618
+ this.#unregisterSearchCommands?.();
20619
+ this.#unregisterSearchCommands = void 0;
20620
+ this.#searchRegistry = void 0;
19996
20621
  if (this.#reflowTimer) {
19997
20622
  clearTimeout(this.#reflowTimer);
19998
20623
  this.#reflowTimer = 0;
@@ -20060,6 +20685,8 @@ let GridListElement = class GridListElement extends i {
20060
20685
  this.requestUpdate();
20061
20686
  }
20062
20687
  willUpdate(changed) {
20688
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
20689
+ if (changed.has(`filterPredicate`)) this.#syncItems();
20063
20690
  if (changed.has(`itemSize`) || changed.has(`gap`)) {
20064
20691
  this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
20065
20692
  this.gap = Math.max(0, Math.round(this.gap));
@@ -20144,7 +20771,7 @@ let GridListElement = class GridListElement extends i {
20144
20771
  }
20145
20772
  }
20146
20773
  #syncItems() {
20147
- this.#items = Array.from(this.querySelectorAll(ITEM_SELECTOR$1));
20774
+ this.#items = Array.from(this.querySelectorAll(ITEM_SELECTOR$1)).filter((el) => !el.hidden && (this.filterPredicate === void 0 || this.filterPredicate(el)));
20148
20775
  for (const el of this.#items) {
20149
20776
  const name = this.#slotNameFor(el);
20150
20777
  if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
@@ -20300,6 +20927,7 @@ let GridListElement = class GridListElement extends i {
20300
20927
  ${repeat(slice, (el) => this.#slotNameFor(el), (el, i) => this.#renderCell(el, win.startIndex + i, win))}
20301
20928
  </div>
20302
20929
  </div>
20930
+ ${this.search.renderField()}
20303
20931
  `;
20304
20932
  }
20305
20933
  #renderCell(el, index, win) {
@@ -20396,6 +21024,12 @@ __decorate([property({
20396
21024
  type: String,
20397
21025
  attribute: `item-wrap`
20398
21026
  })], GridListElement.prototype, "itemWrap", void 0);
21027
+ __decorate([property({ attribute: false })], GridListElement.prototype, "filterPredicate", void 0);
21028
+ __decorate([property({
21029
+ type: String,
21030
+ attribute: `search-modes`,
21031
+ reflect: true
21032
+ })], GridListElement.prototype, "searchModes", void 0);
20399
21033
  __decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
20400
21034
  __decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
20401
21035
  __decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
@@ -20778,6 +21412,10 @@ let TreeBaseElement = class TreeBaseElement extends i {
20778
21412
  }
20779
21413
  });
20780
21414
  this._focusedDepth = 0;
21415
+ this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
21416
+ labelSelector: `.tree-label`,
21417
+ highlightKey: `incr-search-highlight`
21418
+ }) });
20781
21419
  this.#config = { indent: 20 };
20782
21420
  this.#renderedKeys = [];
20783
21421
  this.#lastNodeByKey = /* @__PURE__ */ new Map();
@@ -20786,6 +21424,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
20786
21424
  this.#leavingAnimating = /* @__PURE__ */ new Set();
20787
21425
  this.#treeAnimReady = false;
20788
21426
  this.#onKeyDown = (event) => {
21427
+ if (this.search.isOpen) return;
20789
21428
  const nodes = this.#getAllNodes();
20790
21429
  if (nodes.length === 0) return;
20791
21430
  if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
@@ -20906,6 +21545,19 @@ let TreeBaseElement = class TreeBaseElement extends i {
20906
21545
  get selectedNodes() {
20907
21546
  return this._selectedNodes;
20908
21547
  }
21548
+ #unregisterSearchCommands;
21549
+ #effectiveSearchModes() {
21550
+ const raw = this.searchModes?.trim();
21551
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
21552
+ const requested = new Set(raw.split(/\s+/));
21553
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
21554
+ }
21555
+ #syncSearchCommands() {
21556
+ if (!this.#ownedRegistry) return;
21557
+ this.search.setModes(this.#effectiveSearchModes());
21558
+ this.#unregisterSearchCommands?.();
21559
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
21560
+ }
20909
21561
  #pendingNode;
20910
21562
  #pendingController;
20911
21563
  #modelUnsubscribe;
@@ -20948,6 +21600,13 @@ let TreeBaseElement = class TreeBaseElement extends i {
20948
21600
  _isLeaving(node) {
20949
21601
  return this.#leaving.has(node.item.key);
20950
21602
  }
21603
+ /**
21604
+ * True when `filterPredicate` is set and neither `node` nor any descendant
21605
+ * matches — the row should carry the `filtered` class (`display: none`).
21606
+ */
21607
+ _isFilteredOut(node, depth) {
21608
+ return this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
21609
+ }
20951
21610
  /** Splice the fading ghost rows back into the visible list near where they were. */
20952
21611
  #weaveLeaving(nodes) {
20953
21612
  if (this.#leaving.size === 0) return nodes;
@@ -21044,11 +21703,14 @@ let TreeBaseElement = class TreeBaseElement extends i {
21044
21703
  execute: () => this.deselectAll()
21045
21704
  });
21046
21705
  this.registry = registry;
21706
+ this.#syncSearchCommands();
21047
21707
  }
21048
21708
  disconnectedCallback() {
21049
21709
  super.disconnectedCallback();
21050
21710
  this.#modelUnsubscribe?.();
21051
21711
  this.#modelUnsubscribe = void 0;
21712
+ this.#unregisterSearchCommands?.();
21713
+ this.#unregisterSearchCommands = void 0;
21052
21714
  if (this.#ownedRegistry) {
21053
21715
  this.#ownedRegistry.unregister(`select-all`);
21054
21716
  this.#ownedRegistry.unregister(`deselect-all`);
@@ -21146,6 +21808,28 @@ let TreeBaseElement = class TreeBaseElement extends i {
21146
21808
  if (!keyPath) return false;
21147
21809
  return this.navigateTo(keyPath);
21148
21810
  }
21811
+ /** Expand ancestors + move the cursor to `key`, without touching the selection (search "navigate" mode). */
21812
+ revealKey(key) {
21813
+ if (!this.root?.children) return false;
21814
+ const path = this.#findNodePath(this.root.children, (n) => n.item.key === key);
21815
+ if (!path) return false;
21816
+ for (let i = 0; i < path.length - 1; i++) {
21817
+ this.#applyExclusivity(path[i]);
21818
+ this._expandedKeys.add(path[i]);
21819
+ }
21820
+ this.tickled.setTickled(key);
21821
+ this.requestUpdate();
21822
+ this.updateComplete.then(() => this.#scrollKeyIntoView(key));
21823
+ return true;
21824
+ }
21825
+ /** Replace the selection with `nodes` (subject to `selectionFilter`) — search "select" mode. */
21826
+ selectMany(nodes) {
21827
+ const prev = new Set(this._selectedNodes);
21828
+ this._selectedNodes = new Set([...nodes].filter((n) => this.#canSelect(n)));
21829
+ this.selectedNode = [...this._selectedNodes].at(-1);
21830
+ this.#dispatchSelect(prev);
21831
+ this.requestUpdate();
21832
+ }
21149
21833
  getNodes() {
21150
21834
  if (!this.root?.children) return [];
21151
21835
  const result = [];
@@ -21549,7 +22233,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21549
22233
  node: n,
21550
22234
  depth: d
21551
22235
  });
21552
- if (this._expandedKeys.has(n.item.key) && n.children) for (const child of n.children) visit(child, d + 1);
22236
+ if (n.children && this._isExpanded(n, d)) for (const child of n.children) visit(child, d + 1);
21553
22237
  };
21554
22238
  if (node.children) for (const child of node.children) visit(child, depth + 1);
21555
22239
  return result;
@@ -21606,6 +22290,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21606
22290
  >
21607
22291
  ${this.root ? this.#renderNodes(nodes) : A}
21608
22292
  </div>
22293
+ ${this.search.renderField()}
21609
22294
  `;
21610
22295
  }
21611
22296
  #onDblClick;
@@ -21617,7 +22302,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21617
22302
  `;
21618
22303
  return b`
21619
22304
  <ul class="tree-list" role="tree">
21620
- ${this.#weaveLeaving(nodes).map(({ node, depth }) => this._renderNode(node, depth))}
22305
+ ${repeat(this.#weaveLeaving(nodes), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
21621
22306
  </ul>
21622
22307
  `;
21623
22308
  }
@@ -21627,7 +22312,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21627
22312
  const isBranch = node.item.isLeaf !== true;
21628
22313
  const isExpanded = this._isExpanded(node, depth);
21629
22314
  const isPending = this.#pendingNode === node;
21630
- const isFiltered = this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
22315
+ const isFiltered = this._isFilteredOut(node, depth);
21631
22316
  const isLeaving = this._isLeaving(node);
21632
22317
  return b`
21633
22318
  <li
@@ -21662,6 +22347,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21662
22347
  themeFallbacks,
21663
22348
  tickledStyles,
21664
22349
  tickledItemStyles,
22350
+ listSearchStyles,
21665
22351
  i$3`
21666
22352
  :host {
21667
22353
  --tree-bg: var(--surface-3);
@@ -21672,16 +22358,23 @@ let TreeBaseElement = class TreeBaseElement extends i {
21672
22358
  --tree-caret-size: 10px;
21673
22359
  --tree-radius: var(--radius-s);
21674
22360
 
22361
+ position: relative;
21675
22362
  display: block;
21676
- overflow: auto;
22363
+ overflow: hidden;
21677
22364
  box-sizing: border-box;
21678
22365
  background: var(--tree-bg);
21679
22366
  color: var(--tree-text);
21680
22367
  }
21681
22368
 
22369
+ /* min-height (not height) so the tree still grows to its content when the
22370
+ host has no explicit height; when it does, this fills it and scrolls.
22371
+ The scroll lives here, not on :host, so the search popover (a sibling of
22372
+ #container, positioned against :host) stays put while the list scrolls. */
21682
22373
  #container {
21683
22374
  outline: none;
21684
22375
  min-height: 100%;
22376
+ max-height: 100%;
22377
+ overflow: auto;
21685
22378
  }
21686
22379
 
21687
22380
  .tree-list {
@@ -21795,6 +22488,11 @@ __decorate([property({
21795
22488
  attribute: `disable-keyboard-navigation`
21796
22489
  })], TreeBaseElement.prototype, "disableKeyboardNavigation", void 0);
21797
22490
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "registry", void 0);
22491
+ __decorate([property({
22492
+ type: String,
22493
+ attribute: `search-modes`,
22494
+ reflect: true
22495
+ })], TreeBaseElement.prototype, "searchModes", void 0);
21798
22496
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
21799
22497
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
21800
22498
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
@@ -21875,9 +22573,10 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
21875
22573
  const isPending = this._pendingNode === node;
21876
22574
  const hasIconAtDepth = this.#depthsWithIcons.has(depth);
21877
22575
  const isLeaving = this._isLeaving(node);
22576
+ const isFiltered = this._isFilteredOut(node, depth);
21878
22577
  return b`
21879
22578
  <li
21880
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``}"
22579
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${isFiltered ? `filtered` : ``}"
21881
22580
  data-key=${node.item.key}
21882
22581
  role="treeitem"
21883
22582
  aria-selected=${selected}
@@ -22146,13 +22845,11 @@ function createTreeListAdapter(toTreeItem, options) {
22146
22845
  //#region src/vertical-list/vertical-list.ts
22147
22846
  const ITEM_SELECTOR = `li, [data-list-item]`;
22148
22847
  let VerticalListElement = class VerticalListElement extends i {
22149
- constructor(..._args2) {
22150
- super(..._args2);
22848
+ constructor(..._args) {
22849
+ super(..._args);
22151
22850
  this.selectionMode = `single`;
22152
22851
  this.interactionMode = `standard`;
22153
22852
  this.itemWrap = `stop`;
22154
- this._searchVisible = false;
22155
- this._searchQuery = ``;
22156
22853
  this.#customWrappers = /* @__PURE__ */ new Map();
22157
22854
  this.#wrappedElements = /* @__PURE__ */ new Set();
22158
22855
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
@@ -22184,6 +22881,11 @@ let VerticalListElement = class VerticalListElement extends i {
22184
22881
  onStateChange: () => this._syncItemStates(),
22185
22882
  itemWrap: this.itemWrap
22186
22883
  });
22884
+ this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
22885
+ itemSelector: ITEM_SELECTOR,
22886
+ getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
22887
+ highlightKey: `vertical-list-search`
22888
+ }) });
22187
22889
  this._onPointerOver = (e) => {
22188
22890
  const item = this._findListItem(e.composedPath());
22189
22891
  if (item) this.tickled.handlePointerEnter(item);
@@ -22192,11 +22894,7 @@ let VerticalListElement = class VerticalListElement extends i {
22192
22894
  this.tickled.handlePointerLeave(e);
22193
22895
  };
22194
22896
  this._onKeyDown = (e) => {
22195
- if ((e.ctrlKey || e.metaKey) && e.key === `f`) {
22196
- e.preventDefault();
22197
- this._openSearch();
22198
- return;
22199
- }
22897
+ if (this.search.isOpen) return;
22200
22898
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
22201
22899
  if (this.selection.canSelectAll()) {
22202
22900
  e.preventDefault();
@@ -22204,7 +22902,6 @@ let VerticalListElement = class VerticalListElement extends i {
22204
22902
  }
22205
22903
  return;
22206
22904
  }
22207
- if (this._searchVisible) return;
22208
22905
  switch (e.key) {
22209
22906
  case `ArrowDown`:
22210
22907
  e.preventDefault();
@@ -22252,40 +22949,31 @@ let VerticalListElement = class VerticalListElement extends i {
22252
22949
  const item = this._findListItem(e.composedPath());
22253
22950
  if (item) this._activateItem(item);
22254
22951
  };
22255
- this._onSearchInput = (e) => {
22256
- const input = e.target;
22257
- this._searchQuery = input.value;
22258
- this.#search?.setQuery(this._searchQuery);
22259
- };
22260
- this._onSearchKeydown = (e) => {
22261
- if (e.key === `Escape`) {
22262
- e.stopPropagation();
22263
- this._closeSearch();
22264
- } else if (e.key === `ArrowDown`) {
22265
- e.preventDefault();
22266
- this.tickled.handleKeyboardNavigation(`next`);
22267
- } else if (e.key === `ArrowUp`) {
22268
- e.preventDefault();
22269
- this.tickled.handleKeyboardNavigation(`previous`);
22270
- } else if (e.key === `Enter`) {
22271
- e.preventDefault();
22272
- e.stopPropagation();
22273
- const id = this.tickled.tickledId ?? this._getVisibleItems()[0];
22274
- if (id) {
22275
- this._dispatch(`list-item-click`, { item: id });
22276
- this._activateItem(id);
22277
- this._closeSearch();
22278
- }
22279
- }
22280
- };
22281
22952
  }
22282
- #search;
22283
22953
  #customWrappers;
22284
22954
  #wrappedElements;
22285
22955
  /** Watches for items added to the light DOM so they can be animated in. */
22286
22956
  #itemObserver;
22287
22957
  /** Items that have already played (or been skipped for) their enter animation. */
22288
22958
  #animatedItems;
22959
+ #searchRegistry;
22960
+ #unregisterSearchCommands;
22961
+ /** Command registry carrying this list's `list.search.*` commands. */
22962
+ get registry() {
22963
+ return this.#searchRegistry;
22964
+ }
22965
+ #effectiveSearchModes() {
22966
+ const raw = this.searchModes?.trim();
22967
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
22968
+ const requested = new Set(raw.split(/\s+/));
22969
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
22970
+ }
22971
+ #syncSearchCommands() {
22972
+ if (!this.#searchRegistry) return;
22973
+ this.search.setModes(this.#effectiveSearchModes());
22974
+ this.#unregisterSearchCommands?.();
22975
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
22976
+ }
22289
22977
  connectedCallback() {
22290
22978
  super.connectedCallback();
22291
22979
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -22297,6 +22985,8 @@ let VerticalListElement = class VerticalListElement extends i {
22297
22985
  for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
22298
22986
  this.#itemObserver ??= new MutationObserver((records) => this.#onItemsMutated(records));
22299
22987
  this.#itemObserver.observe(this, { childList: true });
22988
+ this.#searchRegistry ??= createCommandRegistry();
22989
+ this.#syncSearchCommands();
22300
22990
  }
22301
22991
  disconnectedCallback() {
22302
22992
  super.disconnectedCallback();
@@ -22306,8 +22996,9 @@ let VerticalListElement = class VerticalListElement extends i {
22306
22996
  this.removeEventListener(`click`, this._onClick);
22307
22997
  this.removeEventListener(`dblclick`, this._onDblClick);
22308
22998
  this.#itemObserver?.disconnect();
22309
- this.#search?.dispose();
22310
- this.#search = void 0;
22999
+ this.#unregisterSearchCommands?.();
23000
+ this.#unregisterSearchCommands = void 0;
23001
+ this.#searchRegistry = void 0;
22311
23002
  }
22312
23003
  /**
22313
23004
  * Animate in items appended to the light DOM (however they got there —
@@ -22330,17 +23021,9 @@ let VerticalListElement = class VerticalListElement extends i {
22330
23021
  if (fresh.length >= total) return;
22331
23022
  for (const el of fresh) animateListItemIn(el);
22332
23023
  }
22333
- firstUpdated() {
22334
- this.#search = createElementIncrSearch({
22335
- container: this,
22336
- itemSelector: ITEM_SELECTOR,
22337
- getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
22338
- getKey: (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
22339
- highlightKey: `vertical-list-search`
22340
- });
22341
- }
22342
23024
  updated(changed) {
22343
23025
  super.updated(changed);
23026
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
22344
23027
  if (changed.has(`interactionMode`)) {
22345
23028
  this.selection.clearAnchor();
22346
23029
  const previousSelected = this.selection.selected;
@@ -22408,11 +23091,13 @@ let VerticalListElement = class VerticalListElement extends i {
22408
23091
  if (this.interactionMode === `checked`) this._syncCheckboxes();
22409
23092
  }
22410
23093
  /**
22411
- * Dispatch a command to this list. No-op by default.
22412
- * Intended for use with `ixfx-grouped-item-lister`'s `dispatch()` method,
22413
- * which forwards commands to all sub-components.
23094
+ * Dispatch a command to this list used by `ixfx-grouped-item-lister` to
23095
+ * forward commands to all sub-components. Routes through the command registry
23096
+ * (so `list.search.*` and any other registered commands work).
22414
23097
  */
22415
- dispatch(_commandId, _args) {}
23098
+ dispatch(commandId, args) {
23099
+ this.#searchRegistry?.invoke(commandId, args);
23100
+ }
22416
23101
  select(item) {
22417
23102
  this.selection.replace(item);
22418
23103
  }
@@ -22622,22 +23307,15 @@ let VerticalListElement = class VerticalListElement extends i {
22622
23307
  composed: true
22623
23308
  }));
22624
23309
  }
22625
- _openSearch() {
22626
- this._searchVisible = true;
22627
- this.updateComplete.then(() => this._searchInputEl?.focus());
22628
- }
22629
- _closeSearch() {
22630
- this._searchVisible = false;
22631
- this._searchQuery = ``;
22632
- this.#search?.clearQuery();
22633
- this.focus();
22634
- }
22635
23310
  _activateItem(item) {
22636
23311
  if (this.interactionMode !== `manual`) this.selection.replace(item);
22637
23312
  this._dispatch(`list-activate`, { item });
22638
23313
  }
22639
23314
  static {
22640
- this.styles = [themeFallbacks, i$3`
23315
+ this.styles = [
23316
+ themeFallbacks,
23317
+ listSearchStyles,
23318
+ i$3`
22641
23319
  :host {
22642
23320
  display: block;
22643
23321
  position: relative;
@@ -22688,6 +23366,10 @@ let VerticalListElement = class VerticalListElement extends i {
22688
23366
  display: flex;
22689
23367
  align-items: center;
22690
23368
  padding: var(--ixfx-list-item-padding);
23369
+ /* Inset each row from the scroll container's edges (margin, not padding:
23370
+ padding is inside the border box, so the tickle/selection outlines
23371
+ would still sit flush against the edge and be clipped). */
23372
+ margin: 0 var(--list-gutter, 4px);
22691
23373
  min-height: var(--list-item-height, 28px);
22692
23374
  cursor: pointer;
22693
23375
  user-select: none;
@@ -22700,6 +23382,7 @@ let VerticalListElement = class VerticalListElement extends i {
22700
23382
  /* Custom shadow-DOM components handle their own internal layout/padding */
22701
23383
  ::slotted([data-list-item]) {
22702
23384
  display: block;
23385
+ margin: 0 var(--list-gutter, 4px);
22703
23386
  cursor: pointer;
22704
23387
  user-select: none;
22705
23388
  transition: background var(--transition, 0.1s ease-out),
@@ -22755,49 +23438,8 @@ let VerticalListElement = class VerticalListElement extends i {
22755
23438
  color: inherit;
22756
23439
  outline: var(--item-border-tickled);
22757
23440
  }
22758
-
22759
- .search-overlay {
22760
- position: absolute;
22761
- top: 8px;
22762
- right: 8px;
22763
- z-index: 10;
22764
- display: flex;
22765
- align-items: center;
22766
- gap: 4px;
22767
- background: var(--surface-4, #2a2a2a);
22768
- border: 1px solid var(--border, #444);
22769
- border-radius: var(--radius-m, 4px);
22770
- padding: 4px 6px;
22771
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
22772
- }
22773
-
22774
- .search-overlay[hidden] {
22775
- display: none;
22776
- }
22777
-
22778
- .search-input {
22779
- background: transparent;
22780
- border: none;
22781
- outline: none;
22782
- color: inherit;
22783
- font-size: var(--text-m, 0.875em);
22784
- min-width: 140px;
22785
- }
22786
-
22787
- .search-close {
22788
- background: transparent;
22789
- border: none;
22790
- color: var(--surface-muted-text, #888);
22791
- cursor: pointer;
22792
- font-size: 1.1em;
22793
- line-height: 1;
22794
- padding: 0 2px;
22795
- }
22796
-
22797
- .search-close:hover {
22798
- color: inherit;
22799
- }
22800
- `];
23441
+ `
23442
+ ];
22801
23443
  }
22802
23444
  render() {
22803
23445
  return b`
@@ -22805,21 +23447,7 @@ let VerticalListElement = class VerticalListElement extends i {
22805
23447
  <ul part="list">
22806
23448
  <slot></slot>
22807
23449
  </ul>
22808
- <div
22809
- class="search-overlay"
22810
- part="search-overlay"
22811
- ?hidden=${!this._searchVisible}
22812
- >
22813
- <input
22814
- type="text"
22815
- class="search-input"
22816
- placeholder="Search…"
22817
- .value=${this._searchQuery}
22818
- @input=${this._onSearchInput}
22819
- @keydown=${this._onSearchKeydown}
22820
- />
22821
- <button class="search-close" @click=${this._closeSearch} title="Close search (Esc)">×</button>
22822
- </div>
23450
+ ${this.search.renderField()}
22823
23451
  </div>
22824
23452
  `;
22825
23453
  }
@@ -22838,9 +23466,11 @@ __decorate([property({
22838
23466
  attribute: `item-wrap`
22839
23467
  })], VerticalListElement.prototype, "itemWrap", void 0);
22840
23468
  __decorate([property({ attribute: false })], VerticalListElement.prototype, "checkboxRenderer", void 0);
22841
- __decorate([state()], VerticalListElement.prototype, "_searchVisible", void 0);
22842
- __decorate([state()], VerticalListElement.prototype, "_searchQuery", void 0);
22843
- __decorate([query(`.search-input`)], VerticalListElement.prototype, "_searchInputEl", void 0);
23469
+ __decorate([property({
23470
+ type: String,
23471
+ attribute: `search-modes`,
23472
+ reflect: true
23473
+ })], VerticalListElement.prototype, "searchModes", void 0);
22844
23474
  VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
22845
23475
  //#endregion
22846
23476
  //#region src/grouped-item-lister/vertical-list-adapter.ts
@@ -27338,6 +27968,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27338
27968
  constructor(..._args) {
27339
27969
  super(..._args);
27340
27970
  this.headers = false;
27971
+ this.stretchLastColumn = false;
27341
27972
  this.interactionMode = `standard`;
27342
27973
  this.selectionFilter = `leaf`;
27343
27974
  this.exclusivity = `none`;
@@ -27349,6 +27980,10 @@ let MillerBaseElement = class MillerBaseElement extends i {
27349
27980
  this.#resizingDepth = -1;
27350
27981
  this.#resizeStartX = 0;
27351
27982
  this.#resizeStartWidth = 0;
27983
+ this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
27984
+ labelSelector: `.label`,
27985
+ highlightKey: `incr-search-highlight`
27986
+ }) });
27352
27987
  this.tickled = new TickledController(this, { getItems: () => {
27353
27988
  const items = [];
27354
27989
  for (let depth = 0; depth < this._columns.length; depth++) {
@@ -27358,6 +27993,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27358
27993
  return items;
27359
27994
  } });
27360
27995
  this.#onKeyDown = (event) => {
27996
+ if (this.search.isOpen) return;
27361
27997
  const currentDepth = this.#getTickledDepthOrDefault();
27362
27998
  const col = this._columns[currentDepth];
27363
27999
  if (!col || col.length === 0) {
@@ -27564,9 +28200,31 @@ let MillerBaseElement = class MillerBaseElement extends i {
27564
28200
  #resizeStartX;
27565
28201
  #resizeStartWidth;
27566
28202
  #modelUnsubscribe;
28203
+ #searchRegistry;
28204
+ #unregisterSearchCommands;
28205
+ /** Command registry carrying this component's `list.search.*` commands. */
28206
+ get registry() {
28207
+ return this.#searchRegistry;
28208
+ }
28209
+ #effectiveSearchModes() {
28210
+ const raw = this.searchModes?.trim();
28211
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
28212
+ const requested = new Set(raw.split(/\s+/));
28213
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
28214
+ }
28215
+ #syncSearchCommands() {
28216
+ if (!this.#searchRegistry) return;
28217
+ this.search.setModes(this.#effectiveSearchModes());
28218
+ this.#unregisterSearchCommands?.();
28219
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
28220
+ }
27567
28221
  #buildTickledId(node, depth) {
27568
28222
  return `${depth}:${node.item.key}`;
27569
28223
  }
28224
+ /** The node column 0 renders the children of: `viewRoot` when rebased, else `root`. */
28225
+ get #displayRoot() {
28226
+ return this.viewRoot ?? this.root;
28227
+ }
27570
28228
  #getTickledDepth() {
27571
28229
  const id = this.tickled.tickledId;
27572
28230
  if (!id) return void 0;
@@ -27575,16 +28233,20 @@ let MillerBaseElement = class MillerBaseElement extends i {
27575
28233
  return Number(id.substring(0, colonIndex));
27576
28234
  }
27577
28235
  updated(changedProps) {
28236
+ if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
27578
28237
  if (changedProps.has(`root`) && this.root) {
27579
28238
  this.#abortPending();
27580
- this._columns = this.root.children ? [this.root.children] : [];
28239
+ if (this.viewRoot !== void 0) this.viewRoot = void 0;
27581
28240
  this._selectedByDepth.clear();
27582
28241
  this._selectedKeys = /* @__PURE__ */ new Set();
27583
28242
  this._selectedNodes.clear();
27584
28243
  this.#lastSelectedNode = void 0;
27585
28244
  this.#anchor = void 0;
27586
28245
  this.tickled.clearTickled();
28246
+ const display = this.#displayRoot;
28247
+ this._columns = display?.children ? [display.children] : [];
27587
28248
  }
28249
+ if (changedProps.has(`viewRoot`)) this.#applyViewRoot();
27588
28250
  if (changedProps.has(`model`)) {
27589
28251
  this.#modelUnsubscribe?.();
27590
28252
  this.#modelUnsubscribe = void 0;
@@ -27626,10 +28288,18 @@ let MillerBaseElement = class MillerBaseElement extends i {
27626
28288
  });
27627
28289
  }
27628
28290
  }
28291
+ connectedCallback() {
28292
+ super.connectedCallback();
28293
+ this.#searchRegistry ??= createCommandRegistry();
28294
+ this.#syncSearchCommands();
28295
+ }
27629
28296
  disconnectedCallback() {
27630
28297
  super.disconnectedCallback();
27631
28298
  this.#modelUnsubscribe?.();
27632
28299
  this.#modelUnsubscribe = void 0;
28300
+ this.#unregisterSearchCommands?.();
28301
+ this.#unregisterSearchCommands = void 0;
28302
+ this.#searchRegistry = void 0;
27633
28303
  }
27634
28304
  getConfig() {
27635
28305
  const columnWidths = {};
@@ -27725,15 +28395,17 @@ let MillerBaseElement = class MillerBaseElement extends i {
27725
28395
  return results;
27726
28396
  }
27727
28397
  /**
27728
- * Programmatically navigate to a node by key path.
28398
+ * Programmatically navigate to a node by key path, relative to the current
28399
+ * view root (rebased or full tree).
27729
28400
  */
27730
28401
  navigateTo(keys) {
27731
- if (!this.root?.children) return false;
28402
+ const display = this.#displayRoot;
28403
+ if (!display?.children) return false;
27732
28404
  this.#abortPending();
27733
28405
  this._selectedByDepth.clear();
27734
28406
  this.tickled.clearTickled();
27735
- const newColumns = [this.root.children];
27736
- let currentColumn = this.root.children;
28407
+ const newColumns = [display.children];
28408
+ let currentColumn = display.children;
27737
28409
  let success = true;
27738
28410
  let lastNode;
27739
28411
  let lastDepth = 0;
@@ -27768,13 +28440,51 @@ let MillerBaseElement = class MillerBaseElement extends i {
27768
28440
  * Programmatically navigate to the first node (breadth-first) matching a predicate.
27769
28441
  */
27770
28442
  navigateToNode(predicate) {
27771
- if (!this.root?.children) return false;
27772
- const keyPath = this.#findNodePath(this.root.children, predicate);
28443
+ const display = this.#displayRoot;
28444
+ if (!display?.children) return false;
28445
+ const keyPath = this.#findNodePath(display.children, predicate);
27773
28446
  if (!keyPath) return false;
27774
28447
  return this.navigateTo(keyPath);
27775
28448
  }
28449
+ /** Open the columns down to `key` and move the cursor to it, without changing the selection (search "navigate" mode). */
28450
+ revealKey(key) {
28451
+ const display = this.#displayRoot;
28452
+ if (!display?.children) return false;
28453
+ const path = this.#findNodePath(display.children, (n) => n.item.key === key);
28454
+ if (!path) return false;
28455
+ const columns = [display.children];
28456
+ let current = display.children;
28457
+ let lastNode;
28458
+ let lastDepth = 0;
28459
+ for (let depth = 0; depth < path.length; depth++) {
28460
+ const node = current.find((n) => n.item.key === path[depth]);
28461
+ if (!node) return false;
28462
+ lastNode = node;
28463
+ lastDepth = depth;
28464
+ if (node.children && node.children.length > 0) {
28465
+ columns.push(node.children);
28466
+ current = node.children;
28467
+ } else break;
28468
+ }
28469
+ this._columns = columns;
28470
+ if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
28471
+ this.requestUpdate();
28472
+ this.#scrollNavigatedItemIntoView();
28473
+ return true;
28474
+ }
28475
+ /** Replace the selection with `nodes` (search "select" mode). */
28476
+ selectMany(nodes) {
28477
+ const arr = this.selectionFilter === `none` ? [] : [...nodes];
28478
+ const prev = new Set(this._selectedNodes);
28479
+ this._selectedNodes = new Set(arr);
28480
+ this._selectedKeys = new Set(arr.map((n) => n.item.key));
28481
+ this.#lastSelectedNode = arr.at(-1);
28482
+ this.#dispatchSelectEvent(prev);
28483
+ this.requestUpdate();
28484
+ }
27776
28485
  getNodes() {
27777
- if (!this.root?.children) return [];
28486
+ const display = this.#displayRoot;
28487
+ if (!display?.children) return [];
27778
28488
  const result = [];
27779
28489
  const visit = (node, depth) => {
27780
28490
  result.push({
@@ -27783,7 +28493,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27783
28493
  });
27784
28494
  if (node.children) for (const child of node.children) visit(child, depth + 1);
27785
28495
  };
27786
- for (const child of this.root.children) visit(child, 0);
28496
+ for (const child of display.children) visit(child, 0);
27787
28497
  return result;
27788
28498
  }
27789
28499
  #findNodePath(nodes, predicate, prefix = []) {
@@ -27796,6 +28506,124 @@ let MillerBaseElement = class MillerBaseElement extends i {
27796
28506
  }
27797
28507
  }
27798
28508
  }
28509
+ /**
28510
+ * Re-render the columns for the current `#displayRoot`, clearing selection.
28511
+ * Runs on every `viewRoot` change.
28512
+ */
28513
+ #applyViewRoot() {
28514
+ this.#abortPending();
28515
+ const prev = new Set(this._selectedNodes);
28516
+ this._selectedByDepth.clear();
28517
+ this._selectedKeys = /* @__PURE__ */ new Set();
28518
+ this._selectedNodes.clear();
28519
+ this.#lastSelectedNode = void 0;
28520
+ this.#anchor = void 0;
28521
+ this.tickled.clearTickled();
28522
+ const display = this.#displayRoot;
28523
+ if (display?.children !== void 0) {
28524
+ this._columns = display.children ? [display.children] : [];
28525
+ this._loadingDepth = void 0;
28526
+ } else if (display) {
28527
+ this._columns = [];
28528
+ this.#loadUnloadedChildren(display, -1, false);
28529
+ } else {
28530
+ this._columns = [];
28531
+ this._loadingDepth = void 0;
28532
+ }
28533
+ if (prev.size > 0) this.#dispatchSelectEvent(prev);
28534
+ }
28535
+ /**
28536
+ * Rebase the view to show `target`'s children in column 0 (Finder column-view
28537
+ * behavior on double-click), keeping the full tree in `root`.
28538
+ *
28539
+ * - Pass a `MillerNode` (used as-is) or a key (looked up in the full tree).
28540
+ * - Pass `undefined` to restore the full-tree view.
28541
+ * - Rebasing to the true root is equivalent to restoring the full view.
28542
+ * - Leaves are refused (returns `false`).
28543
+ *
28544
+ * Selection is cleared; all depths, headers, and key paths are relative to
28545
+ * the new view root. Fires a `rebase` event when the view actually changes.
28546
+ * Returns `false` when nothing changed (unknown key, leaf, or same view).
28547
+ */
28548
+ rebaseTo(target) {
28549
+ if (target === void 0) return this.#clearViewRoot();
28550
+ const node = typeof target === `string` ? this.#findNodeByKey(target) : target;
28551
+ if (!node || node.item.isLeaf === true) return false;
28552
+ if (node === this.viewRoot || node.item.key === this.viewRoot?.item.key) return false;
28553
+ if (node === this.root || !this.viewRoot && node.item.key === this.root?.item.key) return this.#clearViewRoot();
28554
+ this.viewRoot = node;
28555
+ this.#dispatchRebaseEvent();
28556
+ return true;
28557
+ }
28558
+ /**
28559
+ * Rebase one level up from the current view root (no-op at the full tree).
28560
+ * Fires a `rebase` event when the view actually changes.
28561
+ */
28562
+ rebaseUp() {
28563
+ if (!this.viewRoot) return false;
28564
+ const parent = this.#findParentOf(this.viewRoot);
28565
+ if (!parent) return false;
28566
+ if (parent === this.root) return this.#clearViewRoot();
28567
+ this.viewRoot = parent;
28568
+ this.#dispatchRebaseEvent();
28569
+ return true;
28570
+ }
28571
+ #clearViewRoot() {
28572
+ if (this.viewRoot === void 0) return false;
28573
+ this.viewRoot = void 0;
28574
+ this.#dispatchRebaseEvent();
28575
+ return true;
28576
+ }
28577
+ #dispatchRebaseEvent() {
28578
+ const viewRoot = this.viewRoot;
28579
+ const detail = {
28580
+ viewRoot,
28581
+ path: viewRoot ? this.#pathToRoot(viewRoot) : []
28582
+ };
28583
+ this.dispatchEvent(new CustomEvent(`rebase`, {
28584
+ detail,
28585
+ bubbles: true,
28586
+ composed: true
28587
+ }));
28588
+ }
28589
+ #findNodeByKey(key) {
28590
+ if (!this.root) return void 0;
28591
+ if (this.root.item.key === key) return this.root;
28592
+ return this.#findNodeIn(this.root.children ?? [], key);
28593
+ }
28594
+ #findNodeIn(nodes, key) {
28595
+ for (const node of nodes) {
28596
+ if (node.item.key === key) return node;
28597
+ if (node.children) {
28598
+ const found = this.#findNodeIn(node.children, key);
28599
+ if (found) return found;
28600
+ }
28601
+ }
28602
+ }
28603
+ /** Find the parent of `target` (by reference, falling back to key) in the full tree. */
28604
+ #findParentOf(target) {
28605
+ if (!this.root || target === this.root) return void 0;
28606
+ return this.#findParentIn(this.root, target);
28607
+ }
28608
+ #findParentIn(node, target) {
28609
+ if (!node.children) return void 0;
28610
+ for (const child of node.children) {
28611
+ if (child === target || child.item.key === target.item.key) return node;
28612
+ const found = this.#findParentIn(child, target);
28613
+ if (found) return found;
28614
+ }
28615
+ }
28616
+ /** Ancestry chain from the true root down to and including `target`. */
28617
+ #pathToRoot(target) {
28618
+ const chain = [target];
28619
+ let current = this.#findParentOf(target);
28620
+ while (current) {
28621
+ chain.unshift(current);
28622
+ if (current === this.root) break;
28623
+ current = this.#findParentOf(current);
28624
+ }
28625
+ return chain;
28626
+ }
27799
28627
  renderColumnContent(nodes, depth) {
27800
28628
  console.warn(`ixfx-miller-base: renderColumnContent not overridden. depth=${depth}, nodes=${nodes.length}`);
27801
28629
  return A;
@@ -27922,7 +28750,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27922
28750
  */
27923
28751
  #buildPath(depth) {
27924
28752
  const path = [];
27925
- if (this.root) path.push(this.root);
28753
+ if (this.#displayRoot) path.push(this.#displayRoot);
27926
28754
  for (let d = 0; d < depth; d++) {
27927
28755
  const sel = this._selectedByDepth.get(d);
27928
28756
  if (sel) path.push(sel);
@@ -28025,57 +28853,8 @@ let MillerBaseElement = class MillerBaseElement extends i {
28025
28853
  this.requestUpdate();
28026
28854
  return;
28027
28855
  }
28028
- if (node.children === void 0) {
28029
- this._columns = this._columns.slice(0, depth + 1);
28030
- this._loadingDepth = depth + 1;
28031
- this.#pendingNode = node;
28032
- const controller = new AbortController();
28033
- this.#pendingController = controller;
28034
- this.dispatchEvent(new CustomEvent(`expand`, {
28035
- detail: {
28036
- node,
28037
- depth
28038
- },
28039
- bubbles: true,
28040
- composed: true
28041
- }));
28042
- const applyChildren = (children) => {
28043
- if (controller.signal.aborted) return;
28044
- node.children = children;
28045
- this.#pendingNode = void 0;
28046
- this.#pendingController = void 0;
28047
- this._loadingDepth = void 0;
28048
- const cols = this._columns.slice(0, depth + 1);
28049
- if (children.length > 0) cols.push(children);
28050
- this._columns = cols;
28051
- };
28052
- const handleError = (err) => {
28053
- if (err.name !== `AbortError`) console.error(err);
28054
- node.children = [];
28055
- this.#pendingNode = void 0;
28056
- this.#pendingController = void 0;
28057
- this._loadingDepth = void 0;
28058
- this.requestUpdate();
28059
- };
28060
- if (this.model || this.loadChildren) {
28061
- if (this.#expandTimer !== void 0) clearTimeout(this.#expandTimer);
28062
- this.#expandTimer = setTimeout(() => {
28063
- this.#expandTimer = void 0;
28064
- if (this.#pendingNode !== node) return;
28065
- (this.model ? this.model.getChildren(node, depth, controller.signal) : collectDataResult(this.loadChildren({
28066
- node,
28067
- depth
28068
- }, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
28069
- }, EXPAND_DEBOUNCE_MS);
28070
- } else {
28071
- this._loadingDepth = void 0;
28072
- this.#pendingNode = void 0;
28073
- this.#pendingController = void 0;
28074
- const newColumns = this._columns.slice(0, depth + 1);
28075
- newColumns.push([]);
28076
- this._columns = newColumns;
28077
- }
28078
- } else {
28856
+ if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true);
28857
+ else {
28079
28858
  const newColumns = this._columns.slice(0, depth + 1);
28080
28859
  newColumns.push(node.children);
28081
28860
  this._columns = newColumns;
@@ -28089,6 +28868,68 @@ let MillerBaseElement = class MillerBaseElement extends i {
28089
28868
  }));
28090
28869
  }
28091
28870
  }
28871
+ /**
28872
+ * Fetch children for a node whose `children === undefined` and show them as
28873
+ * the column after `parentDepth` (with the loading placeholder meanwhile).
28874
+ * Shared by branch expansion (`parentDepth` = the branch's column) and view
28875
+ * rebasing (`parentDepth` = -1, so the children become column 0).
28876
+ *
28877
+ * The `depth` reported to `loadChildren`/`model.getChildren` is the node's
28878
+ * own depth: its column for expansions, -1 for a view root (it sits above
28879
+ * column 0, so loaders computing child depth as `depth + 1` stay correct).
28880
+ */
28881
+ #loadUnloadedChildren(node, parentDepth, fireExpandEvent) {
28882
+ const nodeDepth = parentDepth + 1;
28883
+ this._columns = this._columns.slice(0, nodeDepth);
28884
+ this._loadingDepth = nodeDepth;
28885
+ this.#pendingNode = node;
28886
+ const controller = new AbortController();
28887
+ this.#pendingController = controller;
28888
+ if (fireExpandEvent) this.dispatchEvent(new CustomEvent(`expand`, {
28889
+ detail: {
28890
+ node,
28891
+ depth: parentDepth
28892
+ },
28893
+ bubbles: true,
28894
+ composed: true
28895
+ }));
28896
+ const applyChildren = (children) => {
28897
+ if (controller.signal.aborted) return;
28898
+ node.children = children;
28899
+ this.#pendingNode = void 0;
28900
+ this.#pendingController = void 0;
28901
+ this._loadingDepth = void 0;
28902
+ const cols = this._columns.slice(0, nodeDepth);
28903
+ if (children.length > 0) cols.push(children);
28904
+ this._columns = cols;
28905
+ };
28906
+ const handleError = (err) => {
28907
+ if (err.name !== `AbortError`) console.error(err);
28908
+ node.children = [];
28909
+ this.#pendingNode = void 0;
28910
+ this.#pendingController = void 0;
28911
+ this._loadingDepth = void 0;
28912
+ this.requestUpdate();
28913
+ };
28914
+ if (this.model || this.loadChildren) {
28915
+ if (this.#expandTimer !== void 0) clearTimeout(this.#expandTimer);
28916
+ this.#expandTimer = setTimeout(() => {
28917
+ this.#expandTimer = void 0;
28918
+ if (this.#pendingNode !== node) return;
28919
+ (this.model ? this.model.getChildren(node, nodeDepth - 1, controller.signal) : collectDataResult(this.loadChildren({
28920
+ node,
28921
+ depth: nodeDepth - 1
28922
+ }, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
28923
+ }, EXPAND_DEBOUNCE_MS);
28924
+ } else {
28925
+ this._loadingDepth = void 0;
28926
+ this.#pendingNode = void 0;
28927
+ this.#pendingController = void 0;
28928
+ const newColumns = this._columns.slice(0, nodeDepth);
28929
+ newColumns.push([]);
28930
+ this._columns = newColumns;
28931
+ }
28932
+ }
28092
28933
  #onCheckboxClick(node) {
28093
28934
  const currentlySelected = this._selectedKeys.has(node.item.key);
28094
28935
  if (!!node.children?.length ? this.#allDescendantsSelected(node) : currentlySelected) this.#applyCheckedToggle(node, false);
@@ -28158,7 +28999,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
28158
28999
  #onResizeMove;
28159
29000
  #onResizeEnd;
28160
29001
  #getColumnHeader(depth) {
28161
- if (depth === 0) return this.root?.item.label ?? ``;
29002
+ if (depth === 0) return this.#displayRoot?.item.label ?? ``;
28162
29003
  const explicit = this._selectedByDepth.get(depth - 1);
28163
29004
  if (explicit) return explicit.item.label;
28164
29005
  const prevCol = this._columns[depth - 1];
@@ -28169,9 +29010,11 @@ let MillerBaseElement = class MillerBaseElement extends i {
28169
29010
  }
28170
29011
  return ``;
28171
29012
  }
28172
- #columnStyle(depth) {
29013
+ #columnStyle(depth, isLast) {
28173
29014
  const w = this.#columnWidths.get(depth);
28174
- return w !== void 0 ? `width:${w}px;flex-shrink:0` : `width:max-content`;
29015
+ if (w !== void 0) return `width:${w}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
29016
+ if (this.stretchLastColumn && isLast) return `width:max-content;flex-grow:1;flex-shrink:0;flex-basis:auto;min-width:0`;
29017
+ return `width:max-content;flex-grow:0;flex-shrink:0;flex-basis:auto`;
28175
29018
  }
28176
29019
  #isFocused(node, depth) {
28177
29020
  return this.tickled.isTickled(this.#buildTickledId(node, depth));
@@ -28184,8 +29027,9 @@ let MillerBaseElement = class MillerBaseElement extends i {
28184
29027
  }
28185
29028
  #renderLoadingColumn() {
28186
29029
  const depth = this._loadingDepth;
29030
+ const stretched = this.stretchLastColumn && this.#columnWidths.get(depth) === void 0;
28187
29031
  return b`
28188
- <div class="column loading-column" style=${this.#columnStyle(depth)}>
29032
+ <div class="column loading-column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, true)}>
28189
29033
  ${this.headers ? b`
28190
29034
  <div class="column-header">${this.#getColumnHeader(depth)}</div>
28191
29035
  ` : A}
@@ -28200,8 +29044,10 @@ let MillerBaseElement = class MillerBaseElement extends i {
28200
29044
  <div id="container" @keydown=${this.#onKeyDown} tabindex="0">
28201
29045
  ${this._columns.map((nodes, depth) => {
28202
29046
  const visibleNodes = this.filterPredicate ? nodes.filter((n) => this.filterPredicate(n, depth)) : nodes;
29047
+ const isLast = depth === this._columns.length - 1 && this._loadingDepth === void 0;
29048
+ const stretched = this.stretchLastColumn && isLast && this.#columnWidths.get(depth) === void 0;
28203
29049
  return b`
28204
- <div class="column" style=${this.#columnStyle(depth)}>
29050
+ <div class="column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, isLast)}>
28205
29051
  ${this.headers ? b`
28206
29052
  <div class="column-header"
28207
29053
  @click=${() => this.#onHeaderClick(depth)}
@@ -28220,6 +29066,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
28220
29066
  })}
28221
29067
  ${this._loadingDepth !== void 0 ? this.#renderLoadingColumn() : A}
28222
29068
  </div>
29069
+ ${this.search.renderField()}
28223
29070
  `;
28224
29071
  }
28225
29072
  _getFocusedIndex(depth) {
@@ -28255,6 +29102,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
28255
29102
  this.styles = [
28256
29103
  themeFallbacks,
28257
29104
  tickledStyles,
29105
+ listSearchStyles,
28258
29106
  i$3`
28259
29107
  :host {
28260
29108
  --column-bg: var(--surface-3);
@@ -28289,6 +29137,20 @@ let MillerBaseElement = class MillerBaseElement extends i {
28289
29137
  background: var(--column-bg);
28290
29138
  border-right: 1px solid var(--column-separator);
28291
29139
  flex-shrink: 0;
29140
+ /* Content-sized columns never grow wider than 80% of the available
29141
+ width — pathological labels truncate with ellipsis instead of
29142
+ pushing every other column out of view */
29143
+ max-width: 80%;
29144
+ /* Smoothly animate the last column stretching to fill remaining
29145
+ space and shrinking back to content width when it stops being last */
29146
+ transition: flex-grow var(--transition, 0.2s ease-out);
29147
+ }
29148
+
29149
+ .column.stretch-last {
29150
+ min-width: 0;
29151
+ /* Stretching fills remaining space by definition, so it is exempt
29152
+ from the 80% auto-size cap */
29153
+ max-width: none;
28292
29154
  }
28293
29155
 
28294
29156
  .column-header {
@@ -28372,6 +29234,12 @@ let MillerBaseElement = class MillerBaseElement extends i {
28372
29234
  __decorate([property({ type: Object })], MillerBaseElement.prototype, "root", void 0);
28373
29235
  __decorate([property({ attribute: false })], MillerBaseElement.prototype, "model", void 0);
28374
29236
  __decorate([property({ type: Boolean })], MillerBaseElement.prototype, "headers", void 0);
29237
+ __decorate([property({
29238
+ type: Boolean,
29239
+ attribute: `stretch-last-column`,
29240
+ reflect: true
29241
+ })], MillerBaseElement.prototype, "stretchLastColumn", void 0);
29242
+ __decorate([property({ attribute: false })], MillerBaseElement.prototype, "viewRoot", void 0);
28375
29243
  __decorate([property({ type: String })], MillerBaseElement.prototype, "interactionMode", void 0);
28376
29244
  __decorate([property({
28377
29245
  reflect: true,
@@ -28383,6 +29251,11 @@ __decorate([property({
28383
29251
  })], MillerBaseElement.prototype, "exclusivity", void 0);
28384
29252
  __decorate([property({ type: Object })], MillerBaseElement.prototype, "loadChildren", void 0);
28385
29253
  __decorate([property({ attribute: false })], MillerBaseElement.prototype, "filterPredicate", void 0);
29254
+ __decorate([property({
29255
+ type: String,
29256
+ attribute: `search-modes`,
29257
+ reflect: true
29258
+ })], MillerBaseElement.prototype, "searchModes", void 0);
28386
29259
  __decorate([state()], MillerBaseElement.prototype, "_columns", void 0);
28387
29260
  __decorate([state()], MillerBaseElement.prototype, "_loadingDepth", void 0);
28388
29261
  __decorate([state()], MillerBaseElement.prototype, "_selectedNodes", void 0);
@@ -28620,14 +29493,28 @@ var MillerTreeController = class {
28620
29493
  * Any ancestor `MillerNode`s with unloaded children are populated
28621
29494
  * synchronously before calling `element.navigateTo()`.
28622
29495
  *
29496
+ * Key paths are relative to the element's current view root: when the view
29497
+ * is rebased, only matches at or below the view root can be reached.
29498
+ *
28623
29499
  * Returns `false` if no matching node is found.
28624
29500
  */
28625
29501
  navigateToValue(predicate) {
28626
29502
  const treePath = this.#findTreePath(this.#root, predicate);
28627
29503
  if (!treePath) return false;
28628
- let currentColumn = this.#element.root?.children ?? [];
29504
+ const base = this.#element.viewRoot ?? this.#element.root;
29505
+ if (!base) return false;
29506
+ let startIndex = 1;
29507
+ if (this.#element.viewRoot) {
29508
+ const mappedBase = this.#nodeToTree.get(base);
29509
+ if (!mappedBase) return false;
29510
+ const baseIndex = treePath.indexOf(mappedBase);
29511
+ if (baseIndex < 0) return false;
29512
+ startIndex = baseIndex + 1;
29513
+ if (base.children === void 0) base.children = this.#childrenOf(mappedBase);
29514
+ }
29515
+ let currentColumn = base.children ?? [];
28629
29516
  const keys = [];
28630
- for (let i = 1; i < treePath.length; i++) {
29517
+ for (let i = startIndex; i < treePath.length; i++) {
28631
29518
  const treeNode = treePath[i];
28632
29519
  const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
28633
29520
  keys.push(key);
@@ -37455,29 +38342,24 @@ var TreeController = class {
37455
38342
  navigateToValue(predicate) {
37456
38343
  const treePath = this.#findTreePath(this.#root, predicate);
37457
38344
  if (!treePath) return false;
38345
+ let currentColumn = this.#element.root?.children ?? [];
37458
38346
  const keys = [];
37459
38347
  for (let i = 1; i < treePath.length; i++) {
37460
38348
  const treeNode = treePath[i];
37461
38349
  const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
37462
38350
  keys.push(key);
37463
38351
  if (i < treePath.length - 1) {
37464
- const currentChildren = i === 1 ? this.#element.root?.children ?? [] : this.#findInChildren(this.#element.root?.children ?? [], keys);
37465
- const treeNodeMap = this.#nodeToTree;
37466
- const node = currentChildren.find((n) => n.item.key === key);
38352
+ const node = currentColumn.find((n) => n.item.key === key);
37467
38353
  if (!node) return false;
37468
38354
  if (node.children === void 0) {
37469
- const mapped = treeNodeMap.get(node);
38355
+ const mapped = this.#nodeToTree.get(node);
37470
38356
  if (mapped) node.children = this.#childrenOf(mapped);
37471
38357
  }
38358
+ currentColumn = node.children ?? [];
37472
38359
  }
37473
38360
  }
37474
38361
  return this.#element.navigateTo(keys);
37475
38362
  }
37476
- #findInChildren(children, keys) {
37477
- if (keys.length === 0) return children;
37478
- const key = keys[0];
37479
- return children.find((n) => n.item.key === key)?.children ?? [];
37480
- }
37481
38363
  #childrenOf(treeNode) {
37482
38364
  return Array.from(treeNode.children(), (child) => this.#toTreeNode(child));
37483
38365
  }
@@ -38642,6 +39524,6 @@ function init() {
38642
39524
  console.log(`Init`);
38643
39525
  }
38644
39526
  //#endregion
38645
- export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, readBaseColour, registerFontIcon, registerIcon, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
39527
+ export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
38646
39528
 
38647
39529
  //# sourceMappingURL=index.js.map