@ixfx/components 0.7.4 → 0.8.0

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 (101) hide show
  1. package/bundle/index.d.ts +787 -190
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2770 -592
  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-D-z7bx5-.d.ts} +26 -1
  11. package/dist/data-grid-D-z7bx5-.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-_ZJHYEBr.d.ts} +20 -12
  16. package/dist/detail-list-_ZJHYEBr.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-nRUQOcEL.js +1537 -0
  21. package/dist/grid-list-nRUQOcEL.js.map +1 -0
  22. package/dist/{grid-list-B_orPsot.d.ts → grid-list-u33vmKzb.d.ts} +128 -10
  23. package/dist/grid-list-u33vmKzb.d.ts.map +1 -0
  24. package/dist/grid-list.d.ts +1 -1
  25. package/dist/grid-list.js +1 -932
  26. package/dist/incr-search-C05WjbZh.js +999 -0
  27. package/dist/incr-search-C05WjbZh.js.map +1 -0
  28. package/dist/incr-search.d.ts +2 -2
  29. package/dist/incr-search.js +2 -3
  30. package/dist/index-h6gxtfHH.d.ts +397 -0
  31. package/dist/index-h6gxtfHH.d.ts.map +1 -0
  32. package/dist/index.d.ts +62 -13
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +343 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/led.d.ts +33 -1
  37. package/dist/led.d.ts.map +1 -1
  38. package/dist/led.js +157 -13
  39. package/dist/led.js.map +1 -1
  40. package/dist/{list-selection-controller-CUo-Hhcm.js → list-selection-controller-DTFI40li.js} +2 -76
  41. package/dist/list-selection-controller-DTFI40li.js.map +1 -0
  42. package/dist/miller.d.ts +81 -3
  43. package/dist/miller.d.ts.map +1 -1
  44. package/dist/miller.js +327 -68
  45. package/dist/miller.js.map +1 -1
  46. package/dist/narrowed-text.js +1 -2
  47. package/dist/narrowed-text.js.map +1 -1
  48. package/dist/panel.d.ts +3 -1
  49. package/dist/panel.d.ts.map +1 -1
  50. package/dist/panel.js +31 -1
  51. package/dist/panel.js.map +1 -1
  52. package/dist/plots.d.ts +2 -2
  53. package/dist/plots.js +1 -1
  54. package/dist/{titlebar-Bj5pfdw_.d.ts → titlebar-YQ-jVQoS.d.ts} +2 -2
  55. package/dist/{titlebar-Bj5pfdw_.d.ts.map → titlebar-YQ-jVQoS.d.ts.map} +1 -1
  56. package/dist/titlebar.d.ts +1 -1
  57. package/dist/titlebar.js +83 -1
  58. package/dist/titlebar.js.map +1 -1
  59. package/dist/{tree-B74zdI60.js → tree-Chptss7I.js} +77 -15
  60. package/dist/tree-Chptss7I.js.map +1 -0
  61. package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
  62. package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
  63. package/dist/tree.d.ts +22 -1
  64. package/dist/tree.d.ts.map +1 -1
  65. package/dist/tree.js +1 -1
  66. package/dist/{vertical-list-CQSEVeOT.d.ts → vertical-list-CFuFDygi.d.ts} +17 -14
  67. package/dist/vertical-list-CFuFDygi.d.ts.map +1 -0
  68. package/dist/vertical-list.d.ts +1 -1
  69. package/dist/vertical-list.js +59 -127
  70. package/dist/vertical-list.js.map +1 -1
  71. package/dist/{xy-axis-5KhVTNoX.d.ts → xy-axis-BfP9hKLj.d.ts} +25 -2
  72. package/dist/{xy-axis-5KhVTNoX.d.ts.map → xy-axis-BfP9hKLj.d.ts.map} +1 -1
  73. package/dist/{xy-axis-VrX8dmvS.js → xy-axis-DzM1brBZ.js} +141 -8
  74. package/dist/xy-axis-DzM1brBZ.js.map +1 -0
  75. package/docs-user/data-grid.md +19 -0
  76. package/docs-user/detail-list.md +1 -1
  77. package/docs-user/grid-list.md +120 -11
  78. package/docs-user/incr-search.md +12 -2
  79. package/docs-user/index.json +3 -3
  80. package/docs-user/led.md +37 -7
  81. package/docs-user/llms.txt +1 -1
  82. package/docs-user/panel.md +68 -13
  83. package/docs-user/titlebar.md +3 -4
  84. package/docs-user/tree.md +22 -1
  85. package/docs-user/vertical-list.md +7 -1
  86. package/llms.txt +1 -1
  87. package/package.json +1 -1
  88. package/dist/data-grid-BddUjAti.d.ts.map +0 -1
  89. package/dist/detail-list-Br0WAnry.d.ts.map +0 -1
  90. package/dist/element-search-Y8SRfPGM.js +0 -185
  91. package/dist/element-search-Y8SRfPGM.js.map +0 -1
  92. package/dist/grid-list-B_orPsot.d.ts.map +0 -1
  93. package/dist/grid-list.js.map +0 -1
  94. package/dist/incr-search-vCzuao-2.js +0 -137
  95. package/dist/incr-search-vCzuao-2.js.map +0 -1
  96. package/dist/index-Cu5i8zZY.d.ts +0 -160
  97. package/dist/index-Cu5i8zZY.d.ts.map +0 -1
  98. package/dist/list-selection-controller-CUo-Hhcm.js.map +0 -1
  99. package/dist/tree-B74zdI60.js.map +0 -1
  100. package/dist/vertical-list-CQSEVeOT.d.ts.map +0 -1
  101. package/dist/xy-axis-VrX8dmvS.js.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,186 @@ var IncrSearchTreeController = class {
2498
2499
  }
2499
2500
  };
2500
2501
  //#endregion
2502
+ //#region src/incr-search/data-list-search-adapter.ts
2503
+ /**
2504
+ * A {@link ListSearchAdapter} over a data-backed list. Unlike the element
2505
+ * adapter it never materialises an element per item — it reads keys and text
2506
+ * straight from the source by index. Highlights are painted on whatever
2507
+ * captions are currently rendered in the shadow window; offscreen matches paint
2508
+ * when scrolled into view. See `docs/infra-incremental.md`.
2509
+ */
2510
+ function createDataListSearchAdapter(host, options) {
2511
+ return {
2512
+ getItems: () => Array.from({ length: options.rawCount() }, (_, i) => ({
2513
+ key: options.rawKeyAt(i),
2514
+ index: i
2515
+ })),
2516
+ getLabel: (item) => options.rawSearchTextAt(item.index),
2517
+ getKey: (item) => item.key,
2518
+ focusItem: (item) => {
2519
+ if (item) host.focusKey(item.key);
2520
+ },
2521
+ activateItem: (item) => {
2522
+ if (options.onActivate) options.onActivate(item.key);
2523
+ else host.focusKey(item.key);
2524
+ },
2525
+ selectItems: (items) => {
2526
+ host.selectKeys(items.map((i) => i.key));
2527
+ },
2528
+ applyFilter: (matched) => {
2529
+ options.onFilterChange?.(matched);
2530
+ },
2531
+ highlightRoot: options.getHighlightRoot,
2532
+ highlightSelector: `.cell .caption`,
2533
+ highlightKeyOf: (el) => el.closest(`.cell`)?.getAttribute(`data-key`) ?? void 0,
2534
+ highlightText: (el) => el.textContent ?? ``,
2535
+ highlightKey: `grid-list-search`
2536
+ };
2537
+ }
2538
+ //#endregion
2539
+ //#region src/interaction/keyed-list.ts
2540
+ /**
2541
+ * Helpers for key-based selection on the Element-flavoured list components
2542
+ * (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
2543
+ *
2544
+ * A "key" is a stable string the host puts on each item element — `data-key`
2545
+ * (preferred) or `data-value`. It survives the host rebuilding the item set
2546
+ * (re-fetch, re-sort, filter) where raw `Element` references do not.
2547
+ */
2548
+ /**
2549
+ * The stable key for an item element: `data-key`, then `data-value`, else
2550
+ * `undefined`.
2551
+ */
2552
+ function keyForElement(el) {
2553
+ return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
2554
+ }
2555
+ /**
2556
+ * A lazily-built key → element index over a container. Rebuilt on first use
2557
+ * and after any child-list mutation under `root`; `invalidate()` forces a
2558
+ * rebuild on the next `get()`.
2559
+ *
2560
+ * ```ts
2561
+ * #keyIndex = new KeyedElementIndex(this);
2562
+ * // ...
2563
+ * const el = this.#keyIndex.get(`row-42`);
2564
+ * // on disconnect:
2565
+ * this.#keyIndex.dispose();
2566
+ * ```
2567
+ */
2568
+ var KeyedElementIndex = class {
2569
+ #root;
2570
+ #selector;
2571
+ #map = /* @__PURE__ */ new Map();
2572
+ #dirty = true;
2573
+ #observer;
2574
+ /**
2575
+ * @param root Container whose descendants are indexed.
2576
+ * @param itemSelector Which descendants count as items (default: any `[data-key]`).
2577
+ */
2578
+ constructor(root, itemSelector = `[data-key]`) {
2579
+ this.#root = root;
2580
+ this.#selector = itemSelector;
2581
+ this.#observer = new MutationObserver(() => {
2582
+ this.#dirty = true;
2583
+ });
2584
+ this.#observer.observe(root, {
2585
+ childList: true,
2586
+ subtree: true
2587
+ });
2588
+ }
2589
+ /** The element with this key, or `undefined`. First match wins on duplicates. */
2590
+ get(key) {
2591
+ if (this.#dirty) this.#rebuild();
2592
+ return this.#map.get(key);
2593
+ }
2594
+ /** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
2595
+ invalidate() {
2596
+ this.#dirty = true;
2597
+ }
2598
+ /** Stop observing and drop the cache. */
2599
+ dispose() {
2600
+ this.#observer.disconnect();
2601
+ this.#map.clear();
2602
+ }
2603
+ #rebuild() {
2604
+ this.#map.clear();
2605
+ for (const el of this.#root.querySelectorAll(this.#selector)) {
2606
+ const key = keyForElement(el);
2607
+ if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
2608
+ }
2609
+ this.#dirty = false;
2610
+ }
2611
+ };
2612
+ //#endregion
2613
+ //#region src/incr-search/element-list-search-adapter.ts
2614
+ function defaultLabel(el) {
2615
+ return el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``;
2616
+ }
2617
+ /**
2618
+ * A {@link ListSearchAdapter} over the light-DOM item elements of a list
2619
+ * component. Filter mode toggles `hidden` (only on elements it hid itself, so a
2620
+ * consumer's own `hidden` items are left alone). Highlights are painted on the
2621
+ * item elements in the light DOM — the consuming page styles
2622
+ * `::highlight(<highlightKey>)`. See `docs/infra-incremental.md`.
2623
+ */
2624
+ function createElementListSearchAdapter(host, options = {}) {
2625
+ const itemSelector = options.itemSelector ?? `:scope > *`;
2626
+ const getLabel = options.getLabel ?? defaultLabel;
2627
+ const getKey = options.getKey ?? ((el) => keyForElement(el) ?? defaultLabel(el));
2628
+ const hidden = /* @__PURE__ */ new Set();
2629
+ const items = () => Array.from(host.querySelectorAll(itemSelector));
2630
+ return {
2631
+ getItems: items,
2632
+ getLabel,
2633
+ getKey,
2634
+ focusItem: (el) => {
2635
+ if (el) host.focusKey(getKey(el));
2636
+ },
2637
+ activateItem: (el) => {
2638
+ el.dispatchEvent(new MouseEvent(`click`, {
2639
+ bubbles: true,
2640
+ composed: true
2641
+ }));
2642
+ },
2643
+ selectItems: (els) => {
2644
+ host.selectKeys(els.map(getKey));
2645
+ },
2646
+ applyFilter: (matched) => {
2647
+ if (matched === void 0) {
2648
+ for (const el of hidden) el.hidden = false;
2649
+ hidden.clear();
2650
+ return;
2651
+ }
2652
+ for (const el of items()) if (matched.has(getKey(el))) {
2653
+ if (hidden.delete(el)) el.hidden = false;
2654
+ } else {
2655
+ el.hidden = true;
2656
+ hidden.add(el);
2657
+ }
2658
+ },
2659
+ highlightRoot: () => host,
2660
+ highlightSelector: itemSelector,
2661
+ highlightKeyOf: (el) => getKey(el),
2662
+ highlightText: getLabel,
2663
+ highlightKey: options.highlightKey,
2664
+ container: () => host
2665
+ };
2666
+ }
2667
+ //#endregion
2501
2668
  //#region src/incr-search/element-search.ts
2502
2669
  const defaultGetLabel = (el) => el.textContent?.trim() ?? ``;
2503
2670
  function defaultGetKey(el) {
2504
2671
  return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? el.textContent?.trim() ?? ``;
2505
2672
  }
2673
+ /**
2674
+ * DOM filter-search over a container's item elements (sets `hidden` on
2675
+ * non-matches, applies highlights).
2676
+ *
2677
+ * @deprecated The list components (`ixfx-grid-list`, `ixfx-vertical-list`,
2678
+ * `ixfx-detail-list`, …) now have a built-in navigate / select / filter search
2679
+ * popover — see `docs/infra-incremental.md` and `ListSearchController`. Kept for
2680
+ * standalone / non-component use.
2681
+ */
2506
2682
  function createElementIncrSearch(opts) {
2507
2683
  const { container, itemSelector, getLabel = defaultGetLabel, getKey = defaultGetKey, labelSelector, highlightKey } = opts;
2508
2684
  let elementByKey = /* @__PURE__ */ new Map();
@@ -2576,6 +2752,512 @@ function createElementIncrSearch(opts) {
2576
2752
  };
2577
2753
  }
2578
2754
  //#endregion
2755
+ //#region src/incr-search/list-search-commands.ts
2756
+ /** Command ids registered by {@link registerListSearchCommands}, keyed by mode. */
2757
+ const LIST_SEARCH_COMMAND_IDS = {
2758
+ navigate: `list.search.navigate`,
2759
+ select: `list.search.select`,
2760
+ filter: `list.search.filter`
2761
+ };
2762
+ const LABELS = {
2763
+ navigate: {
2764
+ label: `Find`,
2765
+ description: `Type to move the cursor to the best match`
2766
+ },
2767
+ select: {
2768
+ label: `Find & Select`,
2769
+ description: `Type to select every matching item`
2770
+ },
2771
+ filter: {
2772
+ label: `Filter`,
2773
+ description: `Type to hide items that don't match`
2774
+ }
2775
+ };
2776
+ /**
2777
+ * Register one command per mode the controller offers (see
2778
+ * `controller.registeredModes`). Each command opens the search popover in its
2779
+ * mode. Returns a function that unregisters them again — call it from
2780
+ * `disconnectedCallback`. See `docs/infra-incremental.md`.
2781
+ */
2782
+ function registerListSearchCommands(registry, controller, options = {}) {
2783
+ const id = (mode) => options.idPrefix ? `${options.idPrefix}.search.${mode}` : LIST_SEARCH_COMMAND_IDS[mode];
2784
+ const registered = [];
2785
+ for (const mode of controller.registeredModes) {
2786
+ const commandId = id(mode);
2787
+ if (registry.has(commandId)) continue;
2788
+ registry.register({
2789
+ id: commandId,
2790
+ label: LABELS[mode].label,
2791
+ description: LABELS[mode].description,
2792
+ execute: () => controller.open(mode)
2793
+ });
2794
+ registered.push(commandId);
2795
+ }
2796
+ return () => {
2797
+ for (const commandId of registered) registry.unregister(commandId);
2798
+ };
2799
+ }
2800
+ //#endregion
2801
+ //#region src/incr-search/list-search-controller.ts
2802
+ /** Canonical order — used for the switch-row and to normalise a requested set. */
2803
+ const LIST_SEARCH_MODES = [
2804
+ `navigate`,
2805
+ `select`,
2806
+ `filter`
2807
+ ];
2808
+ const DEFAULT_HIGHLIGHT_KEY = `incr-search-highlight`;
2809
+ const INPUT_CLASS = `list-search-input`;
2810
+ /**
2811
+ * Shared brain for the three-mode incremental-search popover. A Lit
2812
+ * `ReactiveController`: the host adds `listSearchStyles` to `static styles`,
2813
+ * drops `${controller.renderField()}` into `render()`, and registers the
2814
+ * commands with {@link registerListSearchCommands}. See `docs/infra-incremental.md`.
2815
+ */
2816
+ var ListSearchController = class {
2817
+ #host;
2818
+ #adapter;
2819
+ #modes;
2820
+ #placeholder;
2821
+ #search;
2822
+ #unsub;
2823
+ #observer;
2824
+ #itemByKey = /* @__PURE__ */ new Map();
2825
+ #open = false;
2826
+ #mode = `filter`;
2827
+ #query = ``;
2828
+ #matchIndex = 0;
2829
+ #matches = [];
2830
+ /** Whether a filter has been pushed to the adapter and still needs undoing. */
2831
+ #filterActive = false;
2832
+ constructor(host, options) {
2833
+ this.#host = host;
2834
+ this.#adapter = options.adapter;
2835
+ const requested = new Set(options.modes ?? LIST_SEARCH_MODES);
2836
+ this.#modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
2837
+ this.#mode = this.#modes[0] ?? `filter`;
2838
+ this.#placeholder = options.placeholder ?? `Search…`;
2839
+ this.#search = createIncrSearch({
2840
+ selector: (i) => i.label,
2841
+ fzf: options.fzf
2842
+ });
2843
+ this.#unsub = this.#search.onChange((state) => this.#onState(state));
2844
+ host.addController(this);
2845
+ }
2846
+ /** `false` when no modes are available — the host should register no commands and render no field. */
2847
+ get enabled() {
2848
+ return this.#modes.length > 0;
2849
+ }
2850
+ /** The modes actually offered (component capability ∩ consumer request). */
2851
+ get registeredModes() {
2852
+ return this.#modes;
2853
+ }
2854
+ /**
2855
+ * Replace the offered mode set (normalised to {@link LIST_SEARCH_MODES}
2856
+ * order). If the current mode is dropped the popover closes and falls back to
2857
+ * the first remaining mode. Callers should re-run
2858
+ * {@link registerListSearchCommands} afterwards.
2859
+ */
2860
+ setModes(modes) {
2861
+ const set = new Set(modes);
2862
+ this.#modes = LIST_SEARCH_MODES.filter((m) => set.has(m));
2863
+ if (!this.#modes.includes(this.#mode)) {
2864
+ if (this.#open) this.close();
2865
+ this.#mode = this.#modes[0] ?? `filter`;
2866
+ }
2867
+ this.#host.requestUpdate();
2868
+ }
2869
+ get isOpen() {
2870
+ return this.#open;
2871
+ }
2872
+ get mode() {
2873
+ return this.#mode;
2874
+ }
2875
+ get query() {
2876
+ return this.#query;
2877
+ }
2878
+ get matchCount() {
2879
+ return this.#matches.length;
2880
+ }
2881
+ get matchIndex() {
2882
+ return this.#matchIndex;
2883
+ }
2884
+ hostConnected() {
2885
+ const container = this.#adapter.container?.();
2886
+ if (container) {
2887
+ this.#observer = new MutationObserver(() => this.refresh());
2888
+ this.#observer.observe(container, {
2889
+ childList: true,
2890
+ subtree: false
2891
+ });
2892
+ }
2893
+ this.refresh();
2894
+ }
2895
+ hostDisconnected() {
2896
+ this.#observer?.disconnect();
2897
+ this.#observer = void 0;
2898
+ if (this.#open) this.#teardownEffects();
2899
+ this.#open = false;
2900
+ this.#unsub?.();
2901
+ this.#unsub = void 0;
2902
+ this.#search.dispose();
2903
+ }
2904
+ /** Open the popover in `mode` (clamped to the available set). */
2905
+ open(mode) {
2906
+ if (!this.enabled) return;
2907
+ this.#mode = this.#modes.includes(mode) ? mode : this.#modes[0];
2908
+ this.#open = true;
2909
+ this.#query = ``;
2910
+ this.#matchIndex = 0;
2911
+ this.#matches = [];
2912
+ this.refresh();
2913
+ this.#search.setQuery(``);
2914
+ this.#host.requestUpdate();
2915
+ this.#host.updateComplete.then(() => this.#focusInput());
2916
+ }
2917
+ /** Close the popover, restoring any live filter. Selection / cursor stay put. */
2918
+ close() {
2919
+ if (!this.#open) return;
2920
+ this.#open = false;
2921
+ this.#teardownEffects();
2922
+ this.#query = ``;
2923
+ this.#matches = [];
2924
+ this.#search.clearQuery();
2925
+ this.#host.requestUpdate();
2926
+ this.#host.focus();
2927
+ }
2928
+ setQuery(query) {
2929
+ this.#query = query;
2930
+ this.#matchIndex = 0;
2931
+ this.#search.setQuery(query);
2932
+ }
2933
+ /** Re-interpret the current query under a new mode (undoing the old mode's effect). */
2934
+ switchMode(next) {
2935
+ if (!this.#modes.includes(next) || next === this.#mode) return;
2936
+ if (this.#mode === `filter` && this.#filterActive) {
2937
+ this.#adapter.applyFilter(void 0);
2938
+ this.#filterActive = false;
2939
+ }
2940
+ this.#mode = next;
2941
+ this.#applyMode();
2942
+ this.#host.requestUpdate();
2943
+ this.#host.updateComplete.then(() => this.#focusInput());
2944
+ }
2945
+ /** One-shot: turn the current match set into the selection, staying in the current mode. */
2946
+ selectMatches() {
2947
+ if (!this.#modes.includes(`select`)) return;
2948
+ this.#adapter.selectItems(this.#currentItems());
2949
+ this.#host.requestUpdate();
2950
+ }
2951
+ nextMatch() {
2952
+ this.#cycle(1);
2953
+ }
2954
+ prevMatch() {
2955
+ this.#cycle(-1);
2956
+ }
2957
+ /** Enter: in `navigate` activate the current match; then close. */
2958
+ commit() {
2959
+ if (this.#mode === `navigate`) {
2960
+ const item = this.#itemFor(this.#matches[this.#matchIndex]);
2961
+ if (item) {
2962
+ if (this.#adapter.activateItem) this.#adapter.activateItem(item);
2963
+ else this.#adapter.focusItem(item);
2964
+ }
2965
+ }
2966
+ this.close();
2967
+ }
2968
+ /** Rebuild the searchable item set from the adapter. Called on open and on container mutation. */
2969
+ refresh() {
2970
+ this.#itemByKey.clear();
2971
+ const items = [];
2972
+ for (const it of this.#adapter.getItems()) {
2973
+ const key = this.#adapter.getKey(it);
2974
+ this.#itemByKey.set(key, it);
2975
+ items.push({
2976
+ key,
2977
+ label: this.#adapter.getLabel(it)
2978
+ });
2979
+ }
2980
+ this.#search.setItems(items);
2981
+ }
2982
+ /** Popover markup. Pair with `listSearchStyles` in the host's `static styles`. */
2983
+ renderField() {
2984
+ if (!this.enabled) return A;
2985
+ const hasQuery = this.#query.trim().length > 0;
2986
+ const others = this.#modes.filter((m) => m !== this.#mode);
2987
+ const showSelectMatches = this.#modes.includes(`select`) && this.#mode !== `select`;
2988
+ return b`
2989
+ <div class="list-search" part="search" ?hidden=${!this.#open}>
2990
+ <div class="list-search-row">
2991
+ <span class="list-search-mode">${this.#mode}</span>
2992
+ <input
2993
+ class=${INPUT_CLASS}
2994
+ type="text"
2995
+ part="search-input"
2996
+ placeholder=${this.#placeholder}
2997
+ .value=${this.#query}
2998
+ @input=${this.#onInput}
2999
+ @keydown=${this.#onKeydown}
3000
+ />
3001
+ ${hasQuery ? b`<span class="list-search-count">${this.#matches.length === 0 ? `0` : this.#matchIndex + 1} / ${this.#matches.length}</span>` : A}
3002
+ ${this.#mode === `navigate` && this.#matches.length > 1 ? b`
3003
+ <button class="list-search-icon-btn" title="Previous match (↑)" @click=${() => this.prevMatch()}>‹</button>
3004
+ <button class="list-search-icon-btn" title="Next match (↓)" @click=${() => this.nextMatch()}>›</button>
3005
+ ` : A}
3006
+ <button class="list-search-icon-btn" title="Close (Esc)" @click=${() => this.close()}>✕</button>
3007
+ </div>
3008
+ ${others.length > 0 || showSelectMatches ? b`
3009
+ <div class="list-search-switch">
3010
+ ${others.map((m) => b`<button class="list-search-btn" @click=${() => this.switchMode(m)}>${m}</button>`)}
3011
+ ${showSelectMatches ? b`<button class="list-search-btn" @click=${() => this.selectMatches()}>Select matches</button>` : A}
3012
+ </div>
3013
+ ` : A}
3014
+ </div>
3015
+ `;
3016
+ }
3017
+ #focusInput() {
3018
+ this.#host.renderRoot?.querySelector(`.${INPUT_CLASS}`)?.focus();
3019
+ }
3020
+ #onInput = (e) => {
3021
+ this.setQuery(e.target.value);
3022
+ };
3023
+ #onKeydown = (e) => {
3024
+ switch (e.key) {
3025
+ case `Escape`:
3026
+ e.preventDefault();
3027
+ e.stopPropagation();
3028
+ this.close();
3029
+ break;
3030
+ case `Enter`:
3031
+ e.preventDefault();
3032
+ e.stopPropagation();
3033
+ this.commit();
3034
+ break;
3035
+ case `ArrowDown`:
3036
+ if (this.#mode === `navigate`) {
3037
+ e.preventDefault();
3038
+ this.nextMatch();
3039
+ }
3040
+ break;
3041
+ case `ArrowUp`: if (this.#mode === `navigate`) {
3042
+ e.preventDefault();
3043
+ this.prevMatch();
3044
+ }
3045
+ }
3046
+ };
3047
+ #cycle(delta) {
3048
+ if (this.#matches.length === 0) return;
3049
+ const n = this.#matches.length;
3050
+ this.#matchIndex = (this.#matchIndex + delta + n) % n;
3051
+ if (this.#mode === `navigate`) this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
3052
+ this.#host.requestUpdate();
3053
+ }
3054
+ #itemFor(match) {
3055
+ return match ? this.#itemByKey.get(match.item.key) : void 0;
3056
+ }
3057
+ #currentItems() {
3058
+ return this.#matches.map((m) => this.#itemFor(m)).filter((x) => x !== void 0);
3059
+ }
3060
+ get #highlightKey() {
3061
+ return this.#adapter.highlightKey ?? DEFAULT_HIGHLIGHT_KEY;
3062
+ }
3063
+ #onState(state) {
3064
+ if (!this.#open) return;
3065
+ const hasQuery = state.query.trim().length > 0;
3066
+ this.#matches = hasQuery ? [...state.results] : [];
3067
+ if (this.#matchIndex >= this.#matches.length) this.#matchIndex = 0;
3068
+ this.#applyMode();
3069
+ this.#host.requestUpdate();
3070
+ }
3071
+ #applyMode() {
3072
+ if (!(this.#query.trim().length > 0)) {
3073
+ if (this.#filterActive) {
3074
+ this.#adapter.applyFilter(void 0);
3075
+ this.#filterActive = false;
3076
+ }
3077
+ this.#applyHighlights();
3078
+ return;
3079
+ }
3080
+ switch (this.#mode) {
3081
+ case `navigate`:
3082
+ this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
3083
+ break;
3084
+ case `select`:
3085
+ this.#adapter.selectItems(this.#currentItems());
3086
+ break;
3087
+ case `filter`:
3088
+ this.#adapter.applyFilter(new Set(this.#matches.map((m) => m.item.key)));
3089
+ this.#filterActive = true;
3090
+ }
3091
+ this.#applyHighlights();
3092
+ }
3093
+ #applyHighlights() {
3094
+ const root = this.#adapter.highlightRoot?.();
3095
+ const selector = this.#adapter.highlightSelector;
3096
+ const keyOf = this.#adapter.highlightKeyOf;
3097
+ if (!root || !selector || !keyOf) return;
3098
+ if (this.#query.trim().length === 0) {
3099
+ clearHighlights({ key: this.#highlightKey });
3100
+ return;
3101
+ }
3102
+ const state = this.#search.state;
3103
+ const targets = buildHighlightTargets(root, selector, state.positionsByItem, (el) => {
3104
+ const key = keyOf(el);
3105
+ return key === void 0 ? void 0 : state.results.find((r) => r.item.key === key)?.item;
3106
+ }, this.#adapter.highlightText ?? ((el) => el.textContent ?? ``));
3107
+ try {
3108
+ applyHighlights(targets, { key: this.#highlightKey });
3109
+ } catch {
3110
+ clearHighlights({ key: this.#highlightKey });
3111
+ }
3112
+ }
3113
+ #teardownEffects() {
3114
+ if (this.#filterActive) {
3115
+ this.#adapter.applyFilter(void 0);
3116
+ this.#filterActive = false;
3117
+ }
3118
+ clearHighlights({ key: this.#highlightKey });
3119
+ }
3120
+ };
3121
+ //#endregion
3122
+ //#region src/incr-search/list-search-styles.ts
3123
+ /**
3124
+ * Styles for the incremental-search popover rendered by
3125
+ * `ListSearchController.renderField()`. Add to a component's `static styles`;
3126
+ * the class names are the contract between this fragment and `renderField()`.
3127
+ *
3128
+ * The host must give the popover a positioned ancestor (a wrapper with
3129
+ * `position: relative`, or `:host { position: relative }`). Tunables:
3130
+ * `--list-search-inset` (default `8px`) offsets it from the top-right corner.
3131
+ *
3132
+ * See `docs/infra-incremental.md`.
3133
+ */
3134
+ const listSearchStyles = i$3`
3135
+ .list-search {
3136
+ position: absolute;
3137
+ top: var(--list-search-inset, 8px);
3138
+ right: var(--list-search-inset, 8px);
3139
+ z-index: 20;
3140
+ display: flex;
3141
+ flex-direction: column;
3142
+ gap: 4px;
3143
+ padding: 6px;
3144
+ box-sizing: border-box;
3145
+ background: var(--surface-4, #2a2a2a);
3146
+ color: var(--surface-3-text, inherit);
3147
+ border: 1px solid var(--border, #444);
3148
+ border-radius: var(--radius-m, 6px);
3149
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
3150
+ font-size: var(--text-s, 0.85em);
3151
+ }
3152
+
3153
+ .list-search[hidden] {
3154
+ display: none;
3155
+ }
3156
+
3157
+ .list-search-row {
3158
+ display: flex;
3159
+ align-items: center;
3160
+ gap: 4px;
3161
+ }
3162
+
3163
+ .list-search-mode {
3164
+ flex: none;
3165
+ padding: 1px 7px;
3166
+ border-radius: 999px;
3167
+ font-weight: 600;
3168
+ text-transform: capitalize;
3169
+ background: var(--accent, #60a5fa);
3170
+ color: var(--accent-text, #000);
3171
+ }
3172
+
3173
+ .list-search-input {
3174
+ flex: 1;
3175
+ min-width: 140px;
3176
+ background: transparent;
3177
+ border: none;
3178
+ outline: none;
3179
+ color: inherit;
3180
+ font: inherit;
3181
+ }
3182
+
3183
+ .list-search-count {
3184
+ flex: none;
3185
+ opacity: 0.6;
3186
+ font-variant-numeric: tabular-nums;
3187
+ white-space: nowrap;
3188
+ }
3189
+
3190
+ .list-search-switch {
3191
+ display: flex;
3192
+ flex-wrap: wrap;
3193
+ gap: 4px;
3194
+ }
3195
+
3196
+ .list-search-btn {
3197
+ padding: 1px 7px;
3198
+ background: var(--surface-3, #333);
3199
+ color: inherit;
3200
+ border: 1px solid var(--border, #444);
3201
+ border-radius: var(--radius-s, 4px);
3202
+ cursor: pointer;
3203
+ font: inherit;
3204
+ text-transform: capitalize;
3205
+ }
3206
+
3207
+ .list-search-btn:hover {
3208
+ background: var(--item-bg-tickled, #444);
3209
+ }
3210
+
3211
+ .list-search-icon-btn {
3212
+ flex: none;
3213
+ padding: 0 2px;
3214
+ background: transparent;
3215
+ border: none;
3216
+ color: var(--surface-muted-text, #888);
3217
+ cursor: pointer;
3218
+ font-size: 1.1em;
3219
+ line-height: 1;
3220
+ }
3221
+
3222
+ .list-search-icon-btn:hover {
3223
+ color: inherit;
3224
+ }
3225
+ `;
3226
+ //#endregion
3227
+ //#region src/incr-search/tree-list-search-adapter.ts
3228
+ /**
3229
+ * A {@link ListSearchAdapter} over any `TreeSearchHost` (`ixfx-tree-list`,
3230
+ * `ixfx-miller-list`, `ixfx-crumb-navigation`). Navigate reveals the match
3231
+ * (expands its ancestors, moves the cursor); select drives `selectMany`; filter
3232
+ * sets `filterPredicate`. The item set is the whole tree — `getNodes()` returns
3233
+ * every node regardless of expansion or an active filter — so refining a query
3234
+ * can always re-surface a node. See `docs/infra-incremental.md`.
3235
+ */
3236
+ function createTreeListSearchAdapter(host, options = {}) {
3237
+ return {
3238
+ getItems: () => host.getNodes(),
3239
+ getLabel: options.getLabel ?? ((item) => item.node.item.label),
3240
+ getKey: (item) => item.node.item.key,
3241
+ focusItem: (item) => {
3242
+ if (item) host.revealKey(item.node.item.key);
3243
+ },
3244
+ activateItem: (item) => {
3245
+ host.revealKey(item.node.item.key);
3246
+ host.select(item.node);
3247
+ },
3248
+ selectItems: (items) => {
3249
+ host.selectMany(items.map((i) => i.node));
3250
+ },
3251
+ applyFilter: (matched) => {
3252
+ host.filterPredicate = matched === void 0 ? void 0 : (node) => matched.has(node.item.key);
3253
+ },
3254
+ highlightRoot: () => host.shadowRoot,
3255
+ highlightSelector: options.labelSelector ?? `.tree-label`,
3256
+ highlightKeyOf: (el) => el.closest(`[data-key]`)?.dataset.key,
3257
+ highlightKey: options.highlightKey
3258
+ };
3259
+ }
3260
+ //#endregion
2579
3261
  //#region src/ac-token/ac-token.ts
2580
3262
  const DEFAULT_TOKENIZER = (text) => text.split(/[\s,;]+/);
2581
3263
  function makeLruCache(maxSize) {
@@ -3877,80 +4559,6 @@ __decorate([state()], RadioGroup.prototype, "_icons", void 0);
3877
4559
  __decorate([query(`slot`)], RadioGroup.prototype, "_slot", void 0);
3878
4560
  RadioGroup = __decorate([safeCustomElement(`ixfx-radio-group`)], RadioGroup);
3879
4561
  //#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
4562
  //#region src/interaction/list-item-animation.ts
3955
4563
  /**
3956
4564
  * Shared insertion / removal animation for the list & tree components.
@@ -8557,6 +9165,16 @@ let CrumbNavigationElement = class CrumbNavigationElement extends i {
8557
9165
  if (!keyPath) return false;
8558
9166
  return this.navigateTo(keyPath);
8559
9167
  }
9168
+ /** `TreeComponent` parity — a crumb path always follows the selection, so this just navigates to `key`. */
9169
+ revealKey(key) {
9170
+ return this.navigateToNode((n) => n.item.key === key);
9171
+ }
9172
+ /** `TreeComponent` parity — a crumb path is a single selection, so only the last node is kept. */
9173
+ selectMany(nodes) {
9174
+ const last = [...nodes].at(-1);
9175
+ if (last) this.select(last);
9176
+ else this.clearSelection();
9177
+ }
8560
9178
  #findNodePath(nodes, predicate, prefix = []) {
8561
9179
  for (const node of nodes) {
8562
9180
  const path = [...prefix, node.item.key];
@@ -11067,6 +11685,8 @@ let DataGridElement = class DataGridElement extends i {
11067
11685
  this._sortedRows = [];
11068
11686
  this._flat = [];
11069
11687
  this._flatKeys = [];
11688
+ this.#searchKeyByRow = /* @__PURE__ */ new Map();
11689
+ this.search = new ListSearchController(this, { adapter: this.#createSearchAdapter() });
11070
11690
  this.#prevFlatKeys = [];
11071
11691
  this.#enteringRowKeys = /* @__PURE__ */ new Set();
11072
11692
  this.#rowReflowTimer = 0;
@@ -11272,6 +11892,7 @@ let DataGridElement = class DataGridElement extends i {
11272
11892
  tickledStyles,
11273
11893
  tickledItemStyles,
11274
11894
  listItemFadeKeyframes,
11895
+ listSearchStyles,
11275
11896
  i$3`
11276
11897
  :host {
11277
11898
  --data-grid-row-height: 28px;
@@ -11280,6 +11901,7 @@ let DataGridElement = class DataGridElement extends i {
11280
11901
  --data-grid-header-bg: var(--surface-2);
11281
11902
  --data-grid-border: var(--border);
11282
11903
  --data-grid-header-text: var(--surface-3-text);
11904
+ position: relative;
11283
11905
  display: block;
11284
11906
  overflow: hidden;
11285
11907
  border: var(--data-grid-border);
@@ -11373,6 +11995,55 @@ let DataGridElement = class DataGridElement extends i {
11373
11995
  `
11374
11996
  ];
11375
11997
  }
11998
+ #searchRegistry;
11999
+ #unregisterSearchCommands;
12000
+ #searchKeyByRow;
12001
+ /** Command registry carrying this grid's `list.search.*` commands. */
12002
+ get registry() {
12003
+ return this.#searchRegistry;
12004
+ }
12005
+ #rowSearchText(row) {
12006
+ if (this.rowSearchText) return this.rowSearchText(row);
12007
+ const rec = row;
12008
+ return this._visibleColumns.map((c) => String(rec[c.key] ?? ``)).join(` `);
12009
+ }
12010
+ #createSearchAdapter() {
12011
+ return {
12012
+ getItems: () => {
12013
+ this.#searchKeyByRow.clear();
12014
+ this._sortedRows.forEach((r, i) => this.#searchKeyByRow.set(r, this.getRowKey(r, i)));
12015
+ return this._sortedRows;
12016
+ },
12017
+ getLabel: (row) => this.#rowSearchText(row),
12018
+ getKey: (row) => this.#searchKeyByRow.get(row) ?? String(this._sortedRows.indexOf(row)),
12019
+ focusItem: (row) => {
12020
+ if (row === void 0) return;
12021
+ const key = this.#searchKeyByRow.get(row);
12022
+ if (key !== void 0) this.tickleRow(key);
12023
+ },
12024
+ selectItems: (rows) => {
12025
+ this.selectKeys(rows.map((r) => this.#searchKeyByRow.get(r)).filter((k) => k !== void 0));
12026
+ },
12027
+ applyFilter: (matched) => {
12028
+ this.filterPredicate = matched === void 0 ? void 0 : (row) => {
12029
+ const k = this.#searchKeyByRow.get(row);
12030
+ return k === void 0 || matched.has(k);
12031
+ };
12032
+ }
12033
+ };
12034
+ }
12035
+ #effectiveSearchModes() {
12036
+ const raw = this.searchModes?.trim();
12037
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
12038
+ const requested = new Set(raw.split(/\s+/));
12039
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
12040
+ }
12041
+ #syncSearchCommands() {
12042
+ if (!this.#searchRegistry) return;
12043
+ this.search.setModes(this.#effectiveSearchModes());
12044
+ this.#unregisterSearchCommands?.();
12045
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
12046
+ }
11376
12047
  /** Row-key snapshot from the previous update, for the insertion / removal animation. */
11377
12048
  #prevFlatKeys;
11378
12049
  #enteringRowKeys;
@@ -11385,17 +12056,29 @@ let DataGridElement = class DataGridElement extends i {
11385
12056
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
11386
12057
  this.addEventListener(`keydown`, this._onKeyDown);
11387
12058
  this.addEventListener(`focusout`, this._onFocusOut);
12059
+ this.#searchRegistry ??= createCommandRegistry();
12060
+ this.#syncSearchCommands();
11388
12061
  }
11389
12062
  disconnectedCallback() {
11390
12063
  super.disconnectedCallback();
11391
12064
  this.removeEventListener(`keydown`, this._onKeyDown);
11392
12065
  this.removeEventListener(`focusout`, this._onFocusOut);
11393
12066
  cancelAnimationFrame(this._raf);
12067
+ this.#unregisterSearchCommands?.();
12068
+ this.#unregisterSearchCommands = void 0;
12069
+ this.#searchRegistry = void 0;
11394
12070
  if (this.#rowReflowTimer) {
11395
12071
  clearTimeout(this.#rowReflowTimer);
11396
12072
  this.#rowReflowTimer = 0;
11397
12073
  }
11398
12074
  }
12075
+ /**
12076
+ * Dispatch a command to this grid — routes through the command registry so
12077
+ * `list.search.*` (and any other registered commands) work.
12078
+ */
12079
+ dispatch(commandId, args) {
12080
+ this.#searchRegistry?.invoke(commandId, args);
12081
+ }
11399
12082
  firstUpdated() {
11400
12083
  new ResizeObserver(() => {
11401
12084
  if (!this._viewportEl) return;
@@ -11421,6 +12104,7 @@ let DataGridElement = class DataGridElement extends i {
11421
12104
  willUpdate(changed) {
11422
12105
  super.willUpdate(changed);
11423
12106
  this.setAttribute(`data-input-mode`, this._tickled.inputMode);
12107
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
11424
12108
  if (!this.#appendInProgress) {
11425
12109
  if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`)) this._sortedRows = this._applySort([...this.rows]);
11426
12110
  this._rebuildFlat();
@@ -11475,18 +12159,23 @@ let DataGridElement = class DataGridElement extends i {
11475
12159
  return String(this.groupBy(row));
11476
12160
  }
11477
12161
  _rebuildFlat() {
12162
+ const filtered = this.filterPredicate !== void 0;
12163
+ const source = filtered ? this._sortedRows.filter((r) => this.filterPredicate(r)) : this._sortedRows;
11478
12164
  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
- }));
12165
+ this._flat = source.map((r, i) => {
12166
+ const idx = filtered ? this._sortedRows.indexOf(r) : i;
12167
+ return {
12168
+ type: `row`,
12169
+ key: this.getRowKey(r, idx),
12170
+ row: r,
12171
+ idx
12172
+ };
12173
+ });
11485
12174
  this._flatKeys = this._flat.map((f) => f.key);
11486
12175
  return;
11487
12176
  }
11488
12177
  const groups = /* @__PURE__ */ new Map();
11489
- for (const r of this._sortedRows) {
12178
+ for (const r of source) {
11490
12179
  const k = this._getGroupKey(r) ?? ``;
11491
12180
  const arr = groups.get(k);
11492
12181
  if (arr) arr.push(r);
@@ -12356,6 +13045,7 @@ let DataGridElement = class DataGridElement extends i {
12356
13045
  })}
12357
13046
  </div>
12358
13047
  </div>
13048
+ ${this.search.renderField()}
12359
13049
  </div>
12360
13050
  `;
12361
13051
  }
@@ -12399,6 +13089,13 @@ __decorate([property({
12399
13089
  attribute: `column-menu`
12400
13090
  })], DataGridElement.prototype, "columnMenu", void 0);
12401
13091
  __decorate([property({ attribute: false })], DataGridElement.prototype, "globalFormatter", void 0);
13092
+ __decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
13093
+ __decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
13094
+ __decorate([property({
13095
+ type: String,
13096
+ attribute: `search-modes`,
13097
+ reflect: true
13098
+ })], DataGridElement.prototype, "searchModes", void 0);
12402
13099
  __decorate([state()], DataGridElement.prototype, "_selectedKeys", void 0);
12403
13100
  __decorate([state()], DataGridElement.prototype, "_scrollTop", void 0);
12404
13101
  __decorate([state()], DataGridElement.prototype, "_scrollLeft", void 0);
@@ -12680,12 +13377,12 @@ __decorate([state()], DataTableElement.prototype, "_stacked", void 0);
12680
13377
  DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableElement);
12681
13378
  //#endregion
12682
13379
  //#region src/detail-list/detail-list.ts
12683
- const ITEM_SELECTOR$2 = `:scope > li`;
13380
+ const ITEM_SELECTOR$3 = `:scope > li`;
12684
13381
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
12685
13382
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
12686
13383
  let DetailListElement = class DetailListElement extends i {
12687
- constructor(..._args2) {
12688
- super(..._args2);
13384
+ constructor(..._args) {
13385
+ super(..._args);
12689
13386
  this.selectionMode = `single`;
12690
13387
  this.interactionMode = `standard`;
12691
13388
  this.sort = `none`;
@@ -12695,8 +13392,6 @@ let DetailListElement = class DetailListElement extends i {
12695
13392
  this.iconProperty = ``;
12696
13393
  this.keyProperty = ``;
12697
13394
  this.itemWrap = `stop`;
12698
- this._searchVisible = false;
12699
- this._searchQuery = ``;
12700
13395
  this.#itemData = /* @__PURE__ */ new WeakMap();
12701
13396
  this.#grid = [];
12702
13397
  this.#positions = /* @__PURE__ */ new Map();
@@ -12733,6 +13428,11 @@ let DetailListElement = class DetailListElement extends i {
12733
13428
  onStateChange: () => this._syncItemStates(),
12734
13429
  itemWrap: this.itemWrap
12735
13430
  });
13431
+ this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
13432
+ itemSelector: ITEM_SELECTOR$3,
13433
+ getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
13434
+ highlightKey: SEARCH_HIGHLIGHT_KEY
13435
+ }) });
12736
13436
  this._onPointerOver = (e) => {
12737
13437
  const item = this._findListItem(e.composedPath());
12738
13438
  if (item) this.tickled.handlePointerEnter(item);
@@ -12741,11 +13441,7 @@ let DetailListElement = class DetailListElement extends i {
12741
13441
  this.tickled.handlePointerLeave(e);
12742
13442
  };
12743
13443
  this._onKeyDown = (e) => {
12744
- if ((e.ctrlKey || e.metaKey) && e.key === `f`) {
12745
- e.preventDefault();
12746
- this._openSearch();
12747
- return;
12748
- }
13444
+ if (this.search.isOpen) return;
12749
13445
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
12750
13446
  if (this.selection.canSelectAll()) {
12751
13447
  e.preventDefault();
@@ -12753,11 +13449,6 @@ let DetailListElement = class DetailListElement extends i {
12753
13449
  }
12754
13450
  return;
12755
13451
  }
12756
- if (this._searchVisible && e.key === `Escape`) {
12757
- e.preventDefault();
12758
- this._closeSearch();
12759
- return;
12760
- }
12761
13452
  switch (e.key) {
12762
13453
  case `ArrowDown`:
12763
13454
  e.preventDefault();
@@ -12826,44 +13517,7 @@ let DetailListElement = class DetailListElement extends i {
12826
13517
  e.preventDefault();
12827
13518
  this._listEl.scrollLeft += e.deltaY;
12828
13519
  };
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
13520
  }
12866
- #search;
12867
13521
  #itemData;
12868
13522
  #grid;
12869
13523
  #positions;
@@ -12875,6 +13529,24 @@ let DetailListElement = class DetailListElement extends i {
12875
13529
  #builtFromProvider;
12876
13530
  #providerBuilt;
12877
13531
  #data;
13532
+ #searchRegistry;
13533
+ #unregisterSearchCommands;
13534
+ /** Command registry carrying this list's `list.search.*` commands. */
13535
+ get registry() {
13536
+ return this.#searchRegistry;
13537
+ }
13538
+ #effectiveSearchModes() {
13539
+ const raw = this.searchModes?.trim();
13540
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
13541
+ const requested = new Set(raw.split(/\s+/));
13542
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
13543
+ }
13544
+ #syncSearchCommands() {
13545
+ if (!this.#searchRegistry) return;
13546
+ this.search.setModes(this.#effectiveSearchModes());
13547
+ this.#unregisterSearchCommands?.();
13548
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
13549
+ }
12878
13550
  connectedCallback() {
12879
13551
  super.connectedCallback();
12880
13552
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -12896,7 +13568,9 @@ let DetailListElement = class DetailListElement extends i {
12896
13568
  subtree: false,
12897
13569
  attributeFilter: [`hidden`]
12898
13570
  });
12899
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#animatedItems.add(el);
13571
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#animatedItems.add(el);
13572
+ this.#searchRegistry ??= createCommandRegistry();
13573
+ this.#syncSearchCommands();
12900
13574
  }
12901
13575
  /**
12902
13576
  * Collapse-in items appended to the light DOM (however they got there). A bulk
@@ -12913,7 +13587,7 @@ let DetailListElement = class DetailListElement extends i {
12913
13587
  fresh.push(node);
12914
13588
  }
12915
13589
  if (fresh.length === 0) return;
12916
- const total = this.querySelectorAll(ITEM_SELECTOR$2).length;
13590
+ const total = this.querySelectorAll(ITEM_SELECTOR$3).length;
12917
13591
  if (fresh.length >= total) return;
12918
13592
  for (const el of fresh) animateListItemIn(el);
12919
13593
  window.setTimeout(() => this.#scheduleGeometry(), 230);
@@ -12930,23 +13604,18 @@ let DetailListElement = class DetailListElement extends i {
12930
13604
  this.#resizeObserver = void 0;
12931
13605
  this.#mutationObserver?.disconnect();
12932
13606
  this.#mutationObserver = void 0;
12933
- this.#search?.dispose();
12934
- this.#search = void 0;
13607
+ this.#unregisterSearchCommands?.();
13608
+ this.#unregisterSearchCommands = void 0;
13609
+ this.#searchRegistry = void 0;
12935
13610
  }
12936
13611
  firstUpdated() {
12937
13612
  this.#processChildren();
12938
13613
  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
13614
  this.#recomputeGeometry();
12947
13615
  }
12948
13616
  updated(changed) {
12949
13617
  super.updated(changed);
13618
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
12950
13619
  if (changed.has(`provider`)) {
12951
13620
  this.#builtFromProvider = [];
12952
13621
  this.#providerBuilt = 0;
@@ -13040,8 +13709,14 @@ let DetailListElement = class DetailListElement extends i {
13040
13709
  getItemData(item) {
13041
13710
  return this.#itemData.get(item);
13042
13711
  }
13043
- /** Dispatch a command to this list. No-op by default (grouped-lister parity). */
13044
- dispatch(_commandId, _args) {}
13712
+ /**
13713
+ * Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
13714
+ * forward commands. Routes through the command registry (so `list.search.*`
13715
+ * and any other registered commands work).
13716
+ */
13717
+ dispatch(commandId, args) {
13718
+ this.#searchRegistry?.invoke(commandId, args);
13719
+ }
13045
13720
  select(item) {
13046
13721
  this.selection.replace(item);
13047
13722
  }
@@ -13089,7 +13764,7 @@ let DetailListElement = class DetailListElement extends i {
13089
13764
  });
13090
13765
  }
13091
13766
  #itemForKey(key) {
13092
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) if (keyForElement(el) === key) return el;
13767
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) if (keyForElement(el) === key) return el;
13093
13768
  }
13094
13769
  *selectedFilter(kind) {
13095
13770
  const wanted = (el) => {
@@ -13099,7 +13774,7 @@ let DetailListElement = class DetailListElement extends i {
13099
13774
  case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);
13100
13775
  }
13101
13776
  };
13102
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
13777
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) {
13103
13778
  if (el.hidden) continue;
13104
13779
  if (wanted(el)) yield el;
13105
13780
  }
@@ -13112,7 +13787,7 @@ let DetailListElement = class DetailListElement extends i {
13112
13787
  if (previousKeys.size > 0 || anchorKey !== null) {
13113
13788
  const next = [];
13114
13789
  let anchor;
13115
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
13790
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) {
13116
13791
  const key = keyOf(el);
13117
13792
  if (key !== null && previousKeys.has(key)) next.push(el);
13118
13793
  if (key !== null && key === anchorKey) anchor = el;
@@ -13167,15 +13842,15 @@ let DetailListElement = class DetailListElement extends i {
13167
13842
  }
13168
13843
  #applyFilterPredicate() {
13169
13844
  if (!this.filterPredicate) {
13170
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) el.removeAttribute(`hidden`);
13845
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) el.removeAttribute(`hidden`);
13171
13846
  return;
13172
13847
  }
13173
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) if (this.filterPredicate(el)) el.removeAttribute(`hidden`);
13848
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) if (this.filterPredicate(el)) el.removeAttribute(`hidden`);
13174
13849
  else el.setAttribute(`hidden`, ``);
13175
13850
  }
13176
13851
  #applySort() {
13177
13852
  if (this.sort === `none`) return;
13178
- const rows = [...this.querySelectorAll(ITEM_SELECTOR$2)];
13853
+ const rows = [...this.querySelectorAll(ITEM_SELECTOR$3)];
13179
13854
  if (rows.length < 2) return;
13180
13855
  const dir = this.sort === `descending` ? -1 : 1;
13181
13856
  const meta = (el) => ({
@@ -13367,7 +14042,7 @@ let DetailListElement = class DetailListElement extends i {
13367
14042
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
13368
14043
  }
13369
14044
  _getVisibleItems() {
13370
- return Array.from(this.querySelectorAll(ITEM_SELECTOR$2)).filter((el) => {
14045
+ return Array.from(this.querySelectorAll(ITEM_SELECTOR$3)).filter((el) => {
13371
14046
  if (el.hidden) return false;
13372
14047
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
13373
14048
  return true;
@@ -13422,10 +14097,10 @@ let DetailListElement = class DetailListElement extends i {
13422
14097
  }
13423
14098
  /** Scan light-DOM children for unprocessed `data-icon` items. */
13424
14099
  #processChildren() {
13425
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#ensureIcon(el);
14100
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#ensureIcon(el);
13426
14101
  }
13427
14102
  _syncItemStates() {
13428
- for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
14103
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) {
13429
14104
  el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
13430
14105
  el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
13431
14106
  if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !el.hasAttribute(`not-checked`) && this.selection.selected.has(el));
@@ -13525,16 +14200,6 @@ let DetailListElement = class DetailListElement extends i {
13525
14200
  composed: true
13526
14201
  }));
13527
14202
  }
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
14203
  _activateItem(item) {
13539
14204
  if (this.interactionMode !== `manual`) this.selection.replace(item);
13540
14205
  this._dispatch(`list-activate`, { item });
@@ -13543,6 +14208,7 @@ let DetailListElement = class DetailListElement extends i {
13543
14208
  this.styles = [
13544
14209
  themeFallbacks,
13545
14210
  scrollFadeStyles(`x`),
14211
+ listSearchStyles,
13546
14212
  i$3`
13547
14213
  :host {
13548
14214
  display: block;
@@ -13664,48 +14330,6 @@ let DetailListElement = class DetailListElement extends i {
13664
14330
  .loading[hidden] {
13665
14331
  display: none;
13666
14332
  }
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
14333
  `
13710
14334
  ];
13711
14335
  }
@@ -13720,21 +14344,7 @@ let DetailListElement = class DetailListElement extends i {
13720
14344
  <div class="loading" part="loading" ?hidden=${!loading}>
13721
14345
  ${loading ? progress !== void 0 ? `${Math.round(progress * 100)}%` : `loading…` : A}
13722
14346
  </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>
14347
+ ${this.search.renderField()}
13738
14348
  </div>
13739
14349
  `;
13740
14350
  }
@@ -13774,9 +14384,11 @@ __decorate([property({
13774
14384
  attribute: `item-wrap`
13775
14385
  })], DetailListElement.prototype, "itemWrap", void 0);
13776
14386
  __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);
14387
+ __decorate([property({
14388
+ type: String,
14389
+ attribute: `search-modes`,
14390
+ reflect: true
14391
+ })], DetailListElement.prototype, "searchModes", void 0);
13780
14392
  __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
13781
14393
  DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
13782
14394
  //#endregion
@@ -15511,7 +16123,7 @@ SplitLayoutElement = __decorate([safeCustomElement(`ixfx-split-layout`)], SplitL
15511
16123
  //#endregion
15512
16124
  //#region src/tabs/tab-list.ts
15513
16125
  /** Pointer movement threshold (px) before a press becomes a drag. */
15514
- const DRAG_THRESHOLD_PX$3 = 5;
16126
+ const DRAG_THRESHOLD_PX$4 = 5;
15515
16127
  let TabListElement = class TabListElement extends i {
15516
16128
  constructor(..._args) {
15517
16129
  super(..._args);
@@ -15625,7 +16237,7 @@ let TabListElement = class TabListElement extends i {
15625
16237
  const dy = event.clientY - state.startY;
15626
16238
  const distance = Math.hypot(dx, dy);
15627
16239
  if (!state.active) {
15628
- if (distance < DRAG_THRESHOLD_PX$3) return;
16240
+ if (distance < DRAG_THRESHOLD_PX$4) return;
15629
16241
  this.#beginDrag(event);
15630
16242
  }
15631
16243
  state.ghost?.move(event.clientX, event.clientY);
@@ -16475,12 +17087,23 @@ let TitlebarElement = class TitlebarElement extends i {
16475
17087
  const showCaret = this.collapsible && this.toggleInteraction !== `disabled`;
16476
17088
  const implicitToggle = this.collapsible && this.toggleInteraction === `implicit`;
16477
17089
  const caretRotation = isVertical ? this.collapsed ? `` : `-90deg` : this.collapsed ? `-90deg` : ``;
17090
+ const treeVariant = this.variant === `tree`;
16478
17091
  return b`
16479
17092
  <div
16480
17093
  class="bar"
16481
17094
  role="toolbar"
16482
17095
  @click=${implicitToggle ? this._onToggleClick : A}
16483
17096
  >
17097
+ ${treeVariant && showCaret ? b`
17098
+ <button
17099
+ class="caret-btn"
17100
+ type="button"
17101
+ aria-label=${this.collapsed ? `Expand panel` : `Collapse panel`}
17102
+ @click=${this._onToggleClick}
17103
+ >
17104
+ <ixfx-icon name=${caretIcon} prominence .rotate=${caretRotation.length > 0 ? caretRotation : A}></ixfx-icon>
17105
+ </button>
17106
+ ` : A}
16484
17107
  <div class="title-text-wrapper">
16485
17108
  ${this.iconName ? b`<ixfx-icon class="title-icon" name=${this.iconName}></ixfx-icon>` : A}
16486
17109
  <span class="title-text">${this.title}</span>
@@ -16498,7 +17121,7 @@ let TitlebarElement = class TitlebarElement extends i {
16498
17121
  <ixfx-icon name="close"></ixfx-icon>
16499
17122
  </button>
16500
17123
  ` : A}
16501
- ${showCaret ? b`
17124
+ ${!treeVariant && showCaret ? b`
16502
17125
  <button
16503
17126
  class="caret-btn"
16504
17127
  type="button"
@@ -16721,6 +17344,77 @@ let TitlebarElement = class TitlebarElement extends i {
16721
17344
  -1px 0 0 var(--surface-5),
16722
17345
  1px 0 0 var(--surface-1);
16723
17346
  }
17347
+
17348
+ /* === Tree variant: VS Code explorer / Outline style === */
17349
+ :host([variant="tree"]) .bar {
17350
+ background: transparent;
17351
+ padding: 0 var(--space-xs);
17352
+ min-height: unset;
17353
+ gap: var(--space-xs);
17354
+ }
17355
+
17356
+ :host([variant="tree"][orientation="vertical"]) .bar {
17357
+ padding: var(--space-xs) 0;
17358
+ flex-direction: column;
17359
+ min-width: unset;
17360
+ writing-mode: horizontal-tb;
17361
+ }
17362
+
17363
+ :host([variant="tree"]) .title-text-wrapper {
17364
+ display: inline-flex;
17365
+ align-items: center;
17366
+ gap: var(--space-xs);
17367
+ padding: 0;
17368
+ background: transparent;
17369
+ }
17370
+
17371
+ :host([variant="tree"]) .title-icon {
17372
+ background: transparent;
17373
+ flex-shrink: 0;
17374
+ }
17375
+
17376
+ :host([variant="tree"]) .title-text {
17377
+ font-weight: normal;
17378
+ color: var(--surface-3-text);
17379
+ padding: 0;
17380
+ background: transparent;
17381
+ box-shadow: none;
17382
+ flex: unset;
17383
+ width: auto;
17384
+ white-space: nowrap;
17385
+ }
17386
+
17387
+ :host([variant="tree"][orientation="vertical"]) .title-text {
17388
+ writing-mode: vertical-rl;
17389
+ transform: rotate(180deg);
17390
+ padding: 0;
17391
+ }
17392
+
17393
+ :host([variant="tree"]) .caret-btn {
17394
+ background: transparent;
17395
+ opacity: var(--prominence-idle-opacity);
17396
+ padding: 0;
17397
+ }
17398
+
17399
+ :host([variant="tree"]) .caret-btn:hover {
17400
+ background: transparent;
17401
+ opacity: var(--prominence-tickled-opacity);
17402
+ }
17403
+
17404
+ :host([variant="tree"]) .toolbar {
17405
+ margin-left: auto;
17406
+ }
17407
+
17408
+ :host([variant="tree"][orientation="vertical"]) .toolbar {
17409
+ flex-direction: row;
17410
+ margin-left: 0;
17411
+ margin-top: auto;
17412
+ }
17413
+
17414
+ :host([variant="tree"]) ixfx-icon {
17415
+ width: 1em;
17416
+ height: 1em;
17417
+ }
16724
17418
  `];
16725
17419
  }
16726
17420
  };
@@ -16756,7 +17450,7 @@ TitlebarElement = __decorate([safeCustomElement(`ixfx-titlebar`)], TitlebarEleme
16756
17450
  //#endregion
16757
17451
  //#region src/dock/dock-panel.ts
16758
17452
  /** Pointer movement threshold before a titlebar press becomes a dock drag. */
16759
- const DRAG_THRESHOLD_PX$2 = 5;
17453
+ const DRAG_THRESHOLD_PX$3 = 5;
16760
17454
  let DockPanelElement = class DockPanelElement extends i {
16761
17455
  constructor(..._args) {
16762
17456
  super(..._args);
@@ -16788,7 +17482,7 @@ let DockPanelElement = class DockPanelElement extends i {
16788
17482
  this._onPointerMove = (event) => {
16789
17483
  if (this._dragPointerId === null || event.pointerId !== this._dragPointerId) return;
16790
17484
  if (!this._dragStarted) {
16791
- if (Math.hypot(event.clientX - this._dragStartX, event.clientY - this._dragStartY) < DRAG_THRESHOLD_PX$2) return;
17485
+ if (Math.hypot(event.clientX - this._dragStartX, event.clientY - this._dragStartY) < DRAG_THRESHOLD_PX$3) return;
16792
17486
  this._dragStarted = true;
16793
17487
  const pointerId = this._dragPointerId;
16794
17488
  this._teardownDragListeners();
@@ -16943,7 +17637,7 @@ DockPanelElement = __decorate([safeCustomElement(`ixfx-dock-panel`)], DockPanelE
16943
17637
  /** Fraction of the leaf rect (from each edge) that forms the split bands. */
16944
17638
  const BAND = .25;
16945
17639
  /** Pointer movement threshold before a press becomes a dock drag (matches tab-list). */
16946
- const DRAG_THRESHOLD_PX$1 = 5;
17640
+ const DRAG_THRESHOLD_PX$2 = 5;
16947
17641
  /** Tolerance (px) when matching a child gutter endpoint to a parent gutter line. */
16948
17642
  const JUNCTION_TOL_PX = 6;
16949
17643
  /** Junction points closer than this merge into one handle (a 4-way cross yields one per side). */
@@ -17058,7 +17752,7 @@ let DockHostElement = class DockHostElement extends i {
17058
17752
  this._onTabDragMove = (event) => {
17059
17753
  const pending = this._tabDragPending;
17060
17754
  if (pending === null || event.pointerId !== pending.pointerId) return;
17061
- if (Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY) < DRAG_THRESHOLD_PX$1) return;
17755
+ if (Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY) < DRAG_THRESHOLD_PX$2) return;
17062
17756
  this._tabDragPending = null;
17063
17757
  this._teardownTabDragListeners();
17064
17758
  this._suppressNextClick();
@@ -18758,7 +19452,7 @@ function WheelNudgeMixin(superClass) {
18758
19452
  }
18759
19453
  //#endregion
18760
19454
  //#region src/editable-label/editable-number.ts
18761
- const DRAG_THRESHOLD_PX = 3;
19455
+ const DRAG_THRESHOLD_PX$1 = 3;
18762
19456
  let EditableNumber = class EditableNumber extends WheelNudgeMixin(EditableLabel) {
18763
19457
  #dragStartY;
18764
19458
  #dragStartValue;
@@ -18875,7 +19569,7 @@ let EditableNumber = class EditableNumber extends WheelNudgeMixin(EditableLabel)
18875
19569
  if (this._editing) return;
18876
19570
  if (this.disabled || this.noDrag) return;
18877
19571
  const deltaY = this.#dragStartY - e.clientY;
18878
- if (!this.#dragging && Math.abs(deltaY) < DRAG_THRESHOLD_PX) return;
19572
+ if (!this.#dragging && Math.abs(deltaY) < DRAG_THRESHOLD_PX$1) return;
18879
19573
  this.#dragging = true;
18880
19574
  this.#hasDragged = true;
18881
19575
  this.toggleAttribute(`dragging`, true);
@@ -19484,6 +20178,226 @@ let FormElement = class FormElement extends i {
19484
20178
  };
19485
20179
  FormElement = __decorate([safeCustomElement(`ixfx-form`)], FormElement);
19486
20180
  //#endregion
20181
+ //#region src/grid-list/grid-list-source.ts
20182
+ const ITEM_SELECTOR$2 = `:scope > *`;
20183
+ const SYNTHETIC_PREFIX = `gl-auto-`;
20184
+ /** Whether `key` is an auto-assigned key for a keyless light-DOM item. */
20185
+ function isSyntheticKey(key) {
20186
+ return key.startsWith(SYNTHETIC_PREFIX);
20187
+ }
20188
+ function warnDuplicateKeys(keys) {
20189
+ const seen = /* @__PURE__ */ new Set();
20190
+ const dupes = /* @__PURE__ */ new Set();
20191
+ for (const k of keys) {
20192
+ if (isSyntheticKey(k)) continue;
20193
+ if (seen.has(k)) dupes.add(k);
20194
+ else seen.add(k);
20195
+ }
20196
+ if (dupes.size > 0) console.warn(`ixfx-grid-list: duplicate item key(s) ${[...dupes].map((k) => `"${k}"`).join(`, `)} — the first occurrence wins for key-based operations.`);
20197
+ }
20198
+ var ElementGridListSource = class {
20199
+ #host;
20200
+ #items = [];
20201
+ #slotNames = /* @__PURE__ */ new WeakMap();
20202
+ #syntheticKeys = /* @__PURE__ */ new WeakMap();
20203
+ #seq = 0;
20204
+ #predicate;
20205
+ #searchKeys;
20206
+ #effectiveIndices = [];
20207
+ #keysCache;
20208
+ constructor(host) {
20209
+ this.#host = host;
20210
+ this.refresh();
20211
+ }
20212
+ get count() {
20213
+ return this.#effectiveIndices.length;
20214
+ }
20215
+ get rawCount() {
20216
+ return this.#items.length;
20217
+ }
20218
+ keyAt(index) {
20219
+ const eff = this.#effectiveIndices[index];
20220
+ return eff === void 0 ? `` : this.#itemKey(this.#items[eff]);
20221
+ }
20222
+ indexOfKey(key) {
20223
+ return this.#effectiveIndices.findIndex((i) => this.#itemKey(this.#items[i]) === key);
20224
+ }
20225
+ keys() {
20226
+ this.#keysCache ??= this.#effectiveIndices.map((i) => this.#itemKey(this.#items[i]));
20227
+ return this.#keysCache;
20228
+ }
20229
+ labelAt(index) {
20230
+ const eff = this.#effectiveIndices[index];
20231
+ return eff === void 0 ? `` : this.#captionFor(this.#items[eff]);
20232
+ }
20233
+ rawKeyAt(index) {
20234
+ const el = this.#items[index];
20235
+ return el === void 0 ? `` : this.#itemKey(el);
20236
+ }
20237
+ rawSearchTextAt(index) {
20238
+ const el = this.#items[index];
20239
+ if (el === void 0) return ``;
20240
+ return el.getAttribute(`data-search-label`) ?? el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? el.textContent?.trim() ?? ``;
20241
+ }
20242
+ isSelectable(index) {
20243
+ const eff = this.#effectiveIndices[index];
20244
+ return eff !== void 0 && !this.#items[eff].hasAttribute(`not-checked`);
20245
+ }
20246
+ renderCellContent(index) {
20247
+ const eff = this.#effectiveIndices[index];
20248
+ if (eff === void 0) return void 0;
20249
+ return b`<slot name=${this.slotNameFor(this.#items[eff])}></slot>`;
20250
+ }
20251
+ setFilters(opts) {
20252
+ if (`predicate` in opts) this.#predicate = opts.predicate;
20253
+ if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
20254
+ this.#recomputeEffective();
20255
+ }
20256
+ refresh() {
20257
+ this.#items = Array.from(this.#host.querySelectorAll(ITEM_SELECTOR$2)).filter((el) => !el.hidden);
20258
+ for (const el of this.#items) {
20259
+ const name = this.slotNameFor(el);
20260
+ if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
20261
+ }
20262
+ warnDuplicateKeys(this.#items.map((el) => this.#itemKey(el)));
20263
+ this.#recomputeEffective();
20264
+ }
20265
+ /** Effective-order elements — the array `GridListElement` renders and slices. */
20266
+ effectiveElements() {
20267
+ return this.#effectiveIndices.map((i) => this.#items[i]);
20268
+ }
20269
+ /** Raw-order element at `index`, or `undefined`. */
20270
+ rawElementAt(index) {
20271
+ return this.#items[index];
20272
+ }
20273
+ /** Stable per-element `<slot>` name. */
20274
+ slotNameFor(el) {
20275
+ let name = this.#slotNames.get(el);
20276
+ if (name === void 0) {
20277
+ this.#seq += 1;
20278
+ name = `gl-item-${this.#seq}`;
20279
+ this.#slotNames.set(el, name);
20280
+ }
20281
+ return name;
20282
+ }
20283
+ resolveKeyToElement(key) {
20284
+ if (isSyntheticKey(key)) return this.#items.find((el) => this.#syntheticKeys.get(el) === key);
20285
+ return this.#items.find((el) => keyForElement(el) === key);
20286
+ }
20287
+ resolveKeyToData(_key) {}
20288
+ #recomputeEffective() {
20289
+ const effective = [];
20290
+ for (let i = 0; i < this.#items.length; i++) {
20291
+ if (this.#predicate !== void 0 && !this.#predicate(i)) continue;
20292
+ if (this.#searchKeys !== void 0) {
20293
+ const key = this.#itemKey(this.#items[i]);
20294
+ if (!isSyntheticKey(key) && !this.#searchKeys.has(key)) continue;
20295
+ }
20296
+ effective.push(i);
20297
+ }
20298
+ this.#effectiveIndices = effective;
20299
+ this.#keysCache = void 0;
20300
+ }
20301
+ #itemKey(el) {
20302
+ const userKey = keyForElement(el);
20303
+ if (userKey !== void 0) return userKey;
20304
+ let synthetic = this.#syntheticKeys.get(el);
20305
+ if (synthetic === void 0) {
20306
+ this.#seq += 1;
20307
+ synthetic = `${SYNTHETIC_PREFIX}${this.#seq}`;
20308
+ this.#syntheticKeys.set(el, synthetic);
20309
+ }
20310
+ return synthetic;
20311
+ }
20312
+ #captionFor(el) {
20313
+ return el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? ``;
20314
+ }
20315
+ };
20316
+ var DataGridListSource = class {
20317
+ #items;
20318
+ #adapter;
20319
+ #predicate;
20320
+ #searchKeys;
20321
+ #effectiveIndices = [];
20322
+ #keysCache;
20323
+ constructor(items, adapter) {
20324
+ this.#items = items;
20325
+ this.#adapter = adapter;
20326
+ warnDuplicateKeys(items.map((it, i) => adapter.key(it, i)));
20327
+ this.#recomputeEffective();
20328
+ }
20329
+ get count() {
20330
+ return this.#effectiveIndices.length;
20331
+ }
20332
+ get rawCount() {
20333
+ return this.#items.length;
20334
+ }
20335
+ keyAt(index) {
20336
+ const eff = this.#effectiveIndices[index];
20337
+ return eff === void 0 ? `` : this.#adapter.key(this.#items[eff], eff);
20338
+ }
20339
+ indexOfKey(key) {
20340
+ return this.#effectiveIndices.findIndex((i) => this.#adapter.key(this.#items[i], i) === key);
20341
+ }
20342
+ keys() {
20343
+ this.#keysCache ??= this.#effectiveIndices.map((i) => this.#adapter.key(this.#items[i], i));
20344
+ return this.#keysCache;
20345
+ }
20346
+ labelAt(index) {
20347
+ const eff = this.#effectiveIndices[index];
20348
+ if (eff === void 0) return ``;
20349
+ return this.#adapter.label?.(this.#items[eff], eff) ?? this.#adapter.key(this.#items[eff], eff);
20350
+ }
20351
+ rawKeyAt(index) {
20352
+ return index < 0 || index >= this.#items.length ? `` : this.#adapter.key(this.#items[index], index);
20353
+ }
20354
+ rawSearchTextAt(index) {
20355
+ if (index < 0 || index >= this.#items.length) return ``;
20356
+ const item = this.#items[index];
20357
+ return this.#adapter.searchableText?.(item, index) ?? this.#adapter.label?.(item, index) ?? ``;
20358
+ }
20359
+ isSelectable(index) {
20360
+ const eff = this.#effectiveIndices[index];
20361
+ return eff !== void 0 && !this.#adapter.disabled?.(this.#items[eff], eff);
20362
+ }
20363
+ renderCellContent(index) {
20364
+ const eff = this.#effectiveIndices[index];
20365
+ return eff === void 0 ? void 0 : this.#adapter.render(this.#items[eff], eff);
20366
+ }
20367
+ setFilters(opts) {
20368
+ if (`predicate` in opts) this.#predicate = opts.predicate;
20369
+ if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
20370
+ this.#recomputeEffective();
20371
+ }
20372
+ refresh() {
20373
+ this.#recomputeEffective();
20374
+ }
20375
+ /** Raw-order item at `index`, or `undefined`. */
20376
+ rawItemAt(index) {
20377
+ return this.#items[index];
20378
+ }
20379
+ /** Effective-order item at `index`, or `undefined`. */
20380
+ getItemAt(index) {
20381
+ const eff = this.#effectiveIndices[index];
20382
+ return eff === void 0 ? void 0 : this.#items[eff];
20383
+ }
20384
+ resolveKeyToElement(_key) {}
20385
+ resolveKeyToData(key) {
20386
+ const idx = this.#items.findIndex((item, i) => this.#adapter.key(item, i) === key);
20387
+ return idx >= 0 ? this.#items[idx] : void 0;
20388
+ }
20389
+ #recomputeEffective() {
20390
+ const effective = [];
20391
+ for (let i = 0; i < this.#items.length; i++) {
20392
+ if (this.#predicate !== void 0 && !this.#predicate(i)) continue;
20393
+ if (this.#searchKeys !== void 0 && !this.#searchKeys.has(this.#adapter.key(this.#items[i], i))) continue;
20394
+ effective.push(i);
20395
+ }
20396
+ this.#effectiveIndices = effective;
20397
+ this.#keysCache = void 0;
20398
+ }
20399
+ };
20400
+ //#endregion
19487
20401
  //#region src/grid-list/grid-list.ts
19488
20402
  const ITEM_SELECTOR$1 = `:scope > *`;
19489
20403
  const MIN_ITEM_SIZE = 16;
@@ -19578,6 +20492,7 @@ let GridListElement = class GridListElement extends i {
19578
20492
  this.captionBackdrop = `scrim`;
19579
20493
  this.captionHeight = 24;
19580
20494
  this.itemWrap = `stop`;
20495
+ this.items = [];
19581
20496
  this._scrollTop = 0;
19582
20497
  this._scrollLeft = 0;
19583
20498
  this._viewportHeight = 0;
@@ -19588,37 +20503,37 @@ let GridListElement = class GridListElement extends i {
19588
20503
  this.#itemWrap = `stop`;
19589
20504
  this.#entering = /* @__PURE__ */ new Set();
19590
20505
  this.#reflowTimer = 0;
19591
- this.#slotNames = /* @__PURE__ */ new WeakMap();
19592
- this.#slotSeq = 0;
20506
+ this.#sourceElement = new ElementGridListSource(this);
20507
+ this.#source = this.#sourceElement;
19593
20508
  this.selection = new ListSelectionController({
19594
- getOrderedItems: () => this._getVisibleItems(),
19595
- isSelectable: () => true,
20509
+ getOrderedItems: () => this.#source.keys(),
20510
+ isSelectable: (k) => {
20511
+ const i = this.#source.indexOfKey(k);
20512
+ return i >= 0 && this.#source.isSelectable(i);
20513
+ },
19596
20514
  getModes: () => ({
19597
20515
  selectionMode: this.selectionMode,
19598
20516
  interactionMode: this.interactionMode
19599
20517
  }),
19600
- getKey: keyForElement,
20518
+ getKey: (k) => k,
19601
20519
  onChange: (selected, previous) => {
19602
20520
  this._syncItemStates();
19603
20521
  this.requestUpdate();
19604
- this._dispatch(`list-select`, {
19605
- selected,
19606
- previous,
19607
- selectedKeys: this.selection.selectedKeys
19608
- });
20522
+ this.#emitSelect(selected, previous);
19609
20523
  }
19610
20524
  });
19611
20525
  this.tickled = new TickledController(this, {
19612
- getItems: () => this.#items.map((el) => ({
19613
- id: el,
19614
- enabled: !el.hasAttribute(`not-checked`)
20526
+ getItems: () => this.#source.keys().map((k, i) => ({
20527
+ id: k,
20528
+ enabled: this.#source.isSelectable(i)
19615
20529
  })),
19616
- onTickle: (id) => {
19617
- if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
20530
+ onTickle: (k) => {
20531
+ if (k !== void 0) this.#emitItemEvent(`list-tickle`, k);
19618
20532
  },
19619
20533
  onStateChange: () => this._syncItemStates(),
19620
20534
  itemWrap: this.#itemWrap
19621
20535
  });
20536
+ this.#selectedSnapshot = /* @__PURE__ */ new Set();
19622
20537
  this.#onScroll = (e) => {
19623
20538
  const el = e.target;
19624
20539
  this.#cancelScrollRaf();
@@ -19638,14 +20553,17 @@ let GridListElement = class GridListElement extends i {
19638
20553
  }
19639
20554
  return;
19640
20555
  }
19641
- const count = this.#items.length;
20556
+ const count = this.#source.count;
19642
20557
  if (count === 0) return;
19643
20558
  const win = this.#currentWindow();
19644
20559
  const move = (delta) => {
19645
- const from = this.tickled.tickledId ? this.#items.indexOf(this.tickled.tickledId) : -1;
20560
+ const from = this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
19646
20561
  const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
19647
- const next = this.#items[nextIndex];
19648
- if (next) this.tickled.handleKeyboardNavigation(next);
20562
+ const nextKey = this.#source.keyAt(nextIndex);
20563
+ if (nextKey) {
20564
+ this.tickled.handleKeyboardNavigation(nextKey);
20565
+ this.#scrollItemIntoView(nextKey);
20566
+ }
19649
20567
  };
19650
20568
  const perLine = this.orientation === `grid` ? win.cols : 1;
19651
20569
  switch (e.key) {
@@ -19663,9 +20581,11 @@ let GridListElement = class GridListElement extends i {
19663
20581
  break;
19664
20582
  case `Home`:
19665
20583
  this.tickled.handleKeyboardNavigation(`first`);
20584
+ if (this.tickled.tickledId) this.#scrollItemIntoView(this.tickled.tickledId);
19666
20585
  break;
19667
20586
  case `End`:
19668
20587
  this.tickled.handleKeyboardNavigation(`last`);
20588
+ if (this.tickled.tickledId) this.#scrollItemIntoView(this.tickled.tickledId);
19669
20589
  break;
19670
20590
  case `PageUp`:
19671
20591
  case `PageDown`: {
@@ -19679,7 +20599,7 @@ let GridListElement = class GridListElement extends i {
19679
20599
  const id = this.tickled.tickledId;
19680
20600
  if (id) {
19681
20601
  e.preventDefault();
19682
- this._dispatch(`list-item-click`, { item: id });
20602
+ this.#emitItemEvent(`list-item-click`, id);
19683
20603
  this.#onCellActivate(id);
19684
20604
  }
19685
20605
  break;
@@ -19708,11 +20628,13 @@ let GridListElement = class GridListElement extends i {
19708
20628
  scrollFadeStyles(`x`),
19709
20629
  scrollFadeStyles(`y`),
19710
20630
  listItemFadeKeyframes,
20631
+ listSearchStyles,
19711
20632
  i$3`
19712
20633
  :host {
19713
20634
  --grid-list-item-bg: var(--item-bg);
19714
20635
  --grid-list-radius: var(--radius-m);
19715
20636
  --grid-list-gap: var(--space-xs);
20637
+ position: relative;
19716
20638
  display: block;
19717
20639
  height: 100%;
19718
20640
  min-height: 2em;
@@ -19732,6 +20654,12 @@ let GridListElement = class GridListElement extends i {
19732
20654
  height: 100%;
19733
20655
  overflow-y: auto;
19734
20656
  overflow-x: hidden;
20657
+ /* Consumer control over the native scrollbar (both orientations): set
20658
+ --grid-list-scrollbar-width to "none" to hide it or "thin" to slim it.
20659
+ The container is also exposed as ::part(viewport). */
20660
+ scrollbar-width: var(--grid-list-scrollbar-width, auto);
20661
+ scrollbar-gutter: var(--grid-list-scrollbar-gutter, auto);
20662
+ scrollbar-color: var(--grid-list-scrollbar-color, auto);
19735
20663
  }
19736
20664
  :host([orientation="row"]) .viewport {
19737
20665
  overflow-x: auto;
@@ -19825,16 +20753,19 @@ let GridListElement = class GridListElement extends i {
19825
20753
  overflow: hidden;
19826
20754
  transition: background var(--transition), border-color var(--transition);
19827
20755
  }
19828
- :host(:not([caption-style="separate"])) .cell:hover {
20756
+ :host(:not([caption-style="separate"])) .cell:hover,
20757
+ :host(:not([caption-style="separate"])) .cell.tickled {
19829
20758
  background: var(--item-bg-tickled);
19830
- border-color: var(--item-border-tickled);
20759
+ border: var(--item-border-tickled);
19831
20760
  }
19832
20761
  :host(:not([caption-style="separate"])) .cell.selected {
19833
20762
  outline: 2px solid var(--accent);
19834
20763
  outline-offset: -2px;
19835
20764
  }
19836
- :host(:not([caption-style="separate"])) .cell.tickled {
19837
- box-shadow: inset 0 0 0 2px var(--accent);
20765
+ :host(:not([caption-style="separate"])) .cell.selected.tickled {
20766
+ background: var(--item-bg-selected-tickled);
20767
+ outline: 2px dashed var(--surface-3);
20768
+ outline-offset: -2px;
19838
20769
  }
19839
20770
 
19840
20771
  /* under: caption is a strip inside the box, below the content */
@@ -19893,9 +20824,10 @@ let GridListElement = class GridListElement extends i {
19893
20824
  background: var(--grid-list-item-bg);
19894
20825
  border: 1px solid transparent;
19895
20826
  }
19896
- :host([caption-style="separate"]) .cell:hover .thumb {
20827
+ :host([caption-style="separate"]) .cell:hover .thumb,
20828
+ :host([caption-style="separate"]) .cell.tickled .thumb {
19897
20829
  background: var(--item-bg-tickled);
19898
- border-color: var(--item-border-tickled);
20830
+ border: var(--item-border-tickled);
19899
20831
  }
19900
20832
  :host([caption-style="separate"]) .caption {
19901
20833
  display: flex;
@@ -19907,20 +20839,27 @@ let GridListElement = class GridListElement extends i {
19907
20839
  }
19908
20840
  :host([caption-style="separate"][caption-align="left"]) .caption { align-self: flex-start; }
19909
20841
  :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 {
20842
+ :host([caption-style="separate"]) .cell.selected .thumb {
19912
20843
  outline: 2px solid var(--accent);
19913
20844
  outline-offset: 2px;
19914
20845
  }
19915
- :host([caption-style="separate"]) .cell.tickled .thumb {
19916
- outline-style: dashed;
20846
+ :host([caption-style="separate"]) .cell.selected.tickled .thumb {
20847
+ background: var(--item-bg-selected-tickled);
20848
+ outline: 2px dashed var(--surface-3);
20849
+ outline-offset: 2px;
19917
20850
  }
19918
20851
  :host([caption-style="separate"]) .cell.selected .caption {
19919
20852
  background: var(--accent);
19920
20853
  color: var(--accent-text, #000);
19921
20854
  }
19922
20855
  :host([caption-style="separate"]) .cell.tickled .caption {
19923
- outline: 1px dashed var(--accent);
20856
+ background: var(--item-bg-tickled);
20857
+ outline: var(--item-border-tickled);
20858
+ }
20859
+ :host([caption-style="separate"]) .cell.selected.tickled .caption {
20860
+ background: var(--item-bg-selected-tickled);
20861
+ outline: 2px dashed var(--surface-3);
20862
+ outline-offset: -2px;
19924
20863
  }
19925
20864
 
19926
20865
  /* ---- Insertion / removal animation ----
@@ -19966,16 +20905,71 @@ let GridListElement = class GridListElement extends i {
19966
20905
  */
19967
20906
  #zoomAnchor;
19968
20907
  /**
19969
- * Per-item slot name. Each cell renders its own named `<slot>` and the
19970
- * matching light-DOM child carries `slot="<name>"`, so a child is projected
19971
- * into exactly one cell. (A single unnamed slot would collapse every child
19972
- * into the first cell.)
20908
+ * The active view over the items. Element mode projects light-DOM children
20909
+ * through named `<slot>`s (owned by {@link ElementGridListSource}); data mode
20910
+ * renders from {@link items} via {@link itemAdapter}. Everything else here
20911
+ * addresses items by effective index + key and never branches on the mode.
20912
+ *
20913
+ * Initialised eagerly (element mode) so the public getters — `count`,
20914
+ * `selectedKeys`, … — are safe to touch before the first Lit update. The
20915
+ * first `willUpdate` rebuilds it once the light DOM / `itemAdapter` is known.
19973
20916
  */
19974
- #slotNames;
19975
- #slotSeq;
20917
+ #sourceElement;
20918
+ #source;
20919
+ #searchMode;
20920
+ #searchFilterKeys;
20921
+ /** (Re)build {@link search} for the current mode; a no-op if it already matches. */
20922
+ #ensureSearch() {
20923
+ const mode = this.itemAdapter !== void 0 ? `data` : `element`;
20924
+ if (this.#searchMode === mode && this.search !== void 0) return;
20925
+ const previous = this.search;
20926
+ if (previous) {
20927
+ previous.hostDisconnected();
20928
+ this.removeController(previous);
20929
+ }
20930
+ this.search = mode === `data` ? new ListSearchController(this, { adapter: createDataListSearchAdapter(this, {
20931
+ rawCount: () => this.#source?.rawCount ?? 0,
20932
+ rawKeyAt: (i) => this.#source?.rawKeyAt(i) ?? ``,
20933
+ rawSearchTextAt: (i) => this.#source?.rawSearchTextAt(i) ?? ``,
20934
+ onFilterChange: (keys) => {
20935
+ this.#searchFilterKeys = keys;
20936
+ this.#source?.setFilters({ searchKeys: keys });
20937
+ this.requestUpdate();
20938
+ this.updateComplete.then(() => this.#emitViewportChange());
20939
+ },
20940
+ onActivate: (key) => this.#onCellActivate(key),
20941
+ getHighlightRoot: () => this.renderRoot
20942
+ }) }) : new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
20943
+ itemSelector: ITEM_SELECTOR$1,
20944
+ getLabel: (el) => el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
20945
+ highlightKey: `grid-list-search`
20946
+ }) });
20947
+ this.#searchMode = mode;
20948
+ }
20949
+ #searchRegistry;
20950
+ #unregisterSearchCommands;
20951
+ /** Command registry carrying this grid's `list.search.*` commands. */
20952
+ get registry() {
20953
+ return this.#searchRegistry;
20954
+ }
20955
+ /** Effective search modes: this component's capability (all three) ∩ the `search-modes` attribute. */
20956
+ #effectiveSearchModes() {
20957
+ const raw = this.searchModes?.trim();
20958
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
20959
+ const requested = new Set(raw.split(/\s+/));
20960
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
20961
+ }
20962
+ #syncSearchCommands() {
20963
+ if (!this.#searchRegistry || !this.search) return;
20964
+ this.search.setModes(this.#effectiveSearchModes());
20965
+ this.#unregisterSearchCommands?.();
20966
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
20967
+ }
19976
20968
  connectedCallback() {
19977
20969
  super.connectedCallback();
19978
20970
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
20971
+ if (!this.hasAttribute(`role`)) this.setAttribute(`role`, `listbox`);
20972
+ this.#syncAriaMultiselectable();
19979
20973
  for (const [attr, value] of [
19980
20974
  [`caption-style`, this.captionStyle],
19981
20975
  [`caption-align`, this.captionAlign],
@@ -19984,7 +20978,13 @@ let GridListElement = class GridListElement extends i {
19984
20978
  ]) if (!this.hasAttribute(attr)) this.setAttribute(attr, value);
19985
20979
  this.addEventListener(`keydown`, this._onKeyDown);
19986
20980
  this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
19987
- this.#mutationObserver.observe(this, { childList: true });
20981
+ this.#mutationObserver.observe(this, {
20982
+ childList: true,
20983
+ attributeFilter: [`hidden`]
20984
+ });
20985
+ this.#searchRegistry ??= createCommandRegistry();
20986
+ this.#ensureSearch();
20987
+ this.#syncSearchCommands();
19988
20988
  }
19989
20989
  disconnectedCallback() {
19990
20990
  super.disconnectedCallback();
@@ -19993,6 +20993,9 @@ let GridListElement = class GridListElement extends i {
19993
20993
  this.#resizeObserver = void 0;
19994
20994
  this.#mutationObserver?.disconnect();
19995
20995
  this.#cancelScrollRaf();
20996
+ this.#unregisterSearchCommands?.();
20997
+ this.#unregisterSearchCommands = void 0;
20998
+ this.#searchRegistry = void 0;
19996
20999
  if (this.#reflowTimer) {
19997
21000
  clearTimeout(this.#reflowTimer);
19998
21001
  this.#reflowTimer = 0;
@@ -20001,6 +21004,19 @@ let GridListElement = class GridListElement extends i {
20001
21004
  #onChildrenChanged(records) {
20002
21005
  const previousLength = this.#items.length;
20003
21006
  this.#syncItems();
21007
+ const removedKeys = [];
21008
+ for (const record of records ?? []) for (const node of record.removedNodes) {
21009
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
21010
+ const key = keyForElement(node);
21011
+ if (key !== void 0) removedKeys.push(key);
21012
+ }
21013
+ if (removedKeys.length > 0) {
21014
+ const present = new Set(this.#source.keys());
21015
+ const gone = removedKeys.filter((k) => !present.has(k));
21016
+ if (gone.length > 0) this.selection.removeKeys(gone, (k) => k);
21017
+ const tickledId = this.tickled.tickledId;
21018
+ if (tickledId && gone.includes(tickledId)) this.tickled.focus(void 0);
21019
+ }
20004
21020
  this._syncItemStates();
20005
21021
  this.#noteChildMutation(previousLength, records);
20006
21022
  this.requestUpdate();
@@ -20042,31 +21058,86 @@ let GridListElement = class GridListElement extends i {
20042
21058
  }, REFLOW_WINDOW_MS);
20043
21059
  }
20044
21060
  #emitViewportChange() {
20045
- this._dispatch(`grid-list-viewport-change`, { visibleKeys: this.visibleKeys() });
21061
+ this._dispatch(`grid-list-viewport-change`, {
21062
+ renderedKeys: this.renderedKeys(),
21063
+ visibleKeys: this.visibleKeys()
21064
+ });
20046
21065
  }
20047
21066
  firstUpdated() {
21067
+ if (!this.#source) this.#rebuildSource();
20048
21068
  const el = this.#viewportRef.value;
20049
21069
  if (!el) return;
20050
- this._viewportWidth = el.clientWidth;
20051
- this._viewportHeight = el.clientHeight;
20052
21070
  this.#resizeObserver = new ResizeObserver((entries) => {
20053
21071
  for (const entry of entries) {
20054
- this._viewportWidth = entry.contentRect.width;
20055
- this._viewportHeight = entry.contentRect.height;
21072
+ const w = entry.contentRect.width;
21073
+ const h = entry.contentRect.height;
21074
+ if (w <= 0 || h <= 0) return;
21075
+ this._viewportWidth = w;
21076
+ this._viewportHeight = h;
20056
21077
  }
20057
21078
  });
20058
21079
  this.#resizeObserver.observe(el);
21080
+ this.#measureViewport(false);
20059
21081
  this.#syncItems();
20060
21082
  this.requestUpdate();
20061
21083
  }
21084
+ /** (Re)construct {@link #source} for the current mode and re-apply the filters. */
21085
+ #rebuildSource() {
21086
+ if (this.itemAdapter !== void 0) {
21087
+ this.#source = new DataGridListSource(this.items, this.itemAdapter);
21088
+ this.#sourceElement = void 0;
21089
+ } else {
21090
+ this.#sourceElement = new ElementGridListSource(this);
21091
+ this.#source = this.#sourceElement;
21092
+ }
21093
+ this.#applyFilters();
21094
+ if (this.#searchFilterKeys) this.#source.setFilters({ searchKeys: this.#searchFilterKeys });
21095
+ }
21096
+ /** Push the current filter predicate into whichever source is active. */
21097
+ #applyFilters() {
21098
+ if (this.#source instanceof ElementGridListSource) {
21099
+ const fp = this.filterPredicate;
21100
+ const src = this.#source;
21101
+ src.setFilters({ predicate: fp ? (i) => fp(src.rawElementAt(i)) : void 0 });
21102
+ this.#items = src.effectiveElements();
21103
+ } else if (this.#source instanceof DataGridListSource) {
21104
+ const ip = this.itemFilterPredicate;
21105
+ const src = this.#source;
21106
+ src.setFilters({ predicate: ip ? (i) => ip(src.rawItemAt(i), i) : void 0 });
21107
+ }
21108
+ }
20062
21109
  willUpdate(changed) {
21110
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
21111
+ if (changed.has(`filterPredicate`) || changed.has(`itemFilterPredicate`)) {
21112
+ this.#applyFilters();
21113
+ this.updateComplete.then(() => this.#emitViewportChange());
21114
+ }
21115
+ if (changed.has(`items`) || changed.has(`itemAdapter`)) {
21116
+ this.#zoomAnchor = void 0;
21117
+ const prevKeys = this.#source?.keys() ?? [];
21118
+ const modeFlipped = this.itemAdapter !== void 0 !== this.#source instanceof DataGridListSource;
21119
+ this.#rebuildSource();
21120
+ const nextKeys = this.#source.keys();
21121
+ const nextKeySet = new Set(nextKeys);
21122
+ const removedKeys = prevKeys.filter((k) => !nextKeySet.has(k));
21123
+ if (removedKeys.length > 0) this.selection.removeKeys(removedKeys, (k) => k);
21124
+ const tickledId = this.tickled.tickledId;
21125
+ if (tickledId && !nextKeySet.has(tickledId)) this.tickled.focus(void 0);
21126
+ this._syncItemStates();
21127
+ this.updateComplete.then(() => this.#emitViewportChange());
21128
+ if (modeFlipped) {
21129
+ this.#ensureSearch();
21130
+ this.#syncSearchCommands();
21131
+ }
21132
+ this.search?.refresh();
21133
+ }
20063
21134
  if (changed.has(`itemSize`) || changed.has(`gap`)) {
20064
21135
  this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
20065
21136
  this.gap = Math.max(0, Math.round(this.gap));
20066
21137
  }
20067
21138
  if (changed.has(`captionHeight`)) this.captionHeight = Math.max(0, Math.round(this.captionHeight));
20068
21139
  if (changed.has(`itemWrap`)) this.#itemWrap = this.itemWrap;
20069
- if ((changed.has(`itemSize`) || changed.has(`gap`) || changed.has(`captionStyle`) || changed.has(`captionHeight`) || changed.has(`orientation`)) && this.hasUpdated && this.#items.length > 0) {
21140
+ if ((changed.has(`itemSize`) || changed.has(`gap`) || changed.has(`captionStyle`) || changed.has(`captionHeight`) || changed.has(`orientation`)) && this.hasUpdated && this.#source.count > 0) {
20070
21141
  const prevOrientation = changed.get(`orientation`) ?? this.orientation;
20071
21142
  const horizontal = prevOrientation === `row`;
20072
21143
  const prevOffset = horizontal ? this._scrollLeft : this._scrollTop;
@@ -20076,19 +21147,13 @@ let GridListElement = class GridListElement extends i {
20076
21147
  const prevGap = changed.get(`gap`) ?? this.gap;
20077
21148
  const prevCaptionStyle = changed.get(`captionStyle`) ?? this.captionStyle;
20078
21149
  const prevCaptionHeight = changed.get(`captionHeight`) ?? this.captionHeight;
20079
- const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
20080
- const prev = computeGridWindow(this.#items.length, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
20081
- const mainStride = horizontal ? prev.colStride : prev.rowStride;
20082
- const centerLine = (prevOffset + prevExtent / 2) / mainStride;
20083
- const lineFloor = Math.max(0, Math.floor(centerLine));
20084
- const index = horizontal ? Math.min(this.#items.length - 1, lineFloor) : Math.min(this.#items.length - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
20085
- this.#zoomAnchor = {
20086
- index,
20087
- lineFrac: centerLine - lineFloor
20088
- };
21150
+ this.#captureCentreAnchor(prevOffset, prevExtent, prevItemSize, prevGap, prevCaptionStyle, prevCaptionHeight, prevOrientation);
20089
21151
  }
20090
21152
  }
20091
21153
  }
21154
+ #syncAriaMultiselectable() {
21155
+ this.setAttribute(`aria-multiselectable`, this.selection.canSelectMultiple() ? `true` : `false`);
21156
+ }
20092
21157
  /** Full cell height: the square, plus a caption band in `separate` / `under`. */
20093
21158
  #rowHeight() {
20094
21159
  const captionBand = this.captionStyle === `separate` || this.captionStyle === `under`;
@@ -20100,7 +21165,10 @@ let GridListElement = class GridListElement extends i {
20100
21165
  updated(changed) {
20101
21166
  super.updated(changed);
20102
21167
  this.#syncItems();
20103
- if (changed.has(`selectionMode`) || changed.has(`interactionMode`)) this.selection.clearAnchor();
21168
+ if (changed.has(`selectionMode`) || changed.has(`interactionMode`)) {
21169
+ this.selection.clearAnchor();
21170
+ this.#syncAriaMultiselectable();
21171
+ }
20104
21172
  if (changed.has(`orientation`)) {
20105
21173
  const vp = this.#viewportRef.value;
20106
21174
  if (vp) {
@@ -20113,6 +21181,11 @@ let GridListElement = class GridListElement extends i {
20113
21181
  }
20114
21182
  }
20115
21183
  }
21184
+ const vp = this.#viewportRef.value;
21185
+ if (vp) {
21186
+ if (Math.abs(vp.scrollTop - this._scrollTop) > .5) vp.scrollTop = this._scrollTop;
21187
+ if (Math.abs(vp.scrollLeft - this._scrollLeft) > .5) vp.scrollLeft = this._scrollLeft;
21188
+ }
20116
21189
  this.#applyZoomAnchor();
20117
21190
  }
20118
21191
  /** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
@@ -20143,30 +21216,67 @@ let GridListElement = class GridListElement extends i {
20143
21216
  this._scrollTop = next;
20144
21217
  }
20145
21218
  }
20146
- #syncItems() {
20147
- this.#items = Array.from(this.querySelectorAll(ITEM_SELECTOR$1));
20148
- for (const el of this.#items) {
20149
- const name = this.#slotNameFor(el);
20150
- if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
20151
- }
21219
+ #captureCentreAnchor(prevOffset, prevExtent, prevItemSize, prevGap, prevCaptionStyle, prevCaptionHeight, prevOrientation) {
21220
+ const count = this.#source?.count ?? 0;
21221
+ if (count === 0) return;
21222
+ const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
21223
+ const horizontal = prevOrientation === `row`;
21224
+ const prev = computeGridWindow(count, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
21225
+ const mainStride = horizontal ? prev.colStride : prev.rowStride;
21226
+ const centerLine = (prevOffset + prevExtent / 2) / mainStride;
21227
+ const lineFloor = Math.max(0, Math.floor(centerLine));
21228
+ const index = horizontal ? Math.min(count - 1, lineFloor) : Math.min(count - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
21229
+ this.#zoomAnchor = {
21230
+ index,
21231
+ lineFrac: centerLine - lineFloor
21232
+ };
20152
21233
  }
20153
- #slotNameFor(el) {
20154
- let name = this.#slotNames.get(el);
20155
- if (name === void 0) {
20156
- this.#slotSeq += 1;
20157
- name = `gl-item-${this.#slotSeq}`;
20158
- this.#slotNames.set(el, name);
21234
+ /**
21235
+ * Re-read the viewport after an ancestor's visibility/layout changed.
21236
+ * Safe while hidden (zero-sized reads are ignored); forces a repaint of the
21237
+ * virtual window even when the size is unchanged.
21238
+ */
21239
+ refreshLayout() {
21240
+ this.#measureViewport(true);
21241
+ }
21242
+ /** Alias of {@link refreshLayout}. */
21243
+ remeasure() {
21244
+ this.#measureViewport(true);
21245
+ }
21246
+ #measureViewport(forceRender = false) {
21247
+ const vp = this.#viewportRef.value;
21248
+ if (!vp) return;
21249
+ const w = vp.clientWidth;
21250
+ const h = vp.clientHeight;
21251
+ if (w <= 0 || h <= 0) return;
21252
+ const horizontal = this.orientation === `row`;
21253
+ const changed = w !== this._viewportWidth || h !== this._viewportHeight;
21254
+ if (changed) {
21255
+ const prevOffset = horizontal ? this._scrollLeft : this._scrollTop;
21256
+ const prevExtent = horizontal ? w : h;
21257
+ this.#captureCentreAnchor(prevOffset, prevExtent, this.itemSize, this.gap, this.captionStyle, this.captionHeight, this.orientation);
21258
+ this._viewportWidth = w;
21259
+ this._viewportHeight = h;
20159
21260
  }
20160
- return name;
21261
+ if (forceRender && !changed) {
21262
+ this.#captureCentreAnchor(horizontal ? this._scrollLeft : this._scrollTop, horizontal ? w : h, this.itemSize, this.gap, this.captionStyle, this.captionHeight, this.orientation);
21263
+ this.requestUpdate();
21264
+ }
21265
+ if (changed || forceRender) this.updateComplete.then(() => this.#emitViewportChange());
20161
21266
  }
20162
- _getVisibleItems() {
20163
- return this.#items.filter((el) => {
20164
- if (el.hidden) return false;
20165
- return true;
20166
- });
21267
+ /**
21268
+ * Element mode only: re-read the light DOM into the source and refresh the
21269
+ * `#items` cache + filter. A no-op in data mode (the source is rebuilt through
21270
+ * `#rebuildSource` when `items` / `itemAdapter` change).
21271
+ */
21272
+ #syncItems() {
21273
+ if (this.#source instanceof ElementGridListSource) {
21274
+ this.#source.refresh();
21275
+ this.#applyFilters();
21276
+ }
20167
21277
  }
20168
21278
  get count() {
20169
- return this.#items.length;
21279
+ return this.#source.count;
20170
21280
  }
20171
21281
  /**
20172
21282
  * Number of cells currently rendered in the shadow DOM. With virtualization
@@ -20177,20 +21287,38 @@ let GridListElement = class GridListElement extends i {
20177
21287
  return this.renderRoot.querySelectorAll(`.cell`).length;
20178
21288
  }
20179
21289
  get selectedItems() {
20180
- return this.selection.selected;
21290
+ const els = /* @__PURE__ */ new Set();
21291
+ for (const key of this.selection.selectedKeys) {
21292
+ const el = this.#source.resolveKeyToElement(key);
21293
+ if (el) els.add(el);
21294
+ }
21295
+ return els;
20181
21296
  }
20182
- /** The selection as stable `data-key` / `data-value` strings. */
21297
+ /**
21298
+ * The selection as stable `data-key` / `data-value` strings. Auto-assigned
21299
+ * keys for keyless light-DOM items are omitted (they are not a stable contract
21300
+ * for the consumer).
21301
+ */
20183
21302
  get selectedKeys() {
20184
- return this.selection.selectedKeys;
21303
+ const out = /* @__PURE__ */ new Set();
21304
+ for (const k of this.selection.selectedKeys) if (!isSyntheticKey(k)) out.add(k);
21305
+ return out;
20185
21306
  }
20186
21307
  select(item) {
20187
- this.selection.replace(item);
21308
+ const key = keyForElement(item);
21309
+ if (key) this.selection.replace(key);
20188
21310
  }
20189
21311
  selectMany(items) {
20190
- this.selection.selectMany(items);
21312
+ const keys = [];
21313
+ for (const item of items) {
21314
+ const key = keyForElement(item);
21315
+ if (key) keys.push(key);
21316
+ }
21317
+ this.selection.selectMany(keys);
20191
21318
  }
20192
21319
  deselect(item) {
20193
- this.selection.deselect(item);
21320
+ const key = keyForElement(item);
21321
+ if (key) this.selection.deselect(key);
20194
21322
  }
20195
21323
  clearSelection() {
20196
21324
  this.selection.clear();
@@ -20204,44 +21332,126 @@ let GridListElement = class GridListElement extends i {
20204
21332
  * have a `data-key` (or `data-value`).
20205
21333
  */
20206
21334
  selectKeys(keys) {
20207
- this.selection.selectKeys(keys, (key) => this.getItem(key));
21335
+ this.selection.selectKeys(keys, (k) => k);
20208
21336
  }
20209
21337
  /** Add items by key to the selection (multi-select modes only). */
20210
21338
  addSelectedKeys(keys) {
20211
- this.selection.addKeys(keys, (key) => this.getItem(key));
21339
+ this.selection.addKeys(keys, (k) => k);
20212
21340
  }
20213
21341
  /** Remove items by key from the selection. */
20214
21342
  removeSelectedKeys(keys) {
20215
- this.selection.removeKeys(keys, (key) => this.getItem(key));
21343
+ this.selection.removeKeys(keys, (k) => k);
21344
+ }
21345
+ /**
21346
+ * Data mode only: raw index of `key` in `this.items`, or `-1`. Data-mode keys
21347
+ * come straight from the adapter, so this is exact even under a filter/search.
21348
+ */
21349
+ #rawIndexOfKey(key) {
21350
+ const adapter = this.itemAdapter;
21351
+ if (!adapter) return -1;
21352
+ return this.items.findIndex((it, i) => adapter.key(it, i) === key);
21353
+ }
21354
+ /**
21355
+ * Data mode: replace the whole item array. Selection and cursor keep any key
21356
+ * that is still present; keys that vanish are dropped (one `list-select`).
21357
+ * A no-op in element mode.
21358
+ */
21359
+ setItems(items) {
21360
+ if (this.itemAdapter) this.items = items;
21361
+ }
21362
+ /**
21363
+ * Data mode: append items to the end. Scroll position, selection and cursor
21364
+ * are untouched; appended items don't play the mid-list `[reflowing]` slide.
21365
+ * A no-op in element mode.
21366
+ */
21367
+ appendItems(items) {
21368
+ if (this.itemAdapter && items.length > 0) this.items = [...this.items, ...items];
21369
+ }
21370
+ /**
21371
+ * Data mode: replace the item carrying `key`. A visible cell re-renders in
21372
+ * place (the keyed subtree is kept); an offscreen item shows its new content
21373
+ * when scrolled into view. Scroll and selection are untouched. Unknown keys
21374
+ * are ignored.
21375
+ */
21376
+ updateItem(key, item) {
21377
+ if (!this.itemAdapter) return;
21378
+ const idx = this.#rawIndexOfKey(key);
21379
+ if (idx < 0) return;
21380
+ const next = [...this.items];
21381
+ next[idx] = item;
21382
+ this.items = next;
21383
+ }
21384
+ /**
21385
+ * Data mode: remove the items carrying these keys. Removed keys are dropped
21386
+ * from selection and cursor (via the `items` change). A no-op in element mode.
21387
+ */
21388
+ removeItems(keys) {
21389
+ if (!this.itemAdapter) return;
21390
+ const adapter = this.itemAdapter;
21391
+ const keySet = new Set(keys);
21392
+ this.items = this.items.filter((it, i) => !keySet.has(adapter.key(it, i)));
21393
+ }
21394
+ /**
21395
+ * Data mode: drop every item. Selection and cursor are reset and one
21396
+ * `list-select` with empty sets is emitted. A no-op in element mode.
21397
+ */
21398
+ clearItems() {
21399
+ if (!this.itemAdapter) return;
21400
+ this.selection.reset();
21401
+ this.tickled.focus(void 0);
21402
+ this.items = [];
21403
+ this.#emitSelect(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
20216
21404
  }
20217
21405
  /** Move the cursor to the item with this key and scroll it into view. */
20218
21406
  focusKey(key) {
20219
- const el = this.getItem(key);
20220
- if (!el) return;
20221
- this.tickled.focus(el);
20222
- this.#scrollItemIntoView(el);
21407
+ if (this.#source.indexOfKey(key) < 0) return;
21408
+ this.tickled.focus(key);
21409
+ this.#scrollItemIntoView(key);
20223
21410
  }
20224
21411
  /** The key of the item under the cursor, or `undefined`. */
20225
21412
  get tickledKey() {
20226
- const id = this.tickled.tickledId;
20227
- return id ? keyForElement(id) : void 0;
21413
+ return this.tickled.tickledId;
20228
21414
  }
21415
+ /** Element mode: the light-DOM element carrying `key`, or `undefined`. */
20229
21416
  getItem(key) {
20230
- return this.#items.find((el) => keyForElement(el) === key);
21417
+ return this.#source.resolveKeyToElement(key);
20231
21418
  }
21419
+ /** Element mode: the light-DOM element at effective index `index`. */
20232
21420
  getItemByIndex(index) {
20233
21421
  return this.#items[index];
20234
21422
  }
21423
+ /** Data mode: the data value carrying `key`, or `undefined`. */
21424
+ getData(key) {
21425
+ return this.#source.resolveKeyToData(key);
21426
+ }
20235
21427
  getKey(item) {
20236
21428
  return keyForElement(item);
20237
21429
  }
21430
+ /**
21431
+ * Keys of items in the rendered window (viewport + overscan) — the priority
21432
+ * set for prefetching. Use {@link visibleKeys} for the strict viewport
21433
+ * intersection. Auto-assigned keyless-item keys are omitted.
21434
+ */
21435
+ renderedKeys() {
21436
+ return this.#windowKeys(this.#currentWindow());
21437
+ }
21438
+ /**
21439
+ * Keys of items actually intersecting the viewport (no overscan).
21440
+ * Auto-assigned keyless-item keys are omitted.
21441
+ */
20238
21442
  visibleKeys() {
20239
- const win = this.#currentWindow();
20240
- return this.#items.slice(win.startIndex, win.endIndex).map((el) => keyForElement(el)).filter((k) => k !== void 0);
21443
+ return this.#windowKeys(this.#currentWindow(true));
21444
+ }
21445
+ #windowKeys(win) {
21446
+ return this.#source.keys().slice(win.startIndex, win.endIndex).filter((k) => !isSyntheticKey(k));
21447
+ }
21448
+ #currentWindow(noOverscan = false) {
21449
+ const overscan = noOverscan ? 0 : 2;
21450
+ return computeGridWindow(this.#source.count, this.itemSize, this.#rowHeight(), this.gap, this._viewportWidth, this._viewportHeight, this.orientation === `row` ? this._scrollLeft : this._scrollTop, this.orientation, overscan);
20241
21451
  }
20242
- /** Adjust the scroll offset so `el` is fully within the viewport. */
20243
- #scrollItemIntoView(el) {
20244
- const index = this.#items.indexOf(el);
21452
+ /** Adjust the scroll offset so `key` is fully within the viewport. */
21453
+ #scrollItemIntoView(key) {
21454
+ const index = this.#source.indexOfKey(key);
20245
21455
  const vp = this.#viewportRef.value;
20246
21456
  if (index < 0 || !vp) return;
20247
21457
  const win = this.#currentWindow();
@@ -20270,12 +21480,43 @@ let GridListElement = class GridListElement extends i {
20270
21480
  }
20271
21481
  }
20272
21482
  _syncItemStates() {
21483
+ const selected = this.selection.selectedKeys;
20273
21484
  for (const el of this.#items) {
20274
- el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
20275
- el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
21485
+ const key = keyForElement(el);
21486
+ el.toggleAttribute(`data-tickled`, key !== void 0 && this.tickled.isTickled(key));
21487
+ el.toggleAttribute(`data-selected`, key !== void 0 && selected.has(key));
20276
21488
  }
20277
21489
  this.setAttribute(`data-input-mode`, this.tickled.inputMode);
20278
21490
  }
21491
+ #emitSelect(selected, previous) {
21492
+ const selectedEls = /* @__PURE__ */ new Set();
21493
+ const previousEls = /* @__PURE__ */ new Set();
21494
+ for (const key of selected) {
21495
+ const el = this.#source.resolveKeyToElement(key);
21496
+ if (el) selectedEls.add(el);
21497
+ }
21498
+ for (const key of previous) {
21499
+ const el = this.#source.resolveKeyToElement(key);
21500
+ if (el) previousEls.add(el);
21501
+ }
21502
+ this._dispatch(`list-select`, {
21503
+ selected: selectedEls,
21504
+ previous: previousEls,
21505
+ selectedKeys: this.selectedKeys,
21506
+ previousKeys: new Set([...previous].filter((k) => !isSyntheticKey(k)))
21507
+ });
21508
+ }
21509
+ #emitItemEvent(name, key) {
21510
+ const index = this.#source.indexOfKey(key);
21511
+ const element = this.#source.resolveKeyToElement(key);
21512
+ const item = element ?? this.#source.resolveKeyToData(key);
21513
+ this._dispatch(name, {
21514
+ item,
21515
+ key,
21516
+ index,
21517
+ element
21518
+ });
21519
+ }
20279
21520
  _dispatch(name, detail) {
20280
21521
  this.dispatchEvent(new CustomEvent(name, {
20281
21522
  detail,
@@ -20283,9 +21524,11 @@ let GridListElement = class GridListElement extends i {
20283
21524
  composed: true
20284
21525
  }));
20285
21526
  }
21527
+ /** Selected keys, snapshotted once per render so the cell renderers don't each rebuild the set. */
21528
+ #selectedSnapshot;
20286
21529
  render() {
20287
21530
  const win = this.#currentWindow();
20288
- const slice = this.#items.slice(win.startIndex, win.endIndex);
21531
+ this.#selectedSnapshot = this.selection.selectedKeys;
20289
21532
  const spacerStyle = win.horizontal ? `width:${win.totalWidth}px;height:100%` : `height:${win.totalHeight}px`;
20290
21533
  const scrollFade = win.horizontal ? `scroll-fade-x` : `scroll-fade-y`;
20291
21534
  return b`
@@ -20297,14 +21540,56 @@ let GridListElement = class GridListElement extends i {
20297
21540
  @scroll=${this.#onScroll}
20298
21541
  >
20299
21542
  <div class="spacer" style=${spacerStyle}>
20300
- ${repeat(slice, (el) => this.#slotNameFor(el), (el, i) => this.#renderCell(el, win.startIndex + i, win))}
21543
+ ${this.#source instanceof DataGridListSource ? this.#renderDataCells(win) : this.#renderElementCells(win)}
20301
21544
  </div>
20302
21545
  </div>
21546
+ ${this.search?.renderField()}
20303
21547
  `;
20304
21548
  }
21549
+ #renderDataCells(win) {
21550
+ const slice = Array.from({ length: win.endIndex - win.startIndex }, (_, i) => win.startIndex + i);
21551
+ return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderDataCell(index, win));
21552
+ }
21553
+ #renderDataCell(index, win) {
21554
+ const key = this.#source.keyAt(index);
21555
+ const selected = this.#selectedSnapshot.has(key);
21556
+ const tickled = this.tickled.isTickled(key);
21557
+ const style = this.captionStyle;
21558
+ const separate = style === `separate`;
21559
+ const caption = style === `hidden` ? `` : this.#source.labelAt(index);
21560
+ const showCaption = caption !== ``;
21561
+ const cellHeight = win.rowStride - this.gap;
21562
+ const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
21563
+ const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
21564
+ const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
21565
+ return b`
21566
+ <div
21567
+ class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
21568
+ part="item${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
21569
+ role="option"
21570
+ aria-selected=${selected ? `true` : `false`}
21571
+ style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
21572
+ data-index=${index}
21573
+ data-key=${key}
21574
+ ?data-selected=${selected}
21575
+ ?data-tickled=${tickled}
21576
+ @click=${(e) => this.#onCellClick(e, key)}
21577
+ @dblclick=${() => this.#onCellActivate(key)}
21578
+ @pointerenter=${() => this.tickled.handlePointerEnter(key)}
21579
+ >
21580
+ <div class="content" part="content">${this.#source.renderCellContent(index)}</div>
21581
+ ${showCaption ? b`<div class="caption" part="caption" style=${captionCss} title=${caption}>${caption}</div>` : ``}
21582
+ </div>
21583
+ `;
21584
+ }
21585
+ #renderElementCells(win) {
21586
+ const slice = this.#items.slice(win.startIndex, win.endIndex);
21587
+ return repeat(slice, (_el, i) => this.#source.keyAt(win.startIndex + i), (el, i) => this.#renderCell(el, win.startIndex + i, win));
21588
+ }
20305
21589
  #renderCell(el, index, win) {
20306
- const selected = this.selection.selected.has(el);
20307
- const tickled = this.tickled.isTickled(el);
21590
+ const key = this.#source.keyAt(index);
21591
+ const selected = this.#selectedSnapshot.has(key);
21592
+ const tickled = this.tickled.isTickled(key);
20308
21593
  const entering = this.#entering.has(el);
20309
21594
  const style = this.captionStyle;
20310
21595
  const separate = style === `separate`;
@@ -20318,17 +21603,22 @@ let GridListElement = class GridListElement extends i {
20318
21603
  return b`
20319
21604
  <div
20320
21605
  class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}"
20321
- part="item"
21606
+ part="item${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
21607
+ role="option"
21608
+ aria-selected=${selected ? `true` : `false`}
20322
21609
  style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
20323
21610
  data-index=${index}
20324
- @click=${(e) => this.#onCellClick(e, el)}
20325
- @dblclick=${() => this.#onCellActivate(el)}
20326
- @pointerenter=${() => this.tickled.handlePointerEnter(el)}
21611
+ data-key=${key}
21612
+ ?data-selected=${selected}
21613
+ ?data-tickled=${tickled}
21614
+ @click=${(e) => this.#onCellClick(e, key)}
21615
+ @dblclick=${() => this.#onCellActivate(key)}
21616
+ @pointerenter=${() => this.tickled.handlePointerEnter(key)}
20327
21617
  >
20328
21618
  <div class="thumb" part="thumb" style=${thumbStyle}>
20329
- <slot name=${this.#slotNameFor(el)}></slot>
21619
+ ${this.#source.renderCellContent(index)}
20330
21620
  </div>
20331
- ${showCaption ? b`<div class="caption" part="caption" style=${captionCss}>${caption}</div>` : ``}
21621
+ ${showCaption ? b`<div class="caption" part="caption" style=${captionCss} title=${caption}>${caption}</div>` : ``}
20332
21622
  </div>
20333
21623
  `;
20334
21624
  }
@@ -20339,16 +21629,13 @@ let GridListElement = class GridListElement extends i {
20339
21629
  this.#scrollRaf = 0;
20340
21630
  }
20341
21631
  }
20342
- #onCellClick(e, item) {
20343
- this.selection.handleClick(item, e);
20344
- this._dispatch(`list-item-click`, { item });
21632
+ #onCellClick(e, key) {
21633
+ this.selection.handleClick(key, e);
21634
+ this.#emitItemEvent(`list-item-click`, key);
20345
21635
  }
20346
- #onCellActivate(item) {
20347
- if (this.interactionMode !== `manual`) this.selection.replace(item);
20348
- this._dispatch(`list-activate`, { item });
20349
- }
20350
- #currentWindow() {
20351
- return computeGridWindow(this.#items.length, this.itemSize, this.#rowHeight(), this.gap, this._viewportWidth, this._viewportHeight, this.orientation === `row` ? this._scrollLeft : this._scrollTop, this.orientation);
21636
+ #onCellActivate(key) {
21637
+ if (this.interactionMode !== `manual`) this.selection.replace(key);
21638
+ this.#emitItemEvent(`list-activate`, key);
20352
21639
  }
20353
21640
  };
20354
21641
  __decorate([property({
@@ -20396,6 +21683,15 @@ __decorate([property({
20396
21683
  type: String,
20397
21684
  attribute: `item-wrap`
20398
21685
  })], GridListElement.prototype, "itemWrap", void 0);
21686
+ __decorate([property({ attribute: false })], GridListElement.prototype, "filterPredicate", void 0);
21687
+ __decorate([property({ attribute: false })], GridListElement.prototype, "itemFilterPredicate", void 0);
21688
+ __decorate([property({
21689
+ type: String,
21690
+ attribute: `search-modes`,
21691
+ reflect: true
21692
+ })], GridListElement.prototype, "searchModes", void 0);
21693
+ __decorate([property({ attribute: false })], GridListElement.prototype, "items", void 0);
21694
+ __decorate([property({ attribute: false })], GridListElement.prototype, "itemAdapter", void 0);
20399
21695
  __decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
20400
21696
  __decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
20401
21697
  __decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
@@ -20415,6 +21711,7 @@ let PanelElement = class PanelElement extends i {
20415
21711
  this.toggleInteraction = `implicit`;
20416
21712
  this.variant = `titlebar`;
20417
21713
  this.hideTitlebar = false;
21714
+ this.keepToolbar = false;
20418
21715
  }
20419
21716
  _onTitlebarToggle(event) {
20420
21717
  event.stopPropagation();
@@ -20434,6 +21731,7 @@ let PanelElement = class PanelElement extends i {
20434
21731
  this.remove();
20435
21732
  }
20436
21733
  render() {
21734
+ const showToolbar = this.keepToolbar || !this.collapsed;
20437
21735
  return b`
20438
21736
  ${this.hideTitlebar ? A : b`
20439
21737
  <ixfx-titlebar
@@ -20448,7 +21746,7 @@ let PanelElement = class PanelElement extends i {
20448
21746
  @toggle=${this._onTitlebarToggle}
20449
21747
  @close=${this._onCloseClick}
20450
21748
  >
20451
- <slot name="toolbar" slot="toolbar"></slot>
21749
+ ${showToolbar ? b`<slot name="toolbar" slot="toolbar"></slot>` : A}
20452
21750
  </ixfx-titlebar>
20453
21751
  `}
20454
21752
 
@@ -20548,6 +21846,30 @@ let PanelElement = class PanelElement extends i {
20548
21846
  :host([variant="midline"][orientation="vertical"]) .content-wrapper {
20549
21847
  background: transparent;
20550
21848
  }
21849
+
21850
+ /* === Tree variant: VS Code explorer / Outline style === */
21851
+
21852
+ :host([variant="tree"]) {
21853
+ border: none !important;
21854
+ border-radius: 0 !important;
21855
+ background: transparent;
21856
+ }
21857
+
21858
+ :host([variant="tree"]) .content-inner {
21859
+ box-shadow: none;
21860
+ background: transparent;
21861
+ padding: 0;
21862
+ }
21863
+
21864
+ :host([variant="tree"]) .content-wrapper {
21865
+ background: transparent;
21866
+ }
21867
+
21868
+ :host([variant="tree"][orientation="vertical"]) .content-inner {
21869
+ width: max-content;
21870
+ height: 100%;
21871
+ transform: none;
21872
+ }
20551
21873
  `];
20552
21874
  }
20553
21875
  };
@@ -20583,6 +21905,10 @@ __decorate([property({
20583
21905
  type: Boolean,
20584
21906
  attribute: `hide-titlebar`
20585
21907
  })], PanelElement.prototype, "hideTitlebar", void 0);
21908
+ __decorate([property({
21909
+ type: Boolean,
21910
+ attribute: `keep-toolbar`
21911
+ })], PanelElement.prototype, "keepToolbar", void 0);
20586
21912
  PanelElement = __decorate([safeCustomElement(`ixfx-panel`)], PanelElement);
20587
21913
  //#endregion
20588
21914
  //#region src/grouped-item-lister/grouped-item-lister.ts
@@ -20778,6 +22104,10 @@ let TreeBaseElement = class TreeBaseElement extends i {
20778
22104
  }
20779
22105
  });
20780
22106
  this._focusedDepth = 0;
22107
+ this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
22108
+ labelSelector: `.tree-label`,
22109
+ highlightKey: `incr-search-highlight`
22110
+ }) });
20781
22111
  this.#config = { indent: 20 };
20782
22112
  this.#renderedKeys = [];
20783
22113
  this.#lastNodeByKey = /* @__PURE__ */ new Map();
@@ -20786,6 +22116,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
20786
22116
  this.#leavingAnimating = /* @__PURE__ */ new Set();
20787
22117
  this.#treeAnimReady = false;
20788
22118
  this.#onKeyDown = (event) => {
22119
+ if (this.search.isOpen) return;
20789
22120
  const nodes = this.#getAllNodes();
20790
22121
  if (nodes.length === 0) return;
20791
22122
  if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
@@ -20906,6 +22237,19 @@ let TreeBaseElement = class TreeBaseElement extends i {
20906
22237
  get selectedNodes() {
20907
22238
  return this._selectedNodes;
20908
22239
  }
22240
+ #unregisterSearchCommands;
22241
+ #effectiveSearchModes() {
22242
+ const raw = this.searchModes?.trim();
22243
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
22244
+ const requested = new Set(raw.split(/\s+/));
22245
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
22246
+ }
22247
+ #syncSearchCommands() {
22248
+ if (!this.#ownedRegistry) return;
22249
+ this.search.setModes(this.#effectiveSearchModes());
22250
+ this.#unregisterSearchCommands?.();
22251
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
22252
+ }
20909
22253
  #pendingNode;
20910
22254
  #pendingController;
20911
22255
  #modelUnsubscribe;
@@ -20948,6 +22292,13 @@ let TreeBaseElement = class TreeBaseElement extends i {
20948
22292
  _isLeaving(node) {
20949
22293
  return this.#leaving.has(node.item.key);
20950
22294
  }
22295
+ /**
22296
+ * True when `filterPredicate` is set and neither `node` nor any descendant
22297
+ * matches — the row should carry the `filtered` class (`display: none`).
22298
+ */
22299
+ _isFilteredOut(node, depth) {
22300
+ return this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
22301
+ }
20951
22302
  /** Splice the fading ghost rows back into the visible list near where they were. */
20952
22303
  #weaveLeaving(nodes) {
20953
22304
  if (this.#leaving.size === 0) return nodes;
@@ -21044,11 +22395,14 @@ let TreeBaseElement = class TreeBaseElement extends i {
21044
22395
  execute: () => this.deselectAll()
21045
22396
  });
21046
22397
  this.registry = registry;
22398
+ this.#syncSearchCommands();
21047
22399
  }
21048
22400
  disconnectedCallback() {
21049
22401
  super.disconnectedCallback();
21050
22402
  this.#modelUnsubscribe?.();
21051
22403
  this.#modelUnsubscribe = void 0;
22404
+ this.#unregisterSearchCommands?.();
22405
+ this.#unregisterSearchCommands = void 0;
21052
22406
  if (this.#ownedRegistry) {
21053
22407
  this.#ownedRegistry.unregister(`select-all`);
21054
22408
  this.#ownedRegistry.unregister(`deselect-all`);
@@ -21146,6 +22500,28 @@ let TreeBaseElement = class TreeBaseElement extends i {
21146
22500
  if (!keyPath) return false;
21147
22501
  return this.navigateTo(keyPath);
21148
22502
  }
22503
+ /** Expand ancestors + move the cursor to `key`, without touching the selection (search "navigate" mode). */
22504
+ revealKey(key) {
22505
+ if (!this.root?.children) return false;
22506
+ const path = this.#findNodePath(this.root.children, (n) => n.item.key === key);
22507
+ if (!path) return false;
22508
+ for (let i = 0; i < path.length - 1; i++) {
22509
+ this.#applyExclusivity(path[i]);
22510
+ this._expandedKeys.add(path[i]);
22511
+ }
22512
+ this.tickled.setTickled(key);
22513
+ this.requestUpdate();
22514
+ this.updateComplete.then(() => this.#scrollKeyIntoView(key));
22515
+ return true;
22516
+ }
22517
+ /** Replace the selection with `nodes` (subject to `selectionFilter`) — search "select" mode. */
22518
+ selectMany(nodes) {
22519
+ const prev = new Set(this._selectedNodes);
22520
+ this._selectedNodes = new Set([...nodes].filter((n) => this.#canSelect(n)));
22521
+ this.selectedNode = [...this._selectedNodes].at(-1);
22522
+ this.#dispatchSelect(prev);
22523
+ this.requestUpdate();
22524
+ }
21149
22525
  getNodes() {
21150
22526
  if (!this.root?.children) return [];
21151
22527
  const result = [];
@@ -21549,7 +22925,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21549
22925
  node: n,
21550
22926
  depth: d
21551
22927
  });
21552
- if (this._expandedKeys.has(n.item.key) && n.children) for (const child of n.children) visit(child, d + 1);
22928
+ if (n.children && this._isExpanded(n, d)) for (const child of n.children) visit(child, d + 1);
21553
22929
  };
21554
22930
  if (node.children) for (const child of node.children) visit(child, depth + 1);
21555
22931
  return result;
@@ -21606,6 +22982,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21606
22982
  >
21607
22983
  ${this.root ? this.#renderNodes(nodes) : A}
21608
22984
  </div>
22985
+ ${this.search.renderField()}
21609
22986
  `;
21610
22987
  }
21611
22988
  #onDblClick;
@@ -21617,7 +22994,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21617
22994
  `;
21618
22995
  return b`
21619
22996
  <ul class="tree-list" role="tree">
21620
- ${this.#weaveLeaving(nodes).map(({ node, depth }) => this._renderNode(node, depth))}
22997
+ ${repeat(this.#weaveLeaving(nodes), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
21621
22998
  </ul>
21622
22999
  `;
21623
23000
  }
@@ -21627,7 +23004,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21627
23004
  const isBranch = node.item.isLeaf !== true;
21628
23005
  const isExpanded = this._isExpanded(node, depth);
21629
23006
  const isPending = this.#pendingNode === node;
21630
- const isFiltered = this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
23007
+ const isFiltered = this._isFilteredOut(node, depth);
21631
23008
  const isLeaving = this._isLeaving(node);
21632
23009
  return b`
21633
23010
  <li
@@ -21662,6 +23039,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
21662
23039
  themeFallbacks,
21663
23040
  tickledStyles,
21664
23041
  tickledItemStyles,
23042
+ listSearchStyles,
21665
23043
  i$3`
21666
23044
  :host {
21667
23045
  --tree-bg: var(--surface-3);
@@ -21672,16 +23050,23 @@ let TreeBaseElement = class TreeBaseElement extends i {
21672
23050
  --tree-caret-size: 10px;
21673
23051
  --tree-radius: var(--radius-s);
21674
23052
 
23053
+ position: relative;
21675
23054
  display: block;
21676
- overflow: auto;
23055
+ overflow: hidden;
21677
23056
  box-sizing: border-box;
21678
23057
  background: var(--tree-bg);
21679
23058
  color: var(--tree-text);
21680
23059
  }
21681
23060
 
23061
+ /* min-height (not height) so the tree still grows to its content when the
23062
+ host has no explicit height; when it does, this fills it and scrolls.
23063
+ The scroll lives here, not on :host, so the search popover (a sibling of
23064
+ #container, positioned against :host) stays put while the list scrolls. */
21682
23065
  #container {
21683
23066
  outline: none;
21684
23067
  min-height: 100%;
23068
+ max-height: 100%;
23069
+ overflow: auto;
21685
23070
  }
21686
23071
 
21687
23072
  .tree-list {
@@ -21795,6 +23180,11 @@ __decorate([property({
21795
23180
  attribute: `disable-keyboard-navigation`
21796
23181
  })], TreeBaseElement.prototype, "disableKeyboardNavigation", void 0);
21797
23182
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "registry", void 0);
23183
+ __decorate([property({
23184
+ type: String,
23185
+ attribute: `search-modes`,
23186
+ reflect: true
23187
+ })], TreeBaseElement.prototype, "searchModes", void 0);
21798
23188
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
21799
23189
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
21800
23190
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
@@ -21875,9 +23265,10 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
21875
23265
  const isPending = this._pendingNode === node;
21876
23266
  const hasIconAtDepth = this.#depthsWithIcons.has(depth);
21877
23267
  const isLeaving = this._isLeaving(node);
23268
+ const isFiltered = this._isFilteredOut(node, depth);
21878
23269
  return b`
21879
23270
  <li
21880
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``}"
23271
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${isFiltered ? `filtered` : ``}"
21881
23272
  data-key=${node.item.key}
21882
23273
  role="treeitem"
21883
23274
  aria-selected=${selected}
@@ -22146,13 +23537,11 @@ function createTreeListAdapter(toTreeItem, options) {
22146
23537
  //#region src/vertical-list/vertical-list.ts
22147
23538
  const ITEM_SELECTOR = `li, [data-list-item]`;
22148
23539
  let VerticalListElement = class VerticalListElement extends i {
22149
- constructor(..._args2) {
22150
- super(..._args2);
23540
+ constructor(..._args) {
23541
+ super(..._args);
22151
23542
  this.selectionMode = `single`;
22152
23543
  this.interactionMode = `standard`;
22153
23544
  this.itemWrap = `stop`;
22154
- this._searchVisible = false;
22155
- this._searchQuery = ``;
22156
23545
  this.#customWrappers = /* @__PURE__ */ new Map();
22157
23546
  this.#wrappedElements = /* @__PURE__ */ new Set();
22158
23547
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
@@ -22184,6 +23573,11 @@ let VerticalListElement = class VerticalListElement extends i {
22184
23573
  onStateChange: () => this._syncItemStates(),
22185
23574
  itemWrap: this.itemWrap
22186
23575
  });
23576
+ this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
23577
+ itemSelector: ITEM_SELECTOR,
23578
+ getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
23579
+ highlightKey: `vertical-list-search`
23580
+ }) });
22187
23581
  this._onPointerOver = (e) => {
22188
23582
  const item = this._findListItem(e.composedPath());
22189
23583
  if (item) this.tickled.handlePointerEnter(item);
@@ -22192,11 +23586,7 @@ let VerticalListElement = class VerticalListElement extends i {
22192
23586
  this.tickled.handlePointerLeave(e);
22193
23587
  };
22194
23588
  this._onKeyDown = (e) => {
22195
- if ((e.ctrlKey || e.metaKey) && e.key === `f`) {
22196
- e.preventDefault();
22197
- this._openSearch();
22198
- return;
22199
- }
23589
+ if (this.search.isOpen) return;
22200
23590
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
22201
23591
  if (this.selection.canSelectAll()) {
22202
23592
  e.preventDefault();
@@ -22204,7 +23594,6 @@ let VerticalListElement = class VerticalListElement extends i {
22204
23594
  }
22205
23595
  return;
22206
23596
  }
22207
- if (this._searchVisible) return;
22208
23597
  switch (e.key) {
22209
23598
  case `ArrowDown`:
22210
23599
  e.preventDefault();
@@ -22252,40 +23641,31 @@ let VerticalListElement = class VerticalListElement extends i {
22252
23641
  const item = this._findListItem(e.composedPath());
22253
23642
  if (item) this._activateItem(item);
22254
23643
  };
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
23644
  }
22282
- #search;
22283
23645
  #customWrappers;
22284
23646
  #wrappedElements;
22285
23647
  /** Watches for items added to the light DOM so they can be animated in. */
22286
23648
  #itemObserver;
22287
23649
  /** Items that have already played (or been skipped for) their enter animation. */
22288
23650
  #animatedItems;
23651
+ #searchRegistry;
23652
+ #unregisterSearchCommands;
23653
+ /** Command registry carrying this list's `list.search.*` commands. */
23654
+ get registry() {
23655
+ return this.#searchRegistry;
23656
+ }
23657
+ #effectiveSearchModes() {
23658
+ const raw = this.searchModes?.trim();
23659
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
23660
+ const requested = new Set(raw.split(/\s+/));
23661
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
23662
+ }
23663
+ #syncSearchCommands() {
23664
+ if (!this.#searchRegistry) return;
23665
+ this.search.setModes(this.#effectiveSearchModes());
23666
+ this.#unregisterSearchCommands?.();
23667
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
23668
+ }
22289
23669
  connectedCallback() {
22290
23670
  super.connectedCallback();
22291
23671
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -22297,6 +23677,8 @@ let VerticalListElement = class VerticalListElement extends i {
22297
23677
  for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
22298
23678
  this.#itemObserver ??= new MutationObserver((records) => this.#onItemsMutated(records));
22299
23679
  this.#itemObserver.observe(this, { childList: true });
23680
+ this.#searchRegistry ??= createCommandRegistry();
23681
+ this.#syncSearchCommands();
22300
23682
  }
22301
23683
  disconnectedCallback() {
22302
23684
  super.disconnectedCallback();
@@ -22306,8 +23688,9 @@ let VerticalListElement = class VerticalListElement extends i {
22306
23688
  this.removeEventListener(`click`, this._onClick);
22307
23689
  this.removeEventListener(`dblclick`, this._onDblClick);
22308
23690
  this.#itemObserver?.disconnect();
22309
- this.#search?.dispose();
22310
- this.#search = void 0;
23691
+ this.#unregisterSearchCommands?.();
23692
+ this.#unregisterSearchCommands = void 0;
23693
+ this.#searchRegistry = void 0;
22311
23694
  }
22312
23695
  /**
22313
23696
  * Animate in items appended to the light DOM (however they got there —
@@ -22330,17 +23713,9 @@ let VerticalListElement = class VerticalListElement extends i {
22330
23713
  if (fresh.length >= total) return;
22331
23714
  for (const el of fresh) animateListItemIn(el);
22332
23715
  }
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
23716
  updated(changed) {
22343
23717
  super.updated(changed);
23718
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
22344
23719
  if (changed.has(`interactionMode`)) {
22345
23720
  this.selection.clearAnchor();
22346
23721
  const previousSelected = this.selection.selected;
@@ -22408,11 +23783,13 @@ let VerticalListElement = class VerticalListElement extends i {
22408
23783
  if (this.interactionMode === `checked`) this._syncCheckboxes();
22409
23784
  }
22410
23785
  /**
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.
23786
+ * Dispatch a command to this list used by `ixfx-grouped-item-lister` to
23787
+ * forward commands to all sub-components. Routes through the command registry
23788
+ * (so `list.search.*` and any other registered commands work).
22414
23789
  */
22415
- dispatch(_commandId, _args) {}
23790
+ dispatch(commandId, args) {
23791
+ this.#searchRegistry?.invoke(commandId, args);
23792
+ }
22416
23793
  select(item) {
22417
23794
  this.selection.replace(item);
22418
23795
  }
@@ -22622,22 +23999,15 @@ let VerticalListElement = class VerticalListElement extends i {
22622
23999
  composed: true
22623
24000
  }));
22624
24001
  }
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
24002
  _activateItem(item) {
22636
24003
  if (this.interactionMode !== `manual`) this.selection.replace(item);
22637
24004
  this._dispatch(`list-activate`, { item });
22638
24005
  }
22639
24006
  static {
22640
- this.styles = [themeFallbacks, i$3`
24007
+ this.styles = [
24008
+ themeFallbacks,
24009
+ listSearchStyles,
24010
+ i$3`
22641
24011
  :host {
22642
24012
  display: block;
22643
24013
  position: relative;
@@ -22688,6 +24058,10 @@ let VerticalListElement = class VerticalListElement extends i {
22688
24058
  display: flex;
22689
24059
  align-items: center;
22690
24060
  padding: var(--ixfx-list-item-padding);
24061
+ /* Inset each row from the scroll container's edges (margin, not padding:
24062
+ padding is inside the border box, so the tickle/selection outlines
24063
+ would still sit flush against the edge and be clipped). */
24064
+ margin: 0 var(--list-gutter, 4px);
22691
24065
  min-height: var(--list-item-height, 28px);
22692
24066
  cursor: pointer;
22693
24067
  user-select: none;
@@ -22700,6 +24074,7 @@ let VerticalListElement = class VerticalListElement extends i {
22700
24074
  /* Custom shadow-DOM components handle their own internal layout/padding */
22701
24075
  ::slotted([data-list-item]) {
22702
24076
  display: block;
24077
+ margin: 0 var(--list-gutter, 4px);
22703
24078
  cursor: pointer;
22704
24079
  user-select: none;
22705
24080
  transition: background var(--transition, 0.1s ease-out),
@@ -22755,49 +24130,8 @@ let VerticalListElement = class VerticalListElement extends i {
22755
24130
  color: inherit;
22756
24131
  outline: var(--item-border-tickled);
22757
24132
  }
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
- `];
24133
+ `
24134
+ ];
22801
24135
  }
22802
24136
  render() {
22803
24137
  return b`
@@ -22805,21 +24139,7 @@ let VerticalListElement = class VerticalListElement extends i {
22805
24139
  <ul part="list">
22806
24140
  <slot></slot>
22807
24141
  </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>
24142
+ ${this.search.renderField()}
22823
24143
  </div>
22824
24144
  `;
22825
24145
  }
@@ -22838,9 +24158,11 @@ __decorate([property({
22838
24158
  attribute: `item-wrap`
22839
24159
  })], VerticalListElement.prototype, "itemWrap", void 0);
22840
24160
  __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);
24161
+ __decorate([property({
24162
+ type: String,
24163
+ attribute: `search-modes`,
24164
+ reflect: true
24165
+ })], VerticalListElement.prototype, "searchModes", void 0);
22844
24166
  VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
22845
24167
  //#endregion
22846
24168
  //#region src/grouped-item-lister/vertical-list-adapter.ts
@@ -27255,15 +28577,62 @@ let LedElement = class LedElement extends i {
27255
28577
  this.value = 0;
27256
28578
  this.lit = false;
27257
28579
  this.color = `oklch(50% 0.15 25)`;
28580
+ this.glow = false;
28581
+ this.mode = `brightness`;
27258
28582
  this.blinking = false;
27259
28583
  this.rate = 500;
28584
+ this.attackTime = 40;
28585
+ this.releaseTime = 400;
28586
+ this.pulseRetrigger = false;
28587
+ this.#appliedMode = `brightness`;
28588
+ this.#pulseStart = 0;
28589
+ this.#pulsePeak = 0;
28590
+ this.#pulseFrom = 0;
28591
+ this.#pulseLevel = 0;
28592
+ this.#pulseTick = (now) => {
28593
+ const attack = Math.max(0, this.attackTime);
28594
+ const release = Math.max(0, this.releaseTime);
28595
+ const from = this.#pulseFrom;
28596
+ const peak = this.#pulsePeak;
28597
+ const elapsed = now - this.#pulseStart;
28598
+ let level;
28599
+ if (attack > 0 && elapsed < attack) level = from + (peak - from) * (elapsed / attack);
28600
+ else if (elapsed < attack + release) level = release > 0 ? peak * (1 - (elapsed - attack) / release) : 0;
28601
+ else level = 0;
28602
+ level = Math.max(0, Math.min(Math.max(from, peak), level));
28603
+ this.#pulseLevel = level;
28604
+ this.style.setProperty(`--value`, String(level));
28605
+ if (elapsed < attack + release) this.#pulseRaf = requestAnimationFrame(this.#pulseTick);
28606
+ else {
28607
+ this.#pulseRaf = void 0;
28608
+ this.#pulseLevel = 0;
28609
+ }
28610
+ };
27260
28611
  }
27261
28612
  #blinkInterval;
28613
+ #appliedMode;
28614
+ #pulseRaf;
28615
+ #pulseStart;
28616
+ #pulsePeak;
28617
+ #pulseFrom;
28618
+ #pulseLevel;
27262
28619
  willUpdate(changedProperties) {
27263
- if (changedProperties.has(`lit`)) this.value = this.lit ? 1 : 0;
27264
- if (changedProperties.has(`value`) || changedProperties.has(`lit`)) this.style.setProperty(`--value`, String(this.value));
27265
- if (changedProperties.has(`blinking`) || changedProperties.has(`rate`)) this.#updateBlinking();
28620
+ if (changedProperties.has(`blinking`)) {
28621
+ if (this.blinking) this.mode = `blink`;
28622
+ else if (this.mode === `blink`) this.mode = `brightness`;
28623
+ }
28624
+ if (this.mode !== this.#appliedMode) {
28625
+ this.#appliedMode = this.mode;
28626
+ this.#onModeChange();
28627
+ }
28628
+ if (changedProperties.has(`lit`)) {
28629
+ if (this.lit) this.value = 1;
28630
+ else if (changedProperties.get(`lit`) !== void 0) this.value = 0;
28631
+ }
28632
+ if (changedProperties.has(`color`)) this.style.setProperty(`--_color-attr`, this.color);
28633
+ if (this.mode !== `pulse` && (changedProperties.has(`value`) || changedProperties.has(`lit`))) this.style.setProperty(`--value`, String(this.value));
27266
28634
  if (changedProperties.has(`rate`)) {
28635
+ this.#updateBlinking();
27267
28636
  const transitionMs = this.rate < 300 ? this.rate * .5 : 150;
27268
28637
  this.style.setProperty(`--transition-duration`, `${transitionMs}ms`);
27269
28638
  }
@@ -27271,18 +28640,43 @@ let LedElement = class LedElement extends i {
27271
28640
  disconnectedCallback() {
27272
28641
  super.disconnectedCallback();
27273
28642
  this.#stopBlinking();
28643
+ this.#stopPulse();
27274
28644
  }
27275
- #updateBlinking() {
27276
- if (this.blinking && this.rate > 0) {
27277
- this.#stopBlinking();
27278
- this.#blinkInterval = window.setInterval(() => {
27279
- this.value = this.value === 0 ? 1 : 0;
27280
- }, this.rate);
27281
- } else {
27282
- this.#stopBlinking();
27283
- this.value = 0;
28645
+ /**
28646
+ * Pulse-mode trigger: lights the LED to {@link value} using an attack/release
28647
+ * envelope, then decays back to unlit. Calling again restarts the envelope
28648
+ * (from 0 if {@link pulseRetrigger} is set, otherwise from the current level).
28649
+ * No-op unless {@link mode} is `pulse`.
28650
+ */
28651
+ pulse() {
28652
+ if (this.mode !== `pulse`) return;
28653
+ this.#pulsePeak = this.value;
28654
+ this.#pulseFrom = this.pulseRetrigger ? 0 : this.#pulseLevel;
28655
+ this.#pulseStart = performance.now();
28656
+ if (this.#pulseRaf === void 0) this.#pulseRaf = requestAnimationFrame(this.#pulseTick);
28657
+ }
28658
+ #onModeChange() {
28659
+ this.#stopPulse();
28660
+ this.#pulseLevel = 0;
28661
+ this.blinking = this.mode === `blink`;
28662
+ this.#updateBlinking();
28663
+ this.style.setProperty(`--value`, this.mode === `pulse` ? `0` : String(this.value));
28664
+ }
28665
+ #pulseTick;
28666
+ #stopPulse() {
28667
+ if (this.#pulseRaf !== void 0) {
28668
+ cancelAnimationFrame(this.#pulseRaf);
28669
+ this.#pulseRaf = void 0;
27284
28670
  }
27285
28671
  }
28672
+ #updateBlinking() {
28673
+ const wasBlinking = this.#blinkInterval !== void 0;
28674
+ this.#stopBlinking();
28675
+ if (this.mode === `blink` && this.rate > 0) this.#blinkInterval = window.setInterval(() => {
28676
+ this.value = this.value === 0 ? 1 : 0;
28677
+ }, this.rate);
28678
+ else if (wasBlinking || this.mode === `blink`) this.value = 0;
28679
+ }
27286
28680
  #stopBlinking() {
27287
28681
  if (this.#blinkInterval !== void 0) {
27288
28682
  clearInterval(this.#blinkInterval);
@@ -27296,6 +28690,8 @@ let LedElement = class LedElement extends i {
27296
28690
  this.styles = i$3`
27297
28691
  :host {
27298
28692
  --_size: var(--size, 1ex);
28693
+ /* Resolved colour: explicit --colour wins, then the \`color\` attribute, then a default. */
28694
+ --_colour: var(--colour, var(--_color-attr, oklch(50% 0.15 25)));
27299
28695
  display: inline-block;
27300
28696
  min-width: 1ex;
27301
28697
  min-height: 1ex;
@@ -27305,12 +28701,61 @@ let LedElement = class LedElement extends i {
27305
28701
  }
27306
28702
 
27307
28703
  div {
28704
+ position: relative;
27308
28705
  width: 100%;
27309
28706
  height: 100%;
27310
28707
  border-radius: 50%;
27311
- background: color-mix(in oklch, var(--colour, oklch(50% 0.15 25)), white calc((1 - var(--value, 0)) * 50%));
28708
+ background: color-mix(in oklch, var(--_colour), white calc((1 - var(--value, 0)) * 50%));
27312
28709
  transition: background var(--transition-duration, 150ms) ease-out;
27313
28710
  }
28711
+
28712
+ /* Glow: a more realistic domed LED with a specular highlight and coloured bloom. */
28713
+ :host([glow]) {
28714
+ /* Body darkens toward black as the LED dims, so an unlit LED reads as a dark dome. */
28715
+ --_body: color-mix(in oklch, var(--_colour), black calc((1 - var(--value, 0)) * 60%));
28716
+ overflow: visible;
28717
+ }
28718
+
28719
+ :host([glow]) div {
28720
+ background:
28721
+ radial-gradient(circle at 32% 28%,
28722
+ color-mix(in oklch, var(--_body), white 55%) 0%,
28723
+ var(--_body) 45%,
28724
+ color-mix(in oklch, var(--_body), black 32%) 100%);
28725
+ box-shadow:
28726
+ /* inner rim shading for depth */
28727
+ inset 0 calc(var(--_size) * -0.1) calc(var(--_size) * 0.18)
28728
+ color-mix(in oklch, var(--_body), black 45%),
28729
+ inset 0 calc(var(--_size) * 0.08) calc(var(--_size) * 0.14)
28730
+ color-mix(in oklch, var(--_body), white 25%),
28731
+ /* coloured bloom, scaled by value so it only glows when lit */
28732
+ 0 0 calc(var(--_size) * 0.6 * var(--value, 0)) calc(var(--_size) * 0.12 * var(--value, 0))
28733
+ var(--_colour),
28734
+ 0 0 calc(var(--_size) * 1.7 * var(--value, 0)) calc(var(--_size) * 0.5 * var(--value, 0))
28735
+ color-mix(in oklch, var(--_colour), transparent 45%);
28736
+ transition:
28737
+ background var(--transition-duration, 150ms) ease-out,
28738
+ box-shadow var(--transition-duration, 150ms) ease-out;
28739
+ }
28740
+
28741
+ /* Specular highlight blob near the top-left. */
28742
+ :host([glow]) div::after {
28743
+ content: "";
28744
+ position: absolute;
28745
+ inset: 0;
28746
+ border-radius: 50%;
28747
+ background: radial-gradient(ellipse 55% 45% at 30% 22%,
28748
+ color-mix(in oklch, white, transparent 10%) 0%,
28749
+ transparent 60%);
28750
+ pointer-events: none;
28751
+ }
28752
+
28753
+ /* Pulse mode animates --value frame-by-frame, so drop the CSS transition
28754
+ that would otherwise smear the envelope. Placed last to win over the
28755
+ glow rule for elements matching both. */
28756
+ :host([mode="pulse"]) div {
28757
+ transition: none;
28758
+ }
27314
28759
  `;
27315
28760
  }
27316
28761
  };
@@ -27321,6 +28766,15 @@ __decorate([property({
27321
28766
  reflect: true
27322
28767
  })], LedElement.prototype, "lit", void 0);
27323
28768
  __decorate([property({ type: String })], LedElement.prototype, "color", void 0);
28769
+ __decorate([property({
28770
+ type: Boolean,
28771
+ attribute: `glow`,
28772
+ reflect: true
28773
+ })], LedElement.prototype, "glow", void 0);
28774
+ __decorate([property({
28775
+ type: String,
28776
+ reflect: true
28777
+ })], LedElement.prototype, "mode", void 0);
27324
28778
  __decorate([property({
27325
28779
  type: Boolean,
27326
28780
  attribute: `blinking`,
@@ -27330,6 +28784,18 @@ __decorate([property({
27330
28784
  type: Number,
27331
28785
  attribute: `rate`
27332
28786
  })], LedElement.prototype, "rate", void 0);
28787
+ __decorate([property({
28788
+ type: Number,
28789
+ attribute: `attack-time`
28790
+ })], LedElement.prototype, "attackTime", void 0);
28791
+ __decorate([property({
28792
+ type: Number,
28793
+ attribute: `release-time`
28794
+ })], LedElement.prototype, "releaseTime", void 0);
28795
+ __decorate([property({
28796
+ type: Boolean,
28797
+ attribute: `pulse-retrigger`
28798
+ })], LedElement.prototype, "pulseRetrigger", void 0);
27333
28799
  LedElement = __decorate([safeCustomElement(`ixfx-led`)], LedElement);
27334
28800
  //#endregion
27335
28801
  //#region src/miller/miller-base.ts
@@ -27338,6 +28804,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27338
28804
  constructor(..._args) {
27339
28805
  super(..._args);
27340
28806
  this.headers = false;
28807
+ this.stretchLastColumn = false;
27341
28808
  this.interactionMode = `standard`;
27342
28809
  this.selectionFilter = `leaf`;
27343
28810
  this.exclusivity = `none`;
@@ -27349,6 +28816,10 @@ let MillerBaseElement = class MillerBaseElement extends i {
27349
28816
  this.#resizingDepth = -1;
27350
28817
  this.#resizeStartX = 0;
27351
28818
  this.#resizeStartWidth = 0;
28819
+ this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
28820
+ labelSelector: `.label`,
28821
+ highlightKey: `incr-search-highlight`
28822
+ }) });
27352
28823
  this.tickled = new TickledController(this, { getItems: () => {
27353
28824
  const items = [];
27354
28825
  for (let depth = 0; depth < this._columns.length; depth++) {
@@ -27358,6 +28829,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27358
28829
  return items;
27359
28830
  } });
27360
28831
  this.#onKeyDown = (event) => {
28832
+ if (this.search.isOpen) return;
27361
28833
  const currentDepth = this.#getTickledDepthOrDefault();
27362
28834
  const col = this._columns[currentDepth];
27363
28835
  if (!col || col.length === 0) {
@@ -27564,9 +29036,31 @@ let MillerBaseElement = class MillerBaseElement extends i {
27564
29036
  #resizeStartX;
27565
29037
  #resizeStartWidth;
27566
29038
  #modelUnsubscribe;
29039
+ #searchRegistry;
29040
+ #unregisterSearchCommands;
29041
+ /** Command registry carrying this component's `list.search.*` commands. */
29042
+ get registry() {
29043
+ return this.#searchRegistry;
29044
+ }
29045
+ #effectiveSearchModes() {
29046
+ const raw = this.searchModes?.trim();
29047
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
29048
+ const requested = new Set(raw.split(/\s+/));
29049
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
29050
+ }
29051
+ #syncSearchCommands() {
29052
+ if (!this.#searchRegistry) return;
29053
+ this.search.setModes(this.#effectiveSearchModes());
29054
+ this.#unregisterSearchCommands?.();
29055
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
29056
+ }
27567
29057
  #buildTickledId(node, depth) {
27568
29058
  return `${depth}:${node.item.key}`;
27569
29059
  }
29060
+ /** The node column 0 renders the children of: `viewRoot` when rebased, else `root`. */
29061
+ get #displayRoot() {
29062
+ return this.viewRoot ?? this.root;
29063
+ }
27570
29064
  #getTickledDepth() {
27571
29065
  const id = this.tickled.tickledId;
27572
29066
  if (!id) return void 0;
@@ -27575,16 +29069,20 @@ let MillerBaseElement = class MillerBaseElement extends i {
27575
29069
  return Number(id.substring(0, colonIndex));
27576
29070
  }
27577
29071
  updated(changedProps) {
29072
+ if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
27578
29073
  if (changedProps.has(`root`) && this.root) {
27579
29074
  this.#abortPending();
27580
- this._columns = this.root.children ? [this.root.children] : [];
29075
+ if (this.viewRoot !== void 0) this.viewRoot = void 0;
27581
29076
  this._selectedByDepth.clear();
27582
29077
  this._selectedKeys = /* @__PURE__ */ new Set();
27583
29078
  this._selectedNodes.clear();
27584
29079
  this.#lastSelectedNode = void 0;
27585
29080
  this.#anchor = void 0;
27586
29081
  this.tickled.clearTickled();
29082
+ const display = this.#displayRoot;
29083
+ this._columns = display?.children ? [display.children] : [];
27587
29084
  }
29085
+ if (changedProps.has(`viewRoot`)) this.#applyViewRoot();
27588
29086
  if (changedProps.has(`model`)) {
27589
29087
  this.#modelUnsubscribe?.();
27590
29088
  this.#modelUnsubscribe = void 0;
@@ -27626,10 +29124,18 @@ let MillerBaseElement = class MillerBaseElement extends i {
27626
29124
  });
27627
29125
  }
27628
29126
  }
29127
+ connectedCallback() {
29128
+ super.connectedCallback();
29129
+ this.#searchRegistry ??= createCommandRegistry();
29130
+ this.#syncSearchCommands();
29131
+ }
27629
29132
  disconnectedCallback() {
27630
29133
  super.disconnectedCallback();
27631
29134
  this.#modelUnsubscribe?.();
27632
29135
  this.#modelUnsubscribe = void 0;
29136
+ this.#unregisterSearchCommands?.();
29137
+ this.#unregisterSearchCommands = void 0;
29138
+ this.#searchRegistry = void 0;
27633
29139
  }
27634
29140
  getConfig() {
27635
29141
  const columnWidths = {};
@@ -27725,15 +29231,17 @@ let MillerBaseElement = class MillerBaseElement extends i {
27725
29231
  return results;
27726
29232
  }
27727
29233
  /**
27728
- * Programmatically navigate to a node by key path.
29234
+ * Programmatically navigate to a node by key path, relative to the current
29235
+ * view root (rebased or full tree).
27729
29236
  */
27730
29237
  navigateTo(keys) {
27731
- if (!this.root?.children) return false;
29238
+ const display = this.#displayRoot;
29239
+ if (!display?.children) return false;
27732
29240
  this.#abortPending();
27733
29241
  this._selectedByDepth.clear();
27734
29242
  this.tickled.clearTickled();
27735
- const newColumns = [this.root.children];
27736
- let currentColumn = this.root.children;
29243
+ const newColumns = [display.children];
29244
+ let currentColumn = display.children;
27737
29245
  let success = true;
27738
29246
  let lastNode;
27739
29247
  let lastDepth = 0;
@@ -27768,13 +29276,51 @@ let MillerBaseElement = class MillerBaseElement extends i {
27768
29276
  * Programmatically navigate to the first node (breadth-first) matching a predicate.
27769
29277
  */
27770
29278
  navigateToNode(predicate) {
27771
- if (!this.root?.children) return false;
27772
- const keyPath = this.#findNodePath(this.root.children, predicate);
29279
+ const display = this.#displayRoot;
29280
+ if (!display?.children) return false;
29281
+ const keyPath = this.#findNodePath(display.children, predicate);
27773
29282
  if (!keyPath) return false;
27774
29283
  return this.navigateTo(keyPath);
27775
29284
  }
29285
+ /** Open the columns down to `key` and move the cursor to it, without changing the selection (search "navigate" mode). */
29286
+ revealKey(key) {
29287
+ const display = this.#displayRoot;
29288
+ if (!display?.children) return false;
29289
+ const path = this.#findNodePath(display.children, (n) => n.item.key === key);
29290
+ if (!path) return false;
29291
+ const columns = [display.children];
29292
+ let current = display.children;
29293
+ let lastNode;
29294
+ let lastDepth = 0;
29295
+ for (let depth = 0; depth < path.length; depth++) {
29296
+ const node = current.find((n) => n.item.key === path[depth]);
29297
+ if (!node) return false;
29298
+ lastNode = node;
29299
+ lastDepth = depth;
29300
+ if (node.children && node.children.length > 0) {
29301
+ columns.push(node.children);
29302
+ current = node.children;
29303
+ } else break;
29304
+ }
29305
+ this._columns = columns;
29306
+ if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
29307
+ this.requestUpdate();
29308
+ this.#scrollNavigatedItemIntoView();
29309
+ return true;
29310
+ }
29311
+ /** Replace the selection with `nodes` (search "select" mode). */
29312
+ selectMany(nodes) {
29313
+ const arr = this.selectionFilter === `none` ? [] : [...nodes];
29314
+ const prev = new Set(this._selectedNodes);
29315
+ this._selectedNodes = new Set(arr);
29316
+ this._selectedKeys = new Set(arr.map((n) => n.item.key));
29317
+ this.#lastSelectedNode = arr.at(-1);
29318
+ this.#dispatchSelectEvent(prev);
29319
+ this.requestUpdate();
29320
+ }
27776
29321
  getNodes() {
27777
- if (!this.root?.children) return [];
29322
+ const display = this.#displayRoot;
29323
+ if (!display?.children) return [];
27778
29324
  const result = [];
27779
29325
  const visit = (node, depth) => {
27780
29326
  result.push({
@@ -27783,7 +29329,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27783
29329
  });
27784
29330
  if (node.children) for (const child of node.children) visit(child, depth + 1);
27785
29331
  };
27786
- for (const child of this.root.children) visit(child, 0);
29332
+ for (const child of display.children) visit(child, 0);
27787
29333
  return result;
27788
29334
  }
27789
29335
  #findNodePath(nodes, predicate, prefix = []) {
@@ -27796,6 +29342,124 @@ let MillerBaseElement = class MillerBaseElement extends i {
27796
29342
  }
27797
29343
  }
27798
29344
  }
29345
+ /**
29346
+ * Re-render the columns for the current `#displayRoot`, clearing selection.
29347
+ * Runs on every `viewRoot` change.
29348
+ */
29349
+ #applyViewRoot() {
29350
+ this.#abortPending();
29351
+ const prev = new Set(this._selectedNodes);
29352
+ this._selectedByDepth.clear();
29353
+ this._selectedKeys = /* @__PURE__ */ new Set();
29354
+ this._selectedNodes.clear();
29355
+ this.#lastSelectedNode = void 0;
29356
+ this.#anchor = void 0;
29357
+ this.tickled.clearTickled();
29358
+ const display = this.#displayRoot;
29359
+ if (display?.children !== void 0) {
29360
+ this._columns = display.children ? [display.children] : [];
29361
+ this._loadingDepth = void 0;
29362
+ } else if (display) {
29363
+ this._columns = [];
29364
+ this.#loadUnloadedChildren(display, -1, false);
29365
+ } else {
29366
+ this._columns = [];
29367
+ this._loadingDepth = void 0;
29368
+ }
29369
+ if (prev.size > 0) this.#dispatchSelectEvent(prev);
29370
+ }
29371
+ /**
29372
+ * Rebase the view to show `target`'s children in column 0 (Finder column-view
29373
+ * behavior on double-click), keeping the full tree in `root`.
29374
+ *
29375
+ * - Pass a `MillerNode` (used as-is) or a key (looked up in the full tree).
29376
+ * - Pass `undefined` to restore the full-tree view.
29377
+ * - Rebasing to the true root is equivalent to restoring the full view.
29378
+ * - Leaves are refused (returns `false`).
29379
+ *
29380
+ * Selection is cleared; all depths, headers, and key paths are relative to
29381
+ * the new view root. Fires a `rebase` event when the view actually changes.
29382
+ * Returns `false` when nothing changed (unknown key, leaf, or same view).
29383
+ */
29384
+ rebaseTo(target) {
29385
+ if (target === void 0) return this.#clearViewRoot();
29386
+ const node = typeof target === `string` ? this.#findNodeByKey(target) : target;
29387
+ if (!node || node.item.isLeaf === true) return false;
29388
+ if (node === this.viewRoot || node.item.key === this.viewRoot?.item.key) return false;
29389
+ if (node === this.root || !this.viewRoot && node.item.key === this.root?.item.key) return this.#clearViewRoot();
29390
+ this.viewRoot = node;
29391
+ this.#dispatchRebaseEvent();
29392
+ return true;
29393
+ }
29394
+ /**
29395
+ * Rebase one level up from the current view root (no-op at the full tree).
29396
+ * Fires a `rebase` event when the view actually changes.
29397
+ */
29398
+ rebaseUp() {
29399
+ if (!this.viewRoot) return false;
29400
+ const parent = this.#findParentOf(this.viewRoot);
29401
+ if (!parent) return false;
29402
+ if (parent === this.root) return this.#clearViewRoot();
29403
+ this.viewRoot = parent;
29404
+ this.#dispatchRebaseEvent();
29405
+ return true;
29406
+ }
29407
+ #clearViewRoot() {
29408
+ if (this.viewRoot === void 0) return false;
29409
+ this.viewRoot = void 0;
29410
+ this.#dispatchRebaseEvent();
29411
+ return true;
29412
+ }
29413
+ #dispatchRebaseEvent() {
29414
+ const viewRoot = this.viewRoot;
29415
+ const detail = {
29416
+ viewRoot,
29417
+ path: viewRoot ? this.#pathToRoot(viewRoot) : []
29418
+ };
29419
+ this.dispatchEvent(new CustomEvent(`rebase`, {
29420
+ detail,
29421
+ bubbles: true,
29422
+ composed: true
29423
+ }));
29424
+ }
29425
+ #findNodeByKey(key) {
29426
+ if (!this.root) return void 0;
29427
+ if (this.root.item.key === key) return this.root;
29428
+ return this.#findNodeIn(this.root.children ?? [], key);
29429
+ }
29430
+ #findNodeIn(nodes, key) {
29431
+ for (const node of nodes) {
29432
+ if (node.item.key === key) return node;
29433
+ if (node.children) {
29434
+ const found = this.#findNodeIn(node.children, key);
29435
+ if (found) return found;
29436
+ }
29437
+ }
29438
+ }
29439
+ /** Find the parent of `target` (by reference, falling back to key) in the full tree. */
29440
+ #findParentOf(target) {
29441
+ if (!this.root || target === this.root) return void 0;
29442
+ return this.#findParentIn(this.root, target);
29443
+ }
29444
+ #findParentIn(node, target) {
29445
+ if (!node.children) return void 0;
29446
+ for (const child of node.children) {
29447
+ if (child === target || child.item.key === target.item.key) return node;
29448
+ const found = this.#findParentIn(child, target);
29449
+ if (found) return found;
29450
+ }
29451
+ }
29452
+ /** Ancestry chain from the true root down to and including `target`. */
29453
+ #pathToRoot(target) {
29454
+ const chain = [target];
29455
+ let current = this.#findParentOf(target);
29456
+ while (current) {
29457
+ chain.unshift(current);
29458
+ if (current === this.root) break;
29459
+ current = this.#findParentOf(current);
29460
+ }
29461
+ return chain;
29462
+ }
27799
29463
  renderColumnContent(nodes, depth) {
27800
29464
  console.warn(`ixfx-miller-base: renderColumnContent not overridden. depth=${depth}, nodes=${nodes.length}`);
27801
29465
  return A;
@@ -27922,7 +29586,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
27922
29586
  */
27923
29587
  #buildPath(depth) {
27924
29588
  const path = [];
27925
- if (this.root) path.push(this.root);
29589
+ if (this.#displayRoot) path.push(this.#displayRoot);
27926
29590
  for (let d = 0; d < depth; d++) {
27927
29591
  const sel = this._selectedByDepth.get(d);
27928
29592
  if (sel) path.push(sel);
@@ -28025,57 +29689,8 @@ let MillerBaseElement = class MillerBaseElement extends i {
28025
29689
  this.requestUpdate();
28026
29690
  return;
28027
29691
  }
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 {
29692
+ if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true);
29693
+ else {
28079
29694
  const newColumns = this._columns.slice(0, depth + 1);
28080
29695
  newColumns.push(node.children);
28081
29696
  this._columns = newColumns;
@@ -28089,6 +29704,68 @@ let MillerBaseElement = class MillerBaseElement extends i {
28089
29704
  }));
28090
29705
  }
28091
29706
  }
29707
+ /**
29708
+ * Fetch children for a node whose `children === undefined` and show them as
29709
+ * the column after `parentDepth` (with the loading placeholder meanwhile).
29710
+ * Shared by branch expansion (`parentDepth` = the branch's column) and view
29711
+ * rebasing (`parentDepth` = -1, so the children become column 0).
29712
+ *
29713
+ * The `depth` reported to `loadChildren`/`model.getChildren` is the node's
29714
+ * own depth: its column for expansions, -1 for a view root (it sits above
29715
+ * column 0, so loaders computing child depth as `depth + 1` stay correct).
29716
+ */
29717
+ #loadUnloadedChildren(node, parentDepth, fireExpandEvent) {
29718
+ const nodeDepth = parentDepth + 1;
29719
+ this._columns = this._columns.slice(0, nodeDepth);
29720
+ this._loadingDepth = nodeDepth;
29721
+ this.#pendingNode = node;
29722
+ const controller = new AbortController();
29723
+ this.#pendingController = controller;
29724
+ if (fireExpandEvent) this.dispatchEvent(new CustomEvent(`expand`, {
29725
+ detail: {
29726
+ node,
29727
+ depth: parentDepth
29728
+ },
29729
+ bubbles: true,
29730
+ composed: true
29731
+ }));
29732
+ const applyChildren = (children) => {
29733
+ if (controller.signal.aborted) return;
29734
+ node.children = children;
29735
+ this.#pendingNode = void 0;
29736
+ this.#pendingController = void 0;
29737
+ this._loadingDepth = void 0;
29738
+ const cols = this._columns.slice(0, nodeDepth);
29739
+ if (children.length > 0) cols.push(children);
29740
+ this._columns = cols;
29741
+ };
29742
+ const handleError = (err) => {
29743
+ if (err.name !== `AbortError`) console.error(err);
29744
+ node.children = [];
29745
+ this.#pendingNode = void 0;
29746
+ this.#pendingController = void 0;
29747
+ this._loadingDepth = void 0;
29748
+ this.requestUpdate();
29749
+ };
29750
+ if (this.model || this.loadChildren) {
29751
+ if (this.#expandTimer !== void 0) clearTimeout(this.#expandTimer);
29752
+ this.#expandTimer = setTimeout(() => {
29753
+ this.#expandTimer = void 0;
29754
+ if (this.#pendingNode !== node) return;
29755
+ (this.model ? this.model.getChildren(node, nodeDepth - 1, controller.signal) : collectDataResult(this.loadChildren({
29756
+ node,
29757
+ depth: nodeDepth - 1
29758
+ }, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
29759
+ }, EXPAND_DEBOUNCE_MS);
29760
+ } else {
29761
+ this._loadingDepth = void 0;
29762
+ this.#pendingNode = void 0;
29763
+ this.#pendingController = void 0;
29764
+ const newColumns = this._columns.slice(0, nodeDepth);
29765
+ newColumns.push([]);
29766
+ this._columns = newColumns;
29767
+ }
29768
+ }
28092
29769
  #onCheckboxClick(node) {
28093
29770
  const currentlySelected = this._selectedKeys.has(node.item.key);
28094
29771
  if (!!node.children?.length ? this.#allDescendantsSelected(node) : currentlySelected) this.#applyCheckedToggle(node, false);
@@ -28158,7 +29835,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
28158
29835
  #onResizeMove;
28159
29836
  #onResizeEnd;
28160
29837
  #getColumnHeader(depth) {
28161
- if (depth === 0) return this.root?.item.label ?? ``;
29838
+ if (depth === 0) return this.#displayRoot?.item.label ?? ``;
28162
29839
  const explicit = this._selectedByDepth.get(depth - 1);
28163
29840
  if (explicit) return explicit.item.label;
28164
29841
  const prevCol = this._columns[depth - 1];
@@ -28169,9 +29846,11 @@ let MillerBaseElement = class MillerBaseElement extends i {
28169
29846
  }
28170
29847
  return ``;
28171
29848
  }
28172
- #columnStyle(depth) {
29849
+ #columnStyle(depth, isLast) {
28173
29850
  const w = this.#columnWidths.get(depth);
28174
- return w !== void 0 ? `width:${w}px;flex-shrink:0` : `width:max-content`;
29851
+ if (w !== void 0) return `width:${w}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
29852
+ if (this.stretchLastColumn && isLast) return `width:max-content;flex-grow:1;flex-shrink:0;flex-basis:auto;min-width:0`;
29853
+ return `width:max-content;flex-grow:0;flex-shrink:0;flex-basis:auto`;
28175
29854
  }
28176
29855
  #isFocused(node, depth) {
28177
29856
  return this.tickled.isTickled(this.#buildTickledId(node, depth));
@@ -28184,8 +29863,9 @@ let MillerBaseElement = class MillerBaseElement extends i {
28184
29863
  }
28185
29864
  #renderLoadingColumn() {
28186
29865
  const depth = this._loadingDepth;
29866
+ const stretched = this.stretchLastColumn && this.#columnWidths.get(depth) === void 0;
28187
29867
  return b`
28188
- <div class="column loading-column" style=${this.#columnStyle(depth)}>
29868
+ <div class="column loading-column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, true)}>
28189
29869
  ${this.headers ? b`
28190
29870
  <div class="column-header">${this.#getColumnHeader(depth)}</div>
28191
29871
  ` : A}
@@ -28200,8 +29880,10 @@ let MillerBaseElement = class MillerBaseElement extends i {
28200
29880
  <div id="container" @keydown=${this.#onKeyDown} tabindex="0">
28201
29881
  ${this._columns.map((nodes, depth) => {
28202
29882
  const visibleNodes = this.filterPredicate ? nodes.filter((n) => this.filterPredicate(n, depth)) : nodes;
29883
+ const isLast = depth === this._columns.length - 1 && this._loadingDepth === void 0;
29884
+ const stretched = this.stretchLastColumn && isLast && this.#columnWidths.get(depth) === void 0;
28203
29885
  return b`
28204
- <div class="column" style=${this.#columnStyle(depth)}>
29886
+ <div class="column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, isLast)}>
28205
29887
  ${this.headers ? b`
28206
29888
  <div class="column-header"
28207
29889
  @click=${() => this.#onHeaderClick(depth)}
@@ -28220,6 +29902,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
28220
29902
  })}
28221
29903
  ${this._loadingDepth !== void 0 ? this.#renderLoadingColumn() : A}
28222
29904
  </div>
29905
+ ${this.search.renderField()}
28223
29906
  `;
28224
29907
  }
28225
29908
  _getFocusedIndex(depth) {
@@ -28255,6 +29938,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
28255
29938
  this.styles = [
28256
29939
  themeFallbacks,
28257
29940
  tickledStyles,
29941
+ listSearchStyles,
28258
29942
  i$3`
28259
29943
  :host {
28260
29944
  --column-bg: var(--surface-3);
@@ -28289,6 +29973,20 @@ let MillerBaseElement = class MillerBaseElement extends i {
28289
29973
  background: var(--column-bg);
28290
29974
  border-right: 1px solid var(--column-separator);
28291
29975
  flex-shrink: 0;
29976
+ /* Content-sized columns never grow wider than 80% of the available
29977
+ width — pathological labels truncate with ellipsis instead of
29978
+ pushing every other column out of view */
29979
+ max-width: 80%;
29980
+ /* Smoothly animate the last column stretching to fill remaining
29981
+ space and shrinking back to content width when it stops being last */
29982
+ transition: flex-grow var(--transition, 0.2s ease-out);
29983
+ }
29984
+
29985
+ .column.stretch-last {
29986
+ min-width: 0;
29987
+ /* Stretching fills remaining space by definition, so it is exempt
29988
+ from the 80% auto-size cap */
29989
+ max-width: none;
28292
29990
  }
28293
29991
 
28294
29992
  .column-header {
@@ -28372,6 +30070,12 @@ let MillerBaseElement = class MillerBaseElement extends i {
28372
30070
  __decorate([property({ type: Object })], MillerBaseElement.prototype, "root", void 0);
28373
30071
  __decorate([property({ attribute: false })], MillerBaseElement.prototype, "model", void 0);
28374
30072
  __decorate([property({ type: Boolean })], MillerBaseElement.prototype, "headers", void 0);
30073
+ __decorate([property({
30074
+ type: Boolean,
30075
+ attribute: `stretch-last-column`,
30076
+ reflect: true
30077
+ })], MillerBaseElement.prototype, "stretchLastColumn", void 0);
30078
+ __decorate([property({ attribute: false })], MillerBaseElement.prototype, "viewRoot", void 0);
28375
30079
  __decorate([property({ type: String })], MillerBaseElement.prototype, "interactionMode", void 0);
28376
30080
  __decorate([property({
28377
30081
  reflect: true,
@@ -28383,6 +30087,11 @@ __decorate([property({
28383
30087
  })], MillerBaseElement.prototype, "exclusivity", void 0);
28384
30088
  __decorate([property({ type: Object })], MillerBaseElement.prototype, "loadChildren", void 0);
28385
30089
  __decorate([property({ attribute: false })], MillerBaseElement.prototype, "filterPredicate", void 0);
30090
+ __decorate([property({
30091
+ type: String,
30092
+ attribute: `search-modes`,
30093
+ reflect: true
30094
+ })], MillerBaseElement.prototype, "searchModes", void 0);
28386
30095
  __decorate([state()], MillerBaseElement.prototype, "_columns", void 0);
28387
30096
  __decorate([state()], MillerBaseElement.prototype, "_loadingDepth", void 0);
28388
30097
  __decorate([state()], MillerBaseElement.prototype, "_selectedNodes", void 0);
@@ -28620,14 +30329,28 @@ var MillerTreeController = class {
28620
30329
  * Any ancestor `MillerNode`s with unloaded children are populated
28621
30330
  * synchronously before calling `element.navigateTo()`.
28622
30331
  *
30332
+ * Key paths are relative to the element's current view root: when the view
30333
+ * is rebased, only matches at or below the view root can be reached.
30334
+ *
28623
30335
  * Returns `false` if no matching node is found.
28624
30336
  */
28625
30337
  navigateToValue(predicate) {
28626
30338
  const treePath = this.#findTreePath(this.#root, predicate);
28627
30339
  if (!treePath) return false;
28628
- let currentColumn = this.#element.root?.children ?? [];
30340
+ const base = this.#element.viewRoot ?? this.#element.root;
30341
+ if (!base) return false;
30342
+ let startIndex = 1;
30343
+ if (this.#element.viewRoot) {
30344
+ const mappedBase = this.#nodeToTree.get(base);
30345
+ if (!mappedBase) return false;
30346
+ const baseIndex = treePath.indexOf(mappedBase);
30347
+ if (baseIndex < 0) return false;
30348
+ startIndex = baseIndex + 1;
30349
+ if (base.children === void 0) base.children = this.#childrenOf(mappedBase);
30350
+ }
30351
+ let currentColumn = base.children ?? [];
28629
30352
  const keys = [];
28630
- for (let i = 1; i < treePath.length; i++) {
30353
+ for (let i = startIndex; i < treePath.length; i++) {
28631
30354
  const treeNode = treePath[i];
28632
30355
  const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
28633
30356
  keys.push(key);
@@ -29240,6 +30963,66 @@ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
29240
30963
  PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
29241
30964
  //#endregion
29242
30965
  //#region src/panel/panel-group.ts
30966
+ /** Pointer movement threshold (px) before a press becomes a drag. */
30967
+ const DRAG_THRESHOLD_PX = 5;
30968
+ /**
30969
+ * Pure helper: nearest gap index for a pointer position along one axis.
30970
+ * `edges` are the [start, end] of each sibling (excluding the drag source),
30971
+ * in DOM order. Returns 0..edges.length.
30972
+ */
30973
+ function panelGapIndex(edges, pos) {
30974
+ let bestGap = 0;
30975
+ let bestDist = Number.POSITIVE_INFINITY;
30976
+ for (let i = 0; i < edges.length; i++) {
30977
+ const [start, end] = edges[i];
30978
+ if (pos < (start + end) / 2) {
30979
+ const d = Math.abs(pos - start);
30980
+ if (d < bestDist) {
30981
+ bestDist = d;
30982
+ bestGap = i;
30983
+ }
30984
+ } else {
30985
+ const d = Math.abs(pos - end);
30986
+ if (d < bestDist) {
30987
+ bestDist = d;
30988
+ bestGap = i + 1;
30989
+ }
30990
+ }
30991
+ }
30992
+ return bestGap;
30993
+ }
30994
+ let panelGhostStylesInjected = false;
30995
+ /**
30996
+ * Global ghost styling so the drag preview matches `ixfx-titlebar` chrome
30997
+ * (font, colours, padding). Injected once; the ghost lives outside shadow DOM
30998
+ * (body/theme container) so component styles cannot reach it.
30999
+ */
31000
+ function injectPanelGhostStyles() {
31001
+ if (panelGhostStylesInjected) return;
31002
+ panelGhostStylesInjected = true;
31003
+ const style = document.createElement(`style`);
31004
+ style.textContent = `
31005
+ .ixfx-drag-ghost.ixfx-panel-group-ghost {
31006
+ padding: var(--space-s, 8px) var(--space-m, 16px);
31007
+ background: var(--surface-4);
31008
+ color: var(--surface-3-text);
31009
+ border: none;
31010
+ border-radius: var(--radius-s, 4px);
31011
+ font-family: var(--font-family);
31012
+ font-size: var(--text-m);
31013
+ font-weight: var(--font-weight-bold, 600);
31014
+ min-height: 2em;
31015
+ user-select: none;
31016
+ -webkit-user-select: none;
31017
+ }
31018
+ .ixfx-drag-ghost.ixfx-panel-group-ghost ixfx-icon {
31019
+ width: 1.25em;
31020
+ height: 1.25em;
31021
+ flex-shrink: 0;
31022
+ }
31023
+ `;
31024
+ document.head.appendChild(style);
31025
+ }
29243
31026
  let PanelGroupElement = class PanelGroupElement extends i {
29244
31027
  constructor(..._args) {
29245
31028
  super(..._args);
@@ -29247,22 +31030,105 @@ let PanelGroupElement = class PanelGroupElement extends i {
29247
31030
  this.variant = `titlebar`;
29248
31031
  this.toggleInteraction = `implicit`;
29249
31032
  this.collapsible = true;
29250
- this.allowMultiple = false;
29251
- this.gap = `var(--space-m)`;
31033
+ this.accordion = false;
31034
+ this.allowReorder = false;
31035
+ this._dragState = null;
31036
+ this._suppressNextClick = false;
31037
+ this._onClickCapture = (event) => {
31038
+ if (!this._suppressNextClick) return;
31039
+ this._suppressNextClick = false;
31040
+ event.preventDefault();
31041
+ event.stopPropagation();
31042
+ };
31043
+ this._onKeyDown = (event) => {
31044
+ const state = this._dragState;
31045
+ if (event.key !== `Escape` || !state?.active) return;
31046
+ event.preventDefault();
31047
+ const pointerId = state.pointerId;
31048
+ this._abortDrag();
31049
+ this._endDrag(pointerId);
31050
+ };
31051
+ this._onPointerDown = (event) => {
31052
+ if (!this.allowReorder || event.button !== 0) return;
31053
+ const target = this._findDragTarget(event);
31054
+ if (!target?.parentNode) return;
31055
+ const fromIndex = Array.prototype.indexOf.call(target.parentNode.children, target);
31056
+ if (fromIndex < 0) return;
31057
+ this._dragState = {
31058
+ pointerId: event.pointerId,
31059
+ source: target,
31060
+ fromIndex,
31061
+ startX: event.clientX,
31062
+ startY: event.clientY,
31063
+ active: false,
31064
+ ghost: null,
31065
+ gapIndex: void 0
31066
+ };
31067
+ window.addEventListener(`pointermove`, this._onPointerMove);
31068
+ window.addEventListener(`pointerup`, this._onPointerUp);
31069
+ window.addEventListener(`pointercancel`, this._onPointerCancel);
31070
+ };
31071
+ this._onPointerMove = (event) => {
31072
+ const state = this._dragState;
31073
+ if (!state || event.pointerId !== state.pointerId) return;
31074
+ const distance = Math.hypot(event.clientX - state.startX, event.clientY - state.startY);
31075
+ if (!state.active) {
31076
+ if (distance < DRAG_THRESHOLD_PX) return;
31077
+ this._beginDrag(event);
31078
+ }
31079
+ state.ghost?.move(event.clientX, event.clientY);
31080
+ const gap = this._computeGapIndex(event.clientX, event.clientY);
31081
+ if (gap !== state.gapIndex) {
31082
+ state.gapIndex = gap;
31083
+ this.requestUpdate();
31084
+ }
31085
+ };
31086
+ this._onPointerUp = (event) => {
31087
+ const state = this._dragState;
31088
+ if (!state || event.pointerId !== state.pointerId) return;
31089
+ if (state.active) {
31090
+ const gap = this._computeGapIndex(event.clientX, event.clientY);
31091
+ this._commitDrag(state.source, state.fromIndex, gap);
31092
+ }
31093
+ this._endDrag(event.pointerId);
31094
+ };
31095
+ this._onPointerCancel = (event) => {
31096
+ const state = this._dragState;
31097
+ if (!state || event.pointerId !== state.pointerId) return;
31098
+ this._abortDrag();
31099
+ this._endDrag(event.pointerId);
31100
+ };
29252
31101
  }
29253
31102
  _onPanelToggle(e) {
29254
31103
  const panel = e.target;
29255
31104
  if (panel.collapsed) return;
29256
- if (this.allowMultiple) return;
31105
+ if (!this.accordion) return;
29257
31106
  for (const p of this._panels) if (p !== panel) p.collapsed = true;
29258
31107
  }
31108
+ connectedCallback() {
31109
+ super.connectedCallback();
31110
+ this.addEventListener(`click`, this._onClickCapture, true);
31111
+ }
31112
+ disconnectedCallback() {
31113
+ this.removeEventListener(`click`, this._onClickCapture, true);
31114
+ if (this._dragState) {
31115
+ this._abortDrag();
31116
+ this._endDrag(this._dragState.pointerId);
31117
+ }
31118
+ super.disconnectedCallback();
31119
+ }
29259
31120
  firstUpdated() {
31121
+ if (this.gap !== void 0) this.style.setProperty(`--group-gap`, this.gap);
29260
31122
  this.updateComplete.then(() => {
29261
31123
  this._propagateAll();
29262
31124
  });
29263
31125
  }
29264
31126
  updated(changed) {
29265
31127
  super.updated(changed);
31128
+ if (changed.has(`gap`)) {
31129
+ if (this.gap !== void 0) this.style.setProperty(`--group-gap`, this.gap);
31130
+ else this.style.removeProperty(`--group-gap`);
31131
+ }
29266
31132
  if (changed.has(`variant`) || changed.has(`toggleInteraction`) || changed.has(`orientation`) || changed.has(`collapsible`)) this.updateComplete.then(() => {
29267
31133
  this._propagateAll();
29268
31134
  });
@@ -29277,19 +31143,167 @@ let PanelGroupElement = class PanelGroupElement extends i {
29277
31143
  else panel.removeAttribute(`collapsible`);
29278
31144
  }
29279
31145
  }
31146
+ _onSlotChange() {
31147
+ this.updateComplete.then(() => {
31148
+ this._propagateAll();
31149
+ });
31150
+ }
31151
+ /**
31152
+ * Walk composedPath for the nearest ixfx-panel, requiring the press to have
31153
+ * gone through an ixfx-titlebar (titlebar-anywhere drag). Bails when the
31154
+ * path crosses interactive chrome (buttons/links/inputs/toolbar/caret/close)
31155
+ * so those controls always win over dragging.
31156
+ */
31157
+ _findDragTarget(event) {
31158
+ if (!this.allowReorder) return null;
31159
+ const path = event.composedPath();
31160
+ let panel = null;
31161
+ let viaTitlebar = false;
31162
+ for (const node of path) {
31163
+ if (!(node instanceof HTMLElement)) continue;
31164
+ const tag = node.tagName;
31165
+ if (tag === `BUTTON` || tag === `A` || tag === `INPUT` || tag === `SELECT` || tag === `TEXTAREA`) return null;
31166
+ if (node.classList.contains(`toolbar`) || node.classList.contains(`close-btn`) || node.classList.contains(`caret-btn`)) return null;
31167
+ if (node.hasAttribute?.(`slot`) && node.getAttribute(`slot`) === `toolbar`) return null;
31168
+ if (tag === `IXFX-TITLEBAR`) viaTitlebar = true;
31169
+ if (!panel && tag === `IXFX-PANEL`) panel = node;
31170
+ }
31171
+ if (!panel || !viaTitlebar) return null;
31172
+ return panel;
31173
+ }
31174
+ _beginDrag(event) {
31175
+ const state = this._dragState;
31176
+ if (!state || state.active) return;
31177
+ try {
31178
+ this.setPointerCapture(event.pointerId);
31179
+ } catch {}
31180
+ state.active = true;
31181
+ this.setAttribute(`dragging`, ``);
31182
+ state.source.setAttribute(`drag-source`, ``);
31183
+ state.ghost = this._createGhost(state.source);
31184
+ state.ghost?.move(event.clientX, event.clientY);
31185
+ }
31186
+ _abortDrag() {
31187
+ const state = this._dragState;
31188
+ if (!state) return;
31189
+ state.source.removeAttribute(`drag-source`);
31190
+ if (state.gapIndex !== void 0) {
31191
+ state.gapIndex = void 0;
31192
+ this.requestUpdate();
31193
+ }
31194
+ }
31195
+ _endDrag(pointerId) {
31196
+ const state = this._dragState;
31197
+ if (!state) return;
31198
+ state.ghost?.destroy();
31199
+ state.ghost = null;
31200
+ this.removeAttribute(`dragging`);
31201
+ this._dragState = null;
31202
+ window.removeEventListener(`pointermove`, this._onPointerMove);
31203
+ window.removeEventListener(`pointerup`, this._onPointerUp);
31204
+ window.removeEventListener(`pointercancel`, this._onPointerCancel);
31205
+ try {
31206
+ this.releasePointerCapture(pointerId);
31207
+ } catch {}
31208
+ this.requestUpdate();
31209
+ }
31210
+ _commitDrag(source, fromIndex, gapIndex) {
31211
+ source.removeAttribute(`drag-source`);
31212
+ const siblings = (this._panels ?? []).filter((el) => el !== source);
31213
+ const target = gapIndex === void 0 ? siblings.length : Math.max(0, Math.min(gapIndex, siblings.length));
31214
+ const parent = source.parentNode;
31215
+ if (!parent) return;
31216
+ const ref = siblings[target] ?? null;
31217
+ parent.insertBefore(source, ref);
31218
+ const toIndex = Array.prototype.indexOf.call(parent.children, source);
31219
+ if (toIndex === fromIndex) return;
31220
+ this._suppressNextClick = true;
31221
+ this.dispatchEvent(new CustomEvent(`reorder`, {
31222
+ bubbles: true,
31223
+ composed: true,
31224
+ detail: {
31225
+ fromIndex,
31226
+ toIndex,
31227
+ panel: source
31228
+ }
31229
+ }));
31230
+ }
31231
+ _createGhost(source) {
31232
+ const content = document.createElement(`span`);
31233
+ content.style.display = `inline-flex`;
31234
+ content.style.alignItems = `center`;
31235
+ content.style.padding = `0 var(--space-s, 8px)`;
31236
+ content.style.minWidth = `0`;
31237
+ if (source.iconName) {
31238
+ const icon = document.createElement(`ixfx-icon`);
31239
+ icon.name = source.iconName;
31240
+ content.appendChild(icon);
31241
+ }
31242
+ const label = document.createElement(`span`);
31243
+ label.textContent = (source.title || (source.textContent ?? ``).trim()).slice(0, 60);
31244
+ label.style.paddingLeft = `1ch`;
31245
+ label.style.overflow = `hidden`;
31246
+ label.style.textOverflow = `ellipsis`;
31247
+ label.style.whiteSpace = `nowrap`;
31248
+ content.appendChild(label);
31249
+ try {
31250
+ injectPanelGhostStyles();
31251
+ return createDragGhost({
31252
+ anchor: source,
31253
+ content,
31254
+ className: `ixfx-panel-group-ghost`
31255
+ });
31256
+ } catch {
31257
+ return null;
31258
+ }
31259
+ }
31260
+ /**
31261
+ * Nearest panel gap to the pointer, ignoring the drag source.
31262
+ * The perpendicular axis is ignored — the pointer always resolves to a gap.
31263
+ */
31264
+ _computeGapIndex(clientX, clientY) {
31265
+ const state = this._dragState;
31266
+ if (!state) return;
31267
+ const siblings = (this._panels ?? []).filter((el) => el !== state.source);
31268
+ if (siblings.length === 0) return 0;
31269
+ const useX = this.orientation === `horizontal`;
31270
+ const pos = useX ? clientX : clientY;
31271
+ return panelGapIndex(siblings.map((el) => {
31272
+ const rect = el.getBoundingClientRect();
31273
+ return [useX ? rect.left : rect.top, useX ? rect.right : rect.bottom];
31274
+ }), pos);
31275
+ }
31276
+ _computeCaretStyle() {
31277
+ const state = this._dragState;
31278
+ if (!state?.active || state.gapIndex === void 0) return;
31279
+ const siblings = (this._panels ?? []).filter((el) => el !== state.source);
31280
+ if (siblings.length === 0) return;
31281
+ const hostRect = this.getBoundingClientRect();
31282
+ if (!hostRect) return;
31283
+ const gap = Math.max(0, Math.min(state.gapIndex, siblings.length));
31284
+ const rect = gap < siblings.length ? siblings[gap].getBoundingClientRect() : siblings[siblings.length - 1].getBoundingClientRect();
31285
+ if (this.orientation === `horizontal`) return `transform: translate(${(gap < siblings.length ? rect.left : rect.right) - hostRect.left - 1}px, 0); width: 2px; height: ${hostRect.height}px;`;
31286
+ return `transform: translate(0, ${(gap < siblings.length ? rect.top : rect.bottom) - hostRect.top - 1}px); width: ${hostRect.width}px; height: 2px;`;
31287
+ }
29280
31288
  render() {
31289
+ const caretStyle = this._computeCaretStyle();
29281
31290
  return b`
29282
31291
  <slot
29283
31292
  @toggle=${this._onPanelToggle}
31293
+ @slotchange=${this._onSlotChange}
31294
+ @pointerdown=${this._onPointerDown}
31295
+ @keydown=${this._onKeyDown}
29284
31296
  ></slot>
31297
+ ${caretStyle ? b`<div class="drop-caret" style=${caretStyle}></div>` : A}
29285
31298
  `;
29286
31299
  }
29287
31300
  static {
29288
31301
  this.styles = i$3`
29289
31302
  :host {
31303
+ position: relative;
29290
31304
  display: flex;
29291
31305
  flex-direction: column;
29292
- gap: var(--group-gap, var(--space-m));
31306
+ gap: var(--group-gap, var(--space-l));
29293
31307
  }
29294
31308
 
29295
31309
  :host([orientation="horizontal"]) {
@@ -29310,6 +31324,36 @@ let PanelGroupElement = class PanelGroupElement extends i {
29310
31324
  flex-basis: content;
29311
31325
  min-width: 0;
29312
31326
  }
31327
+
31328
+ :host([allow-reorder]) ::slotted(ixfx-panel) {
31329
+ cursor: grab;
31330
+ }
31331
+
31332
+ :host([allow-reorder]) ::slotted(ixfx-panel[drag-source]) {
31333
+ cursor: grabbing;
31334
+ opacity: 0.4;
31335
+ }
31336
+
31337
+ /* While dragging, suppress text selection inside the panels. */
31338
+ :host([dragging]),
31339
+ :host([dragging]) ::slotted(*) {
31340
+ user-select: none;
31341
+ -webkit-user-select: none;
31342
+ }
31343
+
31344
+ :host([dragging]) ::slotted(ixfx-panel) {
31345
+ cursor: grabbing;
31346
+ }
31347
+
31348
+ .drop-caret {
31349
+ position: absolute;
31350
+ top: 0;
31351
+ left: 0;
31352
+ background: var(--accent);
31353
+ border-radius: 1px;
31354
+ pointer-events: none;
31355
+ z-index: 1;
31356
+ }
29313
31357
  `;
29314
31358
  }
29315
31359
  };
@@ -29325,8 +31369,14 @@ __decorate([property({
29325
31369
  __decorate([property({ type: Boolean })], PanelGroupElement.prototype, "collapsible", void 0);
29326
31370
  __decorate([property({
29327
31371
  type: Boolean,
29328
- attribute: `allow-multiple`
29329
- })], PanelGroupElement.prototype, "allowMultiple", void 0);
31372
+ attribute: `accordion`,
31373
+ reflect: true
31374
+ })], PanelGroupElement.prototype, "accordion", void 0);
31375
+ __decorate([property({
31376
+ type: Boolean,
31377
+ attribute: `allow-reorder`,
31378
+ reflect: true
31379
+ })], PanelGroupElement.prototype, "allowReorder", void 0);
29330
31380
  __decorate([property()], PanelGroupElement.prototype, "gap", void 0);
29331
31381
  __decorate([queryAssignedElements({ slot: `` })], PanelGroupElement.prototype, "_panels", void 0);
29332
31382
  PanelGroupElement = __decorate([safeCustomElement(`ixfx-panel-group`)], PanelGroupElement);
@@ -30039,11 +32089,18 @@ let PlotHistogram = class PlotHistogram extends i {
30039
32089
  this.valueTooltips = `hover`;
30040
32090
  this.tooltipDelay = 400;
30041
32091
  this.valueUpdateInterval = 500;
32092
+ this.interactionMode = `inspect`;
30042
32093
  this.valueFormat = (valueMin, valueMax, frequency) => `${valueMin.toFixed(2)} - ${valueMax.toFixed(2)}: ${frequency}`;
30043
32094
  this._width = 100;
30044
32095
  this._height = 40;
30045
32096
  this._hoveredIndex = null;
30046
32097
  this._inlineOverlayText = null;
32098
+ this._coloredRangeMin = null;
32099
+ this._coloredRangeMax = null;
32100
+ this._selectionStart = null;
32101
+ this._selectionEnd = null;
32102
+ this._isDragging = false;
32103
+ this._selectionOverlayText = null;
30047
32104
  this.#series = new PlotDataSeries({
30048
32105
  capacityLimit: 100,
30049
32106
  persistentScaling: true
@@ -30056,6 +32113,22 @@ let PlotHistogram = class PlotHistogram extends i {
30056
32113
  this.#tooltipPosition = null;
30057
32114
  this.#valueRefreshTimer = null;
30058
32115
  this.#onPointerMove = (e) => {
32116
+ if (this.interactionMode === `select` && this._isDragging) {
32117
+ const index = this.#bucketIndexFromPosition(e.clientX, e.clientY);
32118
+ this._selectionEnd = index;
32119
+ const start = Math.min(this._selectionStart, this._selectionEnd);
32120
+ const end = Math.max(this._selectionStart, this._selectionEnd);
32121
+ const bars = this.#computeBars();
32122
+ if (start <= end) {
32123
+ const startBar = bars[start];
32124
+ const endBar = bars[end];
32125
+ this.setColoredRange(startBar.valueMin, endBar.valueMax);
32126
+ this._selectionOverlayText = `Select: ${startBar.valueMin.toFixed(2)} – ${endBar.valueMax.toFixed(2)}`;
32127
+ }
32128
+ this.requestUpdate();
32129
+ return;
32130
+ }
32131
+ if (this.interactionMode === `select`) return;
30059
32132
  if (this.valueTooltips === `none`) return;
30060
32133
  const index = this.#bucketIndexFromPointer(e.clientX, e.clientY);
30061
32134
  const bucketIndex = index >= 0 ? index : null;
@@ -30063,10 +32136,34 @@ let PlotHistogram = class PlotHistogram extends i {
30063
32136
  else this.#updateBubbleTooltip(bucketIndex, e.clientX, e.clientY);
30064
32137
  };
30065
32138
  this.#onPointerLeave = () => {
32139
+ if (this.interactionMode === `select`) {
32140
+ if (this._isDragging) {
32141
+ this._isDragging = false;
32142
+ this._selectionOverlayText = null;
32143
+ this.requestUpdate();
32144
+ }
32145
+ return;
32146
+ }
30066
32147
  if (this.valueTooltips === `none`) return;
30067
32148
  if (this.valueTooltips === `inline`) this.#setInlineIndex(null);
30068
32149
  else this.#updateBubbleTooltip(null, 0, 0);
30069
32150
  };
32151
+ this.#onPointerDown = (e) => {
32152
+ if (this.interactionMode !== `select`) return;
32153
+ const index = this.#bucketIndexFromPosition(e.clientX, e.clientY);
32154
+ this._isDragging = true;
32155
+ this._selectionStart = index;
32156
+ this._selectionEnd = index;
32157
+ const bars = this.#computeBars();
32158
+ const startBar = bars[index];
32159
+ const endBar = bars[index];
32160
+ this.setColoredRange(startBar.valueMin, endBar.valueMax);
32161
+ this._selectionOverlayText = `Select: ${startBar.valueMin.toFixed(2)} – ${endBar.valueMax.toFixed(2)}`;
32162
+ this.requestUpdate();
32163
+ };
32164
+ this.#onPointerUp = () => {
32165
+ if (this.interactionMode === `select`) this._isDragging = false;
32166
+ };
30070
32167
  }
30071
32168
  #series;
30072
32169
  #hasProgrammaticData;
@@ -30090,6 +32187,59 @@ let PlotHistogram = class PlotHistogram extends i {
30090
32187
  this.#series.clear();
30091
32188
  this.requestUpdate();
30092
32189
  }
32190
+ setColoredRange(min, max) {
32191
+ this._coloredRangeMin = min;
32192
+ this._coloredRangeMax = max;
32193
+ this.requestUpdate();
32194
+ }
32195
+ clearColoredRange() {
32196
+ this._coloredRangeMin = null;
32197
+ this._coloredRangeMax = null;
32198
+ this.requestUpdate();
32199
+ }
32200
+ setSelection(startBarIndex, endBarIndex) {
32201
+ const bars = this.#computeBars();
32202
+ const start = Math.min(startBarIndex, endBarIndex);
32203
+ const end = Math.max(startBarIndex, endBarIndex);
32204
+ const clampedStart = Math.max(0, Math.min(start, bars.length - 1));
32205
+ const clampedEnd = Math.max(0, Math.min(end, bars.length - 1));
32206
+ this._selectionStart = clampedStart;
32207
+ this._selectionEnd = clampedEnd;
32208
+ if (clampedStart <= clampedEnd) {
32209
+ const startBar = bars[clampedStart];
32210
+ const endBar = bars[clampedEnd];
32211
+ this.setColoredRange(startBar.valueMin, endBar.valueMax);
32212
+ }
32213
+ this.requestUpdate();
32214
+ }
32215
+ clearSelection() {
32216
+ this._selectionStart = null;
32217
+ this._selectionEnd = null;
32218
+ this.requestUpdate();
32219
+ }
32220
+ getSelectedRange() {
32221
+ if (this._selectionStart === null || this._selectionEnd === null) return {
32222
+ barIndices: null,
32223
+ dataValues: null
32224
+ };
32225
+ const bars = this.#computeBars();
32226
+ const start = Math.min(this._selectionStart, this._selectionEnd);
32227
+ const end = Math.max(this._selectionStart, this._selectionEnd);
32228
+ const clampedStart = Math.max(0, Math.min(start, bars.length - 1));
32229
+ const clampedEnd = Math.max(0, Math.min(end, bars.length - 1));
32230
+ const startBar = bars[clampedStart];
32231
+ const endBar = bars[clampedEnd];
32232
+ return {
32233
+ barIndices: {
32234
+ start: clampedStart,
32235
+ end: clampedEnd
32236
+ },
32237
+ dataValues: {
32238
+ min: startBar.valueMin,
32239
+ max: endBar.valueMax
32240
+ }
32241
+ };
32242
+ }
30093
32243
  firstUpdated(_changed) {
30094
32244
  this._width = this.clientWidth || 100;
30095
32245
  this._height = this.clientHeight || 40;
@@ -30224,6 +32374,10 @@ let PlotHistogram = class PlotHistogram extends i {
30224
32374
  }
30225
32375
  });
30226
32376
  }
32377
+ #isBarInColoredRange(bar) {
32378
+ if (this._coloredRangeMin === null || this._coloredRangeMax === null) return true;
32379
+ return bar.valueMin < this._coloredRangeMax && bar.valueMax > this._coloredRangeMin;
32380
+ }
30227
32381
  /** Index of the bar rect under the pointer, or -1 if none (matches click hit-testing). */
30228
32382
  #bucketIndexFromPointer(clientX, clientY) {
30229
32383
  const rect = this.getBoundingClientRect();
@@ -30231,6 +32385,18 @@ let PlotHistogram = class PlotHistogram extends i {
30231
32385
  const y = clientY - rect.top;
30232
32386
  return this.#computeBars().findIndex((b) => x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height);
30233
32387
  }
32388
+ /** Gets bucket index from position, clamping to valid range (0 to bucketCount-1). */
32389
+ #bucketIndexFromPosition(clientX, clientY) {
32390
+ const { w, h, isHorizontal, padding, dataMin, dataSpan, bucketCount, availableMain } = this.#computeGeometry();
32391
+ const rect = this.getBoundingClientRect();
32392
+ const x = clientX - rect.left;
32393
+ const y = clientY - rect.top;
32394
+ let relativePos;
32395
+ if (isHorizontal) relativePos = (x - padding) / availableMain;
32396
+ else relativePos = (y - padding) / availableMain;
32397
+ relativePos = Math.max(0, Math.min(1, relativePos));
32398
+ return Math.floor(relativePos * bucketCount);
32399
+ }
30234
32400
  #tooltipIndex;
30235
32401
  #tooltipVisible;
30236
32402
  #tooltipShowTimer;
@@ -30365,15 +32531,23 @@ let PlotHistogram = class PlotHistogram extends i {
30365
32531
  }
30366
32532
  #onPointerMove;
30367
32533
  #onPointerLeave;
32534
+ #onPointerDown;
32535
+ #onPointerUp;
30368
32536
  render() {
30369
32537
  const { w: w$7, h, padding } = this.#computeGeometry();
30370
- const barsContent = this.#computeBars().map((b, i) => w`
30371
- <rect class="bar" x=${b.x} y=${b.y} width=${b.width} height=${b.height}
30372
- style="--value: ${b.relativeValue}"
30373
- @click=${() => this.#emitClick(i, b.valueMin, b.valueMax, b.relativeValue)} />
30374
- `);
32538
+ const barsContent = this.#computeBars().map((b, i) => {
32539
+ const greyStyle = this.#isBarInColoredRange(b) ? `` : `filter: grayscale(1)`;
32540
+ return w`
32541
+ <rect class="bar" x=${b.x} y=${b.y} width=${b.width} height=${b.height}
32542
+ style="--value: ${b.relativeValue}; ${greyStyle}"
32543
+ @click=${() => this.#emitClick(i, b.valueMin, b.valueMax, b.relativeValue)} />
32544
+ `;
32545
+ });
30375
32546
  const textColor = `var(--plot-text, var(--surface-2-text, #fff))`;
30376
- const overlayText = this.valueTooltips === `inline` && this._inlineOverlayText !== null ? this._inlineOverlayText : this.title;
32547
+ let overlayText;
32548
+ if (this.interactionMode === `select` && this._selectionOverlayText !== null) overlayText = this._selectionOverlayText;
32549
+ else if (this.valueTooltips === `inline` && this._inlineOverlayText !== null) overlayText = this._inlineOverlayText;
32550
+ else overlayText = this.title;
30377
32551
  const titleContent = overlayText ? w`<text x=${padding + 2} y=${padding + Math.min(10, h * .6)}
30378
32552
  class="overlay-text" fill=${textColor}
30379
32553
  style="opacity: 0.6">${overlayText}</text>` : w``;
@@ -30381,7 +32555,9 @@ let PlotHistogram = class PlotHistogram extends i {
30381
32555
  <svg width=${w$7} height=${h} viewBox="0 0 ${w$7} ${h}"
30382
32556
  xmlns="http://www.w3.org/2000/svg"
30383
32557
  @pointermove=${this.#onPointerMove}
30384
- @pointerleave=${this.#onPointerLeave}>
32558
+ @pointerleave=${this.#onPointerLeave}
32559
+ @pointerdown=${this.#onPointerDown}
32560
+ @pointerup=${this.#onPointerUp}>
30385
32561
  <rect width=${w$7} height=${h} fill="transparent" />
30386
32562
  ${barsContent}
30387
32563
  ${titleContent}
@@ -30443,10 +32619,17 @@ __decorate([property({
30443
32619
  type: Number,
30444
32620
  attribute: `value-update-interval`
30445
32621
  })], PlotHistogram.prototype, "valueUpdateInterval", void 0);
32622
+ __decorate([property({ attribute: `interaction-mode` })], PlotHistogram.prototype, "interactionMode", void 0);
30446
32623
  __decorate([state()], PlotHistogram.prototype, "_width", void 0);
30447
32624
  __decorate([state()], PlotHistogram.prototype, "_height", void 0);
30448
32625
  __decorate([state()], PlotHistogram.prototype, "_hoveredIndex", void 0);
30449
32626
  __decorate([state()], PlotHistogram.prototype, "_inlineOverlayText", void 0);
32627
+ __decorate([state()], PlotHistogram.prototype, "_coloredRangeMin", void 0);
32628
+ __decorate([state()], PlotHistogram.prototype, "_coloredRangeMax", void 0);
32629
+ __decorate([state()], PlotHistogram.prototype, "_selectionStart", void 0);
32630
+ __decorate([state()], PlotHistogram.prototype, "_selectionEnd", void 0);
32631
+ __decorate([state()], PlotHistogram.prototype, "_isDragging", void 0);
32632
+ __decorate([state()], PlotHistogram.prototype, "_selectionOverlayText", void 0);
30450
32633
  PlotHistogram = __decorate([safeCustomElement(`ixfx-plot-histogram`)], PlotHistogram);
30451
32634
  //#endregion
30452
32635
  //#region src/plots/single-axis.ts
@@ -37455,29 +39638,24 @@ var TreeController = class {
37455
39638
  navigateToValue(predicate) {
37456
39639
  const treePath = this.#findTreePath(this.#root, predicate);
37457
39640
  if (!treePath) return false;
39641
+ let currentColumn = this.#element.root?.children ?? [];
37458
39642
  const keys = [];
37459
39643
  for (let i = 1; i < treePath.length; i++) {
37460
39644
  const treeNode = treePath[i];
37461
39645
  const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
37462
39646
  keys.push(key);
37463
39647
  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);
39648
+ const node = currentColumn.find((n) => n.item.key === key);
37467
39649
  if (!node) return false;
37468
39650
  if (node.children === void 0) {
37469
- const mapped = treeNodeMap.get(node);
39651
+ const mapped = this.#nodeToTree.get(node);
37470
39652
  if (mapped) node.children = this.#childrenOf(mapped);
37471
39653
  }
39654
+ currentColumn = node.children ?? [];
37472
39655
  }
37473
39656
  }
37474
39657
  return this.#element.navigateTo(keys);
37475
39658
  }
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
39659
  #childrenOf(treeNode) {
37482
39660
  return Array.from(treeNode.children(), (child) => this.#toTreeNode(child));
37483
39661
  }
@@ -38642,6 +40820,6 @@ function init() {
38642
40820
  console.log(`Init`);
38643
40821
  }
38644
40822
  //#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 };
40823
+ 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, createDataListSearchAdapter, 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, panelGapIndex, 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
40824
 
38647
40825
  //# sourceMappingURL=index.js.map