@ixfx/components 0.13.2 → 0.13.3

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 (76) hide show
  1. package/bundle/index.d.ts +97 -14
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2183 -636
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-token.js +1 -1
  6. package/dist/{data-grid-Ck1Qx2H1.d.ts → data-grid-DHWJN66w.d.ts} +7 -5
  7. package/dist/{data-grid-Ck1Qx2H1.d.ts.map → data-grid-DHWJN66w.d.ts.map} +1 -1
  8. package/dist/data-grid.d.ts +1 -1
  9. package/dist/data-grid.js +210 -120
  10. package/dist/data-grid.js.map +1 -1
  11. package/dist/detail-list-C96B5Txz.js +2001 -0
  12. package/dist/detail-list-C96B5Txz.js.map +1 -0
  13. package/dist/{detail-list-C1xzr1z7.d.ts → detail-list-dX7jLO6b.d.ts} +12 -3
  14. package/dist/{detail-list-C1xzr1z7.d.ts.map → detail-list-dX7jLO6b.d.ts.map} +1 -1
  15. package/dist/detail-list.d.ts +2 -2
  16. package/dist/detail-list.js +1 -1680
  17. package/dist/{flat-list-Ohxqsy0g.d.ts → flat-list-Y98IqH00.d.ts} +2 -2
  18. package/dist/{flat-list-Ohxqsy0g.d.ts.map → flat-list-Y98IqH00.d.ts.map} +1 -1
  19. package/dist/flat-list.d.ts +1 -1
  20. package/dist/flat-list.js +5 -1
  21. package/dist/flat-list.js.map +1 -1
  22. package/dist/{grid-list-pefLORIr.d.ts → grid-list-CoT9e1jB.d.ts} +9 -2
  23. package/dist/{grid-list-pefLORIr.d.ts.map → grid-list-CoT9e1jB.d.ts.map} +1 -1
  24. package/dist/{grid-list-DjIl910A.js → grid-list-DeqAd9-h.js} +189 -40
  25. package/dist/grid-list-DeqAd9-h.js.map +1 -0
  26. package/dist/grid-list.d.ts +1 -1
  27. package/dist/grid-list.js +1 -1
  28. package/dist/{incr-search-D91M4Ozq.js → incr-search-JIhwB49y.js} +27 -5
  29. package/dist/incr-search-JIhwB49y.js.map +1 -0
  30. package/dist/incr-search.d.ts +1 -1
  31. package/dist/incr-search.js +1 -1
  32. package/dist/index-CnQPCywE.d.ts +325 -0
  33. package/dist/index-CnQPCywE.d.ts.map +1 -0
  34. package/dist/{index-CxZ61DO1.d.ts → index-Dlah7Zfi.d.ts} +8 -1
  35. package/dist/{index-CxZ61DO1.d.ts.map → index-Dlah7Zfi.d.ts.map} +1 -1
  36. package/dist/index.d.ts +9 -9
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +46 -25
  39. package/dist/index.js.map +1 -1
  40. package/dist/miller.d.ts +1 -1
  41. package/dist/miller.d.ts.map +1 -1
  42. package/dist/miller.js +146 -100
  43. package/dist/miller.js.map +1 -1
  44. package/dist/narrowed-text.js +1 -1
  45. package/dist/split-layout-C5SYbDrq.d.ts.map +1 -1
  46. package/dist/{split-layout-CXotkdbx.js → split-layout-Deoxy47d.js} +6 -1
  47. package/dist/{split-layout-CXotkdbx.js.map → split-layout-Deoxy47d.js.map} +1 -1
  48. package/dist/split-layout.js +1 -1
  49. package/dist/{tab-list-v8mKX7Fu.d.ts → tab-list-D_dmImJM.d.ts} +2 -3
  50. package/dist/tab-list-D_dmImJM.d.ts.map +1 -0
  51. package/dist/tabs.d.ts +1 -1
  52. package/dist/tabs.js +22 -0
  53. package/dist/tabs.js.map +1 -1
  54. package/dist/{tree-DGveys4K.js → tree-DD8OaqsK.js} +1010 -230
  55. package/dist/tree-DD8OaqsK.js.map +1 -0
  56. package/dist/tree.d.ts +4 -267
  57. package/dist/tree.js +1 -1
  58. package/dist/{vertical-list-CqBc-t8w.d.ts → vertical-list-lj_19DCx.d.ts} +3 -2
  59. package/dist/{vertical-list-CqBc-t8w.d.ts.map → vertical-list-lj_19DCx.d.ts.map} +1 -1
  60. package/dist/vertical-list.d.ts +1 -1
  61. package/dist/vertical-list.js +113 -24
  62. package/dist/vertical-list.js.map +1 -1
  63. package/docs-user/data-grid.md +74 -4
  64. package/docs-user/detail-list.md +65 -0
  65. package/docs-user/grid-list.md +81 -4
  66. package/docs-user/index.json +1 -1
  67. package/docs-user/miller.md +59 -3
  68. package/docs-user/tree.md +63 -2
  69. package/docs-user/vertical-list.md +9 -1
  70. package/package.json +6 -1
  71. package/dist/detail-list.js.map +0 -1
  72. package/dist/grid-list-DjIl910A.js.map +0 -1
  73. package/dist/incr-search-D91M4Ozq.js.map +0 -1
  74. package/dist/tab-list-v8mKX7Fu.d.ts.map +0 -1
  75. package/dist/tree-DGveys4K.js.map +0 -1
  76. package/dist/tree.d.ts.map +0 -1
package/bundle/index.js CHANGED
@@ -10,6 +10,7 @@ import { Colour } from "@ixfx/visual";
10
10
  import { Trees } from "@ixfx/collections";
11
11
  import { clamp, clamper, rangeStream, round, scaler } from "@ixfx/numbers";
12
12
  import { classMap } from "lit/directives/class-map.js";
13
+ import { ifDefined } from "lit/directives/if-defined.js";
13
14
  import { createRef, ref } from "lit/directives/ref.js";
14
15
  import { Easings } from "@ixfx/modulation";
15
16
  import "@clinth/vcanvas";
@@ -2073,7 +2074,11 @@ function createElementListSearchAdapter(host, options = {}) {
2073
2074
  const getLabel = options.getLabel ?? defaultLabel;
2074
2075
  const getKey = options.getKey ?? ((el) => keyForElement(el) ?? defaultLabel(el));
2075
2076
  const hidden = /* @__PURE__ */ new Set();
2076
- const items = () => Array.from(host.querySelectorAll(itemSelector));
2077
+ const observeContainer = options.observeContainer ?? true;
2078
+ const items = () => {
2079
+ if (options.getAllItems) return options.getAllItems();
2080
+ return Array.from(host.querySelectorAll(itemSelector));
2081
+ };
2077
2082
  return {
2078
2083
  getItems: items,
2079
2084
  getLabel,
@@ -2082,7 +2087,9 @@ function createElementListSearchAdapter(host, options = {}) {
2082
2087
  if (el) host.focusKey(getKey(el));
2083
2088
  },
2084
2089
  activateItem: (el) => {
2085
- el.dispatchEvent(new MouseEvent(`click`, {
2090
+ const key = getKey(el);
2091
+ if (host.activateKey) host.activateKey(key);
2092
+ else el.dispatchEvent(new MouseEvent(`click`, {
2086
2093
  bubbles: true,
2087
2094
  composed: true
2088
2095
  }));
@@ -2108,7 +2115,7 @@ function createElementListSearchAdapter(host, options = {}) {
2108
2115
  highlightKeyOf: (el) => getKey(el),
2109
2116
  highlightText: getLabel,
2110
2117
  highlightKey: options.highlightKey,
2111
- container: () => host
2118
+ container: observeContainer ? () => host : void 0
2112
2119
  };
2113
2120
  }
2114
2121
  //#endregion
@@ -2426,6 +2433,10 @@ var ListSearchController = class {
2426
2433
  }
2427
2434
  this.#search.setItems(items);
2428
2435
  }
2436
+ /** Re-paint highlights without rebuilding the item set. Use when the rendered window changes. */
2437
+ repaintHighlights() {
2438
+ this.#applyHighlights();
2439
+ }
2429
2440
  /** Popover markup. Pair with `listSearchStyles` in the host's `static styles`. */
2430
2441
  renderField() {
2431
2442
  if (!this.enabled) return nothing;
@@ -2547,9 +2558,10 @@ var ListSearchController = class {
2547
2558
  return;
2548
2559
  }
2549
2560
  const state = this.#search.state;
2561
+ const itemByKey = new Map(state.results.map((r) => [r.item.key, r.item]));
2550
2562
  const targets = buildHighlightTargets(root, selector, state.positionsByItem, (el) => {
2551
2563
  const key = keyOf(el);
2552
- return key === void 0 ? void 0 : state.results.find((r) => r.item.key === key)?.item;
2564
+ return key === void 0 ? void 0 : itemByKey.get(key);
2553
2565
  }, this.#adapter.highlightText ?? ((el) => el.textContent ?? ``));
2554
2566
  try {
2555
2567
  applyHighlights(targets, { key: this.#highlightKey });
@@ -2673,6 +2685,16 @@ const listSearchStyles = css`
2673
2685
  //#endregion
2674
2686
  //#region src/incr-search/tree-list-search-adapter.ts
2675
2687
  /**
2688
+ * The label's own text. Highlights are painted on one text node matched by its
2689
+ * exact content, but a Lit-rendered label also holds template whitespace and
2690
+ * sibling bindings (tooltips), so `textContent` is padded and never equals the
2691
+ * node's content. Take the first non-blank text node instead.
2692
+ */
2693
+ function labelText(el) {
2694
+ for (const node of el.childNodes) if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? ``).trim().length > 0) return node.textContent ?? ``;
2695
+ return el.textContent ?? ``;
2696
+ }
2697
+ /**
2676
2698
  * A {@link ListSearchAdapter} over any `TreeSearchHost` (`ixfx-tree-list`,
2677
2699
  * `ixfx-miller-list`, `ixfx-crumb-navigation`). Navigate reveals the match
2678
2700
  * (expands its ancestors, moves the cursor); select drives `selectMany`; filter
@@ -2700,6 +2722,7 @@ function createTreeListSearchAdapter(host, options = {}) {
2700
2722
  },
2701
2723
  highlightRoot: () => host.shadowRoot,
2702
2724
  highlightSelector: options.labelSelector ?? `.tree-label`,
2725
+ highlightText: labelText,
2703
2726
  highlightKeyOf: (el) => el.closest(`[data-key]`)?.dataset.key,
2704
2727
  highlightKey: options.highlightKey
2705
2728
  };
@@ -15220,6 +15243,10 @@ function sameKeys$4(a, b) {
15220
15243
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
15221
15244
  return true;
15222
15245
  }
15246
+ /** Element-wise reference equality, treating `undefined` (never computed) as different. */
15247
+ function sameRefs(a, b) {
15248
+ return a !== void 0 && a.length === b.length && a.every((v, i) => v === b[i]);
15249
+ }
15223
15250
  let DataGridElement = class DataGridElement extends LitElement {
15224
15251
  constructor(..._args) {
15225
15252
  super(..._args);
@@ -15237,7 +15264,7 @@ let DataGridElement = class DataGridElement extends LitElement {
15237
15264
  this.groupBy = null;
15238
15265
  this.allowGrouping = false;
15239
15266
  this.crossGroupSelection = false;
15240
- this.columnMenu = `auto`;
15267
+ this.columnMenu = `none`;
15241
15268
  this.textColorColumn = ``;
15242
15269
  this.iconColumn = ``;
15243
15270
  this.badgeColumn = ``;
@@ -15258,9 +15285,9 @@ let DataGridElement = class DataGridElement extends LitElement {
15258
15285
  this._lastClickedCell = null;
15259
15286
  this._editValue = ``;
15260
15287
  this._tickled = new TickledController(this, {
15261
- getItems: () => this._flatKeys.map((k) => ({
15262
- id: k,
15263
- enabled: true
15288
+ getItems: () => this.#tickledItems ??= this._flat.map((f) => ({
15289
+ id: f.key,
15290
+ enabled: f.type === `row`
15264
15291
  })),
15265
15292
  onStateChange: () => this.requestUpdate(),
15266
15293
  hoverDisabled: () => this.#nativeDrag.isDragging
@@ -15307,6 +15334,9 @@ let DataGridElement = class DataGridElement extends LitElement {
15307
15334
  this._sortedRows = [];
15308
15335
  this._flat = [];
15309
15336
  this._flatKeys = [];
15337
+ this.#flatIndexByKey = /* @__PURE__ */ new Map();
15338
+ this.#flatVersion = 0;
15339
+ this.#notedFlatVersion = -1;
15310
15340
  this.#searchKeyByRow = /* @__PURE__ */ new Map();
15311
15341
  this.search = new ListSearchController(this, { adapter: this.#createSearchAdapter() });
15312
15342
  this.#prevFlatKeys = [];
@@ -15318,7 +15348,6 @@ let DataGridElement = class DataGridElement extends LitElement {
15318
15348
  this._colWidthSum = 0;
15319
15349
  this._raf = 0;
15320
15350
  this.#viewportChangeRaf = 0;
15321
- this.#appendInProgress = false;
15322
15351
  this.#resortRaf = 0;
15323
15352
  this._drag = null;
15324
15353
  this._onFocusOut = (e) => {
@@ -15335,10 +15364,12 @@ let DataGridElement = class DataGridElement extends LitElement {
15335
15364
  const el = e.target;
15336
15365
  cancelAnimationFrame(this._raf);
15337
15366
  this._raf = requestAnimationFrame(() => {
15367
+ const before = this.#windowRows(2);
15338
15368
  this._scrollTop = el.scrollTop;
15339
15369
  this._scrollLeft = el.scrollLeft;
15340
15370
  if (this._headerScrollEl) this._headerScrollEl.scrollLeft = el.scrollLeft;
15341
- this.requestUpdate();
15371
+ const after = this.#windowRows(2);
15372
+ if (before.start !== after.start || before.end !== after.end) this.requestUpdate();
15342
15373
  cancelAnimationFrame(this.#viewportChangeRaf);
15343
15374
  this.#viewportChangeRaf = 0;
15344
15375
  this.#emitViewportChangeIfChanged();
@@ -15377,7 +15408,7 @@ let DataGridElement = class DataGridElement extends LitElement {
15377
15408
  let nextIdx = idx + dir;
15378
15409
  while (nextIdx >= 0 && nextIdx < rowKeys.length) {
15379
15410
  const nk = rowKeys[nextIdx];
15380
- const row = this._sortedRows.find((r, i) => this.getRowKey(r, i) === nk);
15411
+ const row = this.#rowEntry(nk)?.row;
15381
15412
  const col = this._visibleColumns.find((c) => c.key === colKey);
15382
15413
  if (row && col && this._isEditable(row, col)) {
15383
15414
  this._startEdit(nk, colKey);
@@ -15426,9 +15457,9 @@ let DataGridElement = class DataGridElement extends LitElement {
15426
15457
  if (!key) break;
15427
15458
  e.preventDefault();
15428
15459
  if (!this._editing && this.editability === `user`) {
15429
- const idx = this._sortedRows.findIndex((r, i) => this.getRowKey(r, i) === key);
15430
- if (idx >= 0) {
15431
- const row = this._sortedRows[idx];
15460
+ const entry = this.#rowEntry(key);
15461
+ if (entry) {
15462
+ const row = entry.row;
15432
15463
  if (this._lastClickedCell?.rowKey === key) {
15433
15464
  const col = this._visibleColumns.find((c) => c.key === this._lastClickedCell.colKey);
15434
15465
  if (col && this._isEditable(row, col)) {
@@ -15444,9 +15475,10 @@ let DataGridElement = class DataGridElement extends LitElement {
15444
15475
  }
15445
15476
  }
15446
15477
  if (!this._editing) {
15447
- const idx = this._sortedRows.findIndex((r, i) => this.getRowKey(r, i) === key);
15478
+ const entry = this.#rowEntry(key);
15479
+ const idx = entry?.idx ?? -1;
15448
15480
  if (this._selectedKeys.has(key)) {
15449
- this._activateRow(key, idx >= 0 ? this._sortedRows[idx] : void 0, idx);
15481
+ this._activateRow(key, entry?.row, idx);
15450
15482
  break;
15451
15483
  }
15452
15484
  if (this.interactionMode !== `manual` && this.interactionMode !== `checked`) this._replaceSelection(key);
@@ -15476,7 +15508,7 @@ let DataGridElement = class DataGridElement extends LitElement {
15476
15508
  next = !active.checked;
15477
15509
  active.checked = next;
15478
15510
  } else {
15479
- const row = this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey);
15511
+ const row = this.#rowEntry(rowKey)?.row;
15480
15512
  next = !(this._editValue !== `` ? Boolean(this._editValue) : Boolean(row?.[colKey]));
15481
15513
  }
15482
15514
  if (isImmediate) this._commitImmediate(rowKey, colKey, next);
@@ -15745,6 +15777,15 @@ let DataGridElement = class DataGridElement extends LitElement {
15745
15777
  this.#postDragSuppressTimer = 0;
15746
15778
  }, 0);
15747
15779
  }
15780
+ #flatIndexByKey;
15781
+ /** Bumped whenever `_flat` changes; drives `#noteFlatMutation`. */
15782
+ #flatVersion;
15783
+ #notedFlatVersion;
15784
+ /** Inputs `_sortedRows` / `_flat` were last derived from — see `#syncDerivedRows`. */
15785
+ #sortInputs;
15786
+ #flatInputs;
15787
+ /** `_flat` projected for the tickled controller, cached until `_flat` changes. */
15788
+ #tickledItems;
15748
15789
  #searchRegistry;
15749
15790
  #unregisterSearchCommands;
15750
15791
  #searchKeyByRow;
@@ -15803,7 +15844,6 @@ let DataGridElement = class DataGridElement extends LitElement {
15803
15844
  /** Key sets as of the last `datagrid-viewport-change` dispatch, for dedupe. */
15804
15845
  #lastViewportRenderedKeys;
15805
15846
  #lastViewportVisibleKeys;
15806
- #appendInProgress;
15807
15847
  #resortRaf;
15808
15848
  connectedCallback() {
15809
15849
  super.connectedCallback();
@@ -15876,7 +15916,7 @@ let DataGridElement = class DataGridElement extends LitElement {
15876
15916
  updated(changed) {
15877
15917
  super.updated(changed);
15878
15918
  if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`filterPredicate`) || changed.has(`rowHeight`)) this.#scheduleViewportChangeEmit();
15879
- if (changed.has(`_editing`) || changed.has(`_editingRow`)) requestAnimationFrame(() => {
15919
+ if (changed.has(`_editing`) || changed.has(`_editingRow`)) this.updateComplete.then(() => {
15880
15920
  let sel = null;
15881
15921
  if (this._editing) sel = `.row[data-key="${this._editing.rowKey}"] .cell[data-col="${this._editing.colKey}"] input, .row[data-key="${this._editing.rowKey}"] .cell[data-col="${this._editing.colKey}"] select`;
15882
15922
  else if (this._editingRow) sel = `.row[data-key="${this._editingRow}"] .cell input, .row[data-key="${this._editingRow}"] .cell select`;
@@ -15890,10 +15930,7 @@ let DataGridElement = class DataGridElement extends LitElement {
15890
15930
  super.willUpdate(changed);
15891
15931
  this.setAttribute(`data-input-mode`, this._tickled.inputMode);
15892
15932
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
15893
- if (!this.#appendInProgress) {
15894
- if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`)) this._sortedRows = this._applySort([...this.rows]);
15895
- this._rebuildFlat();
15896
- }
15933
+ this.#syncDerivedRows();
15897
15934
  this._visibleColumns = this.columns.filter((c) => c.visible !== false);
15898
15935
  let off = 0;
15899
15936
  this._colOffsets = this._visibleColumns.map((c) => {
@@ -15903,7 +15940,53 @@ let DataGridElement = class DataGridElement extends LitElement {
15903
15940
  });
15904
15941
  this._colWidthSum = off;
15905
15942
  if (this.interactionMode === `checked`) this._colWidthSum += 28;
15906
- this.#noteFlatMutation();
15943
+ if (this.#flatVersion !== this.#notedFlatVersion) {
15944
+ this.#notedFlatVersion = this.#flatVersion;
15945
+ this.#noteFlatMutation();
15946
+ }
15947
+ }
15948
+ /**
15949
+ * Re-derive `_sortedRows` / `_flat` only when one of their inputs changed
15950
+ * identity. Scroll, hover, selection and edit-state updates touch none of
15951
+ * them, so those frames cost O(window) instead of O(rows).
15952
+ */
15953
+ #syncDerivedRows() {
15954
+ const sortInputs = [
15955
+ this.rows,
15956
+ this.sortState,
15957
+ this.columns
15958
+ ];
15959
+ const flatInputs = [
15960
+ this.filterPredicate,
15961
+ this.groupBy,
15962
+ this.groupLabel,
15963
+ this._collapsedGroups,
15964
+ this.getRowKey
15965
+ ];
15966
+ const resort = !sameRefs(this.#sortInputs, sortInputs);
15967
+ if (resort) {
15968
+ this.#sortInputs = sortInputs;
15969
+ this._sortedRows = this._applySort([...this.rows]);
15970
+ }
15971
+ if (resort || !sameRefs(this.#flatInputs, flatInputs)) {
15972
+ this.#flatInputs = flatInputs;
15973
+ this._rebuildFlat();
15974
+ }
15975
+ }
15976
+ /** Mark the derived rows as matching the current inputs (after an incremental update). */
15977
+ #markDerivedRowsSynced() {
15978
+ this.#sortInputs = [
15979
+ this.rows,
15980
+ this.sortState,
15981
+ this.columns
15982
+ ];
15983
+ this.#flatInputs = [
15984
+ this.filterPredicate,
15985
+ this.groupBy,
15986
+ this.groupLabel,
15987
+ this._collapsedGroups,
15988
+ this.getRowKey
15989
+ ];
15907
15990
  }
15908
15991
  /**
15909
15992
  * Diff the row-key list against the previous update. Rows that appeared get an
@@ -15924,10 +16007,9 @@ let DataGridElement = class DataGridElement extends LitElement {
15924
16007
  return;
15925
16008
  }
15926
16009
  const before = new Set(prev);
15927
- const after = new Set(now);
15928
16010
  const added = now.filter((k) => !before.has(k));
15929
- const removed = prev.filter((k) => !after.has(k));
15930
- if (added.length === now.length || added.length === 0 && removed.length === 0) return;
16011
+ const removedCount = prev.length - (now.length - added.length);
16012
+ if (added.length === now.length || added.length === 0 && removedCount === 0) return;
15931
16013
  this.#enteringRowKeys = new Set(added);
15932
16014
  this.toggleAttribute(`reflowing`, true);
15933
16015
  if (this.#rowReflowTimer) clearTimeout(this.#rowReflowTimer);
@@ -15944,55 +16026,64 @@ let DataGridElement = class DataGridElement extends LitElement {
15944
16026
  return String(this.groupBy(row));
15945
16027
  }
15946
16028
  _rebuildFlat() {
15947
- const filtered = this.filterPredicate !== void 0;
15948
- const source = filtered ? this._sortedRows.filter((r) => this.filterPredicate(r)) : this._sortedRows;
15949
- if (!this.groupBy) {
15950
- this._flat = source.map((r, i) => {
15951
- const idx = filtered ? this._sortedRows.indexOf(r) : i;
15952
- return {
15953
- type: `row`,
15954
- key: this.getRowKey(r, idx),
15955
- row: r,
15956
- idx
15957
- };
15958
- });
15959
- this._flatKeys = this._flat.map((f) => f.key);
15960
- return;
15961
- }
15962
- const groups = /* @__PURE__ */ new Map();
15963
- for (const r of source) {
15964
- const k = this._getGroupKey(r) ?? ``;
15965
- const arr = groups.get(k);
15966
- if (arr) arr.push(r);
15967
- else groups.set(k, [r]);
15968
- }
16029
+ const pred = this.filterPredicate;
16030
+ const sourceIdx = [];
16031
+ for (let i = 0; i < this._sortedRows.length; i++) if (!pred || pred(this._sortedRows[i])) sourceIdx.push(i);
15969
16032
  const flat = [];
15970
16033
  const keys = [];
15971
- for (const [gKey, rows] of groups) {
15972
- const label = this.groupLabel ? this.groupLabel(gKey, rows) : gKey;
15973
- const gId = `__group__${gKey}`;
16034
+ const pushRow = (idx) => {
16035
+ const row = this._sortedRows[idx];
16036
+ const key = this.getRowKey(row, idx);
15974
16037
  flat.push({
15975
- type: `group`,
15976
- key: gId,
15977
- label,
15978
- count: rows.length
16038
+ type: `row`,
16039
+ key,
16040
+ row,
16041
+ idx
15979
16042
  });
15980
- keys.push(gId);
15981
- if (this._collapsedGroups.has(gKey)) continue;
15982
- for (const r of rows) {
15983
- const idx = this._sortedRows.indexOf(r);
15984
- const rk = this.getRowKey(r, idx);
16043
+ keys.push(key);
16044
+ };
16045
+ if (!this.groupBy) sourceIdx.forEach(pushRow);
16046
+ else {
16047
+ const groups = /* @__PURE__ */ new Map();
16048
+ for (const idx of sourceIdx) {
16049
+ const k = this._getGroupKey(this._sortedRows[idx]) ?? ``;
16050
+ const arr = groups.get(k);
16051
+ if (arr) arr.push(idx);
16052
+ else groups.set(k, [idx]);
16053
+ }
16054
+ for (const [gKey, idxs] of groups) {
16055
+ const label = this.groupLabel ? this.groupLabel(gKey, idxs.map((i) => this._sortedRows[i])) : gKey;
16056
+ const gId = `__group__${gKey}`;
15985
16057
  flat.push({
15986
- type: `row`,
15987
- key: rk,
15988
- row: r,
15989
- idx
16058
+ type: `group`,
16059
+ key: gId,
16060
+ label,
16061
+ count: idxs.length
15990
16062
  });
15991
- keys.push(rk);
16063
+ keys.push(gId);
16064
+ if (!this._collapsedGroups.has(gKey)) idxs.forEach(pushRow);
15992
16065
  }
15993
16066
  }
15994
16067
  this._flat = flat;
15995
16068
  this._flatKeys = keys;
16069
+ this.#rebuildFlatIndex();
16070
+ }
16071
+ #rebuildFlatIndex() {
16072
+ this.#flatIndexByKey.clear();
16073
+ this._flat.forEach((f, i) => this.#flatIndexByKey.set(f.key, i));
16074
+ this.#flatVersion++;
16075
+ this.#tickledItems = void 0;
16076
+ }
16077
+ /** Flat entry for a row key, or `undefined` for unknown keys and group headers. */
16078
+ #rowEntry(key) {
16079
+ const i = this.#flatIndexByKey.get(key);
16080
+ const entry = i === void 0 ? void 0 : this._flat[i];
16081
+ if (entry?.type === `row`) return entry;
16082
+ const idx = this._sortedRows.findIndex((r, j) => this.getRowKey(r, j) === key);
16083
+ return idx < 0 ? void 0 : {
16084
+ row: this._sortedRows[idx],
16085
+ idx
16086
+ };
15996
16087
  }
15997
16088
  _toggleGroup(gKey) {
15998
16089
  const next = new Set(this._collapsedGroups);
@@ -16222,17 +16313,26 @@ let DataGridElement = class DataGridElement extends LitElement {
16222
16313
  }));
16223
16314
  }
16224
16315
  _scrollRowIntoView(key) {
16225
- const idx = this._flat.findIndex((f) => f.key === key);
16226
- if (idx < 0 || !this._viewportEl) return;
16227
- const top = idx * this.rowHeight;
16228
- const bottom = top + this.rowHeight;
16316
+ const idx = this.#flatIndexByKey.get(key);
16317
+ if (idx === void 0 || !this._viewportEl) return;
16318
+ const rh = this.rowHeight;
16319
+ const top = (idx > 0 && this._flat[idx - 1].type === `group` ? idx - 1 : idx) * rh;
16320
+ const bottom = (idx + 1) * rh;
16229
16321
  const vt = this._viewportEl.scrollTop;
16230
- const vb = vt + this._viewportEl.clientHeight;
16231
- if (top < vt) this._viewportEl.scrollTop = top;
16232
- else if (bottom > vb) this._viewportEl.scrollTop = bottom - this._viewportEl.clientHeight;
16322
+ const vh = this._viewportEl.clientHeight;
16323
+ let next = vt;
16324
+ if (top < vt) next = top;
16325
+ else if (bottom > vt + vh) next = bottom - vh;
16326
+ if (next !== vt) {
16327
+ this._viewportEl.scrollTop = next;
16328
+ this._scrollTop = next;
16329
+ this.requestUpdate();
16330
+ }
16233
16331
  }
16234
16332
  _getRowForKey(key) {
16235
- return this._flat.find((f) => f.key === key)?.row;
16333
+ const i = this.#flatIndexByKey.get(key);
16334
+ const entry = i === void 0 ? void 0 : this._flat[i];
16335
+ return entry?.type === `row` ? entry.row : void 0;
16236
16336
  }
16237
16337
  /** The selected row keys (read-only). */
16238
16338
  get selectedKeys() {
@@ -16249,8 +16349,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16249
16349
  * set when the selection actually changes.
16250
16350
  */
16251
16351
  selectKeys(keys) {
16252
- const known = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
16253
- let next = new Set([...keys].filter((k) => known.has(k)));
16352
+ let next = new Set([...keys].filter((k) => this._getRowForKey(k) !== void 0));
16254
16353
  const multi = this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
16255
16354
  if (this.selectionMode === `none`) next = /* @__PURE__ */ new Set();
16256
16355
  else if (!multi && next.size > 1) {
@@ -16276,7 +16375,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16276
16375
  }
16277
16376
  /** Move the cursor to this row key and scroll it into view. */
16278
16377
  tickleRow(key) {
16279
- if (!this._flatKeys.includes(key)) return;
16378
+ if (!this.#flatIndexByKey.has(key) || key.startsWith(`__group__`)) return;
16280
16379
  this._tickled.focus(key);
16281
16380
  this._scrollRowIntoView(key);
16282
16381
  }
@@ -16290,25 +16389,21 @@ let DataGridElement = class DataGridElement extends LitElement {
16290
16389
  appendRows(rows) {
16291
16390
  if (rows.length === 0) return;
16292
16391
  const startIdx = this.rows.length;
16293
- this.#appendInProgress = true;
16392
+ const inSync = this._sortedRows.length === startIdx && sameRefs(this.#sortInputs, [
16393
+ this.rows,
16394
+ this.sortState,
16395
+ this.columns
16396
+ ]);
16294
16397
  this.rows = [...this.rows, ...rows];
16398
+ if (!inSync) return;
16295
16399
  const appended = this.rows.slice(startIdx);
16296
- if (this._sortedRows.length === startIdx) this._sortedRows = [...this._sortedRows, ...appended];
16297
- else {
16298
- this._sortedRows = this._applySort([...this.rows]);
16299
- this._rebuildFlat();
16300
- this.#appendInProgress = false;
16301
- this.requestUpdate();
16302
- return;
16303
- }
16400
+ this._sortedRows = [...this._sortedRows, ...appended];
16304
16401
  this.#appendToFlat(appended, startIdx);
16305
- this.#appendInProgress = false;
16306
- this.requestUpdate();
16402
+ this.#markDerivedRowsSynced();
16307
16403
  if (this.sortState != null) {
16308
16404
  cancelAnimationFrame(this.#resortRaf);
16309
16405
  this.#resortRaf = requestAnimationFrame(() => {
16310
- this._sortedRows = this._applySort([...this.rows]);
16311
- this._rebuildFlat();
16406
+ this.#sortInputs = void 0;
16312
16407
  this.requestUpdate();
16313
16408
  });
16314
16409
  }
@@ -16320,6 +16415,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16320
16415
  }
16321
16416
  for (let j = 0; j < appended.length; j++) {
16322
16417
  const idx = startIdx + j;
16418
+ if (this.filterPredicate && !this.filterPredicate(appended[j])) continue;
16323
16419
  const key = this.getRowKey(appended[j], idx);
16324
16420
  this._flat.push({
16325
16421
  type: `row`,
@@ -16328,7 +16424,10 @@ let DataGridElement = class DataGridElement extends LitElement {
16328
16424
  idx
16329
16425
  });
16330
16426
  this._flatKeys.push(key);
16427
+ this.#flatIndexByKey.set(key, this._flat.length - 1);
16331
16428
  }
16429
+ this.#flatVersion++;
16430
+ this.#tickledItems = void 0;
16332
16431
  }
16333
16432
  _replaceSelection(key) {
16334
16433
  if (this.selectionMode === `none`) return;
@@ -16434,8 +16533,8 @@ let DataGridElement = class DataGridElement extends LitElement {
16434
16533
  return !e || e === `auto`;
16435
16534
  }
16436
16535
  _isCellEditing(rowKey, colKey) {
16437
- if (this.editability === `immediate` && this._isEditable(this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey) ?? {}, this.columns.find((c) => c.key === colKey))) return true;
16438
- if (this._editingRow === rowKey) return this._isEditable(this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey) ?? {}, this.columns.find((c) => c.key === colKey));
16536
+ if (this.editability === `immediate` && this._isEditable(this.#rowEntry(rowKey)?.row ?? {}, this.columns.find((c) => c.key === colKey))) return true;
16537
+ if (this._editingRow === rowKey) return this._isEditable(this.#rowEntry(rowKey)?.row ?? {}, this.columns.find((c) => c.key === colKey));
16439
16538
  return this._editing?.rowKey === rowKey && this._editing?.colKey === colKey;
16440
16539
  }
16441
16540
  _activateRow(rowKey, row, rowIndex) {
@@ -16451,7 +16550,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16451
16550
  }
16452
16551
  _startEdit(rowKey, colKey) {
16453
16552
  if (this.editability === `disabled`) return;
16454
- const row = this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey);
16553
+ const row = this.#rowEntry(rowKey)?.row;
16455
16554
  if (!row) return;
16456
16555
  const col = this.columns.find((c) => c.key === colKey);
16457
16556
  if (!col || !this._isEditable(row, col)) return;
@@ -16469,7 +16568,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16469
16568
  /** Programmatic: put all editable cells of a row in edit mode. */
16470
16569
  startRowEdit(rowKey) {
16471
16570
  if (this.editability === `disabled`) return;
16472
- if (!this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey)) return;
16571
+ if (!this.#rowEntry(rowKey)?.row) return;
16473
16572
  this._editing = null;
16474
16573
  this._editingRow = rowKey;
16475
16574
  this.requestUpdate();
@@ -16537,7 +16636,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16537
16636
  const target = this._editing;
16538
16637
  if (!target) return;
16539
16638
  const { rowKey, colKey } = target;
16540
- const row = this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey);
16639
+ const row = this.#rowEntry(rowKey)?.row;
16541
16640
  if (!row) {
16542
16641
  this._editing = null;
16543
16642
  return;
@@ -16562,7 +16661,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16562
16661
  }));
16563
16662
  }
16564
16663
  _tabToNext(rowKey, curColKey, dir) {
16565
- const row = this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey);
16664
+ const row = this.#rowEntry(rowKey)?.row;
16566
16665
  if (!row) return;
16567
16666
  const cols = this._visibleColumns;
16568
16667
  let next = cols.findIndex((c) => c.key === curColKey) + dir;
@@ -16583,7 +16682,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16583
16682
  this.requestUpdate();
16584
16683
  }
16585
16684
  _commitImmediate(rowKey, colKey, value) {
16586
- const row = this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey);
16685
+ const row = this.#rowEntry(rowKey)?.row;
16587
16686
  if (!row) return;
16588
16687
  const oldValue = row[colKey];
16589
16688
  if (oldValue === value) return;
@@ -16706,11 +16805,19 @@ let DataGridElement = class DataGridElement extends LitElement {
16706
16805
  window.addEventListener(`pointermove`, onMove);
16707
16806
  window.addEventListener(`pointerup`, onUp);
16708
16807
  }
16709
- getVisibleRange() {
16808
+ /** Flat-index window `[start, end)` intersecting the viewport, widened by `overscan` rows each side. */
16809
+ #windowRows(overscan) {
16710
16810
  const rh = this.rowHeight;
16711
16811
  return {
16712
- startRow: Math.max(0, Math.floor(this._scrollTop / rh) - 2),
16713
- endRow: Math.min(this._flat.length, Math.ceil((this._scrollTop + this._viewportH) / rh) + 2),
16812
+ start: Math.max(0, Math.floor(this._scrollTop / rh) - overscan),
16813
+ end: Math.min(this._flat.length, Math.ceil((this._scrollTop + this._viewportH) / rh) + overscan)
16814
+ };
16815
+ }
16816
+ getVisibleRange() {
16817
+ const { start: startRow, end: endRow } = this.#windowRows(2);
16818
+ return {
16819
+ startRow,
16820
+ endRow,
16714
16821
  startCol: this._findFirstVisibleCol(this._scrollLeft),
16715
16822
  endCol: this._findLastVisibleCol(this._scrollLeft + this._viewportW)
16716
16823
  };
@@ -16725,10 +16832,8 @@ let DataGridElement = class DataGridElement extends LitElement {
16725
16832
  * `ixfx-grid-list.visibleKeys()` for the `datagrid-viewport-change` detail.
16726
16833
  */
16727
16834
  #visibleKeys() {
16728
- const rh = this.rowHeight;
16729
- const startRow = Math.max(0, Math.floor(this._scrollTop / rh));
16730
- const endRow = Math.min(this._flat.length, Math.ceil((this._scrollTop + this._viewportH) / rh));
16731
- return this._flat.slice(startRow, endRow).filter((f) => f.type === `row`).map((f) => f.key);
16835
+ const { start, end } = this.#windowRows(0);
16836
+ return this._flat.slice(start, end).filter((f) => f.type === `row`).map((f) => f.key);
16732
16837
  }
16733
16838
  /** Coalesce viewport-change dispatches to at most one per animation frame. */
16734
16839
  #scheduleViewportChangeEmit() {
@@ -16777,17 +16882,28 @@ let DataGridElement = class DataGridElement extends LitElement {
16777
16882
  }
16778
16883
  render() {
16779
16884
  const rh = this.rowHeight;
16780
- const startRow = Math.max(0, Math.floor(this._scrollTop / rh) - 2);
16781
- const endRow = Math.min(this._flat.length, Math.ceil((this._scrollTop + this._viewportH) / rh) + 2);
16782
- const visibleFlat = this._flat.slice(startRow, endRow);
16885
+ const { start: startRow, end: endRow } = this.#windowRows(2);
16886
+ const visibleFlat = this._flat.slice(startRow, endRow).map((entry, i) => ({
16887
+ entry,
16888
+ absIdx: startRow + i
16889
+ }));
16890
+ const pinnedKey = this._editing?.rowKey ?? this._editingRow;
16891
+ const pinnedIdx = pinnedKey ? this.#flatIndexByKey.get(pinnedKey) : void 0;
16892
+ if (pinnedIdx !== void 0) {
16893
+ if (pinnedIdx >= startRow && pinnedIdx < endRow) visibleFlat.splice(pinnedIdx - startRow, 1);
16894
+ visibleFlat.unshift({
16895
+ entry: this._flat[pinnedIdx],
16896
+ absIdx: pinnedIdx
16897
+ });
16898
+ }
16783
16899
  const showCheckbox = this.interactionMode === `checked`;
16784
16900
  return html`
16785
- <div class="grid">
16901
+ <div class="grid" role="grid" aria-rowcount=${this._flat.length + 1} aria-colcount=${this._visibleColumns.length} aria-multiselectable=${this.selectionMode === `multiple` ? `true` : `false`}>
16786
16902
  <div class="header-bar">
16787
- <div class="header-scroll scroll-fade-x">
16788
- ${showCheckbox ? html`<div class="header-cell checkbox-col"></div>` : nothing}
16903
+ <div class="header-scroll scroll-fade-x" role="row" aria-rowindex="1">
16904
+ ${showCheckbox ? html`<div class="header-cell checkbox-col" role="columnheader"></div>` : nothing}
16789
16905
  ${repeat(this._visibleColumns, (c) => c.key, (col, idx) => html`
16790
- <div class="header-cell ${this._animatingColKeys.has(col.key) ? `width-animating` : ``}" style="width:${col.width ?? 150}px" ?drag-source=${this._drag?.active && this._drag.sourceCol.key === col.key}
16906
+ <div class="header-cell ${this._animatingColKeys.has(col.key) ? `width-animating` : ``}" role="columnheader" aria-sort=${this.sortState?.key === col.key ? this.sortState.direction === `asc` ? `ascending` : `descending` : `none`} style="width:${col.width ?? 150}px" ?drag-source=${this._drag?.active && this._drag.sourceCol.key === col.key}
16791
16907
  @click=${(e) => this._onHeaderClick(col, e)}
16792
16908
  @pointerenter=${() => this._tickled.handlePointerEnter(col.key)}>
16793
16909
  ${col.reorderable !== false ? html`<span class="reorder-handle" draggable="false"
@@ -16810,7 +16926,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16810
16926
  return last >= 0 ? this._colOffsets[last] + (this._visibleColumns[last].width ?? 150) : 0;
16811
16927
  })()}px"></div>` : nothing}
16812
16928
  </div>
16813
- ${this.columnMenu === `none` ? nothing : html`
16929
+ ${this.columnMenu !== `auto` ? nothing : html`
16814
16930
  <ixfx-menu-trigger label="⚙" placement="bottom-end" @menu-command=${(e) => {
16815
16931
  const cmd = e.detail.command;
16816
16932
  const checked = e.detail.checked;
@@ -16854,12 +16970,11 @@ let DataGridElement = class DataGridElement extends LitElement {
16854
16970
  </div>
16855
16971
  <div class="viewport scroll-fade-y" @scroll=${this._onScroll}>
16856
16972
  <div class="spacer" style="height:${this._flat.length * rh}px; width:${this._colWidthSum}px">
16857
- ${repeat(visibleFlat, (f) => f.key, (entry, i) => {
16858
- const absIdx = startRow + i;
16973
+ ${repeat(visibleFlat, (f) => f.entry.key, ({ entry, absIdx }) => {
16859
16974
  if (entry.type === `group`) {
16860
16975
  const gKey = entry.key.replace(/^__group__/, ``);
16861
16976
  const collapsed = this._collapsedGroups.has(gKey);
16862
- return html`<div class="group-row ${collapsed ? `collapsed` : ``}" style="top:${absIdx * rh}px; height:${rh}px" @click=${() => this._toggleGroup(gKey)}><span class="caret"><ixfx-icon name="chevron-down" size="12"></ixfx-icon></span><span>${entry.label}</span><span class="count">(${entry.count})</span><span class="divider"></span></div>`;
16977
+ return html`<div class="group-row ${collapsed ? `collapsed` : ``}" role="row" aria-rowindex=${absIdx + 2} style="top:${absIdx * rh}px; height:${rh}px" @click=${() => this._toggleGroup(gKey)}><span class="caret"><ixfx-icon name="chevron-down" size="12"></ixfx-icon></span><span role="rowheader" aria-expanded=${collapsed ? `false` : `true`}>${entry.label}</span><span class="count">(${entry.count})</span><span class="divider"></span></div>`;
16863
16978
  }
16864
16979
  const row = entry.row;
16865
16980
  const absIdx2 = entry.idx;
@@ -16867,7 +16982,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16867
16982
  const selected = this._selectedKeys.has(rowKey);
16868
16983
  const tickled = this._tickled.isTickled(rowKey);
16869
16984
  return html`
16870
- <div class="row item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${this.zebraStripes && absIdx2 % 2 === 1 ? `zebra-odd` : ``} ${this.#enteringRowKeys.has(rowKey) ? `entering` : ``} ${this.#nativeDragging && rowKey === this.#dragOriginKey ? `dragging-origin` : ``}" style="top:${absIdx * rh}px; height:${rh}px"
16985
+ <div class="row item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${this.zebraStripes && absIdx2 % 2 === 1 ? `zebra-odd` : ``} ${this.#enteringRowKeys.has(rowKey) ? `entering` : ``} ${this.#nativeDragging && rowKey === this.#dragOriginKey ? `dragging-origin` : ``}" role="row" aria-rowindex=${absIdx + 2} aria-selected=${selected ? `true` : `false`} style="top:${absIdx * rh}px; height:${rh}px"
16871
16986
  data-key=${rowKey}
16872
16987
  draggable=${this.#nativeDrag.draggableAttr}
16873
16988
  @click=${(e) => {
@@ -16896,7 +17011,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16896
17011
  const immediate = this.editability === `immediate` || this._editingRow === rowKey;
16897
17012
  const curVal = immediate ? val : this._editValue ?? val;
16898
17013
  const effectiveDataType = col.dataType ?? `text`;
16899
- if (effectiveDataType === `boolean` && (col.editor === `auto` || col.editor === `select` || !col.editor)) return html`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px;justify-content:center;align-items:center"><input type="checkbox" .checked=${Boolean(curVal)} @change=${(e) => {
17014
+ if (effectiveDataType === `boolean` && (col.editor === `auto` || col.editor === `select` || !col.editor)) return html`<div class="cell" role="gridcell" data-col=${col.key} style="width:${col.width ?? 150}px;justify-content:center;align-items:center"><input type="checkbox" .checked=${Boolean(curVal)} @change=${(e) => {
16900
17015
  const v = e.target.checked;
16901
17016
  if (immediate) this._commitImmediate(rowKey, col.key, v);
16902
17017
  else {
@@ -16906,7 +17021,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16906
17021
  }} /></div>`;
16907
17022
  if (col.editor === `select`) {
16908
17023
  const opts = col.editorOptions ?? [];
16909
- return html`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px"><select @change=${(e) => {
17024
+ return html`<div class="cell" role="gridcell" data-col=${col.key} style="width:${col.width ?? 150}px"><select @change=${(e) => {
16910
17025
  const v = e.target.value;
16911
17026
  if (immediate) this._commitImmediate(rowKey, col.key, v);
16912
17027
  else {
@@ -16952,11 +17067,11 @@ let DataGridElement = class DataGridElement extends LitElement {
16952
17067
  this._editingRow = null;
16953
17068
  }
16954
17069
  };
16955
- return html`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px">${factory(ctx)}</div>`;
17070
+ return html`<div class="cell" role="gridcell" data-col=${col.key} style="width:${col.width ?? 150}px">${factory(ctx)}</div>`;
16956
17071
  }
16957
17072
  const inputType = effectiveDataType === `number` ? `number` : `text`;
16958
17073
  const inputValue = effectiveDataType === `number` ? String(curVal ?? ``) : String(curVal ?? ``);
16959
- return html`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px"><input type=${inputType} .value=${inputValue} @input=${(e) => this._editValue = e.target.value} @keydown=${(e) => {
17074
+ return html`<div class="cell" role="gridcell" data-col=${col.key} style="width:${col.width ?? 150}px"><input type=${inputType} .value=${inputValue} @input=${(e) => this._editValue = e.target.value} @keydown=${(e) => {
16960
17075
  if (e.key === `Enter`) this._commitEdit();
16961
17076
  if (e.key === `Escape`) {
16962
17077
  this._editing = null;
@@ -16980,7 +17095,7 @@ let DataGridElement = class DataGridElement extends LitElement {
16980
17095
  const canPan = this.cellOverflow === `pan-text` && !col.renderer && (selected || tickled);
16981
17096
  const look = this._cellLook(row, col.key);
16982
17097
  const cellStyle = look.textColor === void 0 ? `` : `color:${look.textColor};`;
16983
- return html`<div class="cell ${textEditable ? `editable-text` : ``} ${this._animatingColKeys.has(col.key) ? `width-animating` : ``}" data-col=${col.key} style="width:${col.width ?? 150}px; justify-content:${col.align === `right` ? `flex-end` : col.align === `center` ? `center` : `flex-start`}; ${indent} ${cellStyle}"
17098
+ return html`<div class="cell ${textEditable ? `editable-text` : ``} ${this._animatingColKeys.has(col.key) ? `width-animating` : ``}" role="gridcell" data-col=${col.key} style="width:${col.width ?? 150}px; justify-content:${col.align === `right` ? `flex-end` : col.align === `center` ? `center` : `flex-start`}; ${indent} ${cellStyle}"
16984
17099
  @click=${(e) => {
16985
17100
  this._lastClickedCell = {
16986
17101
  rowKey,
@@ -17095,8 +17210,6 @@ __decorate([property({
17095
17210
  __decorate([state()], DataGridElement.prototype, "_resizingColKey", void 0);
17096
17211
  __decorate([state()], DataGridElement.prototype, "_animatingColKeys", void 0);
17097
17212
  __decorate([state()], DataGridElement.prototype, "_selectedKeys", void 0);
17098
- __decorate([state()], DataGridElement.prototype, "_scrollTop", void 0);
17099
- __decorate([state()], DataGridElement.prototype, "_scrollLeft", void 0);
17100
17213
  __decorate([state()], DataGridElement.prototype, "_viewportH", void 0);
17101
17214
  __decorate([state()], DataGridElement.prototype, "_viewportW", void 0);
17102
17215
  __decorate([state()], DataGridElement.prototype, "_collapsedGroups", void 0);
@@ -17374,6 +17487,123 @@ __decorate([state()], DataTableElement.prototype, "_detailIndex", void 0);
17374
17487
  __decorate([state()], DataTableElement.prototype, "_stacked", void 0);
17375
17488
  DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableElement);
17376
17489
  //#endregion
17490
+ //#region src/detail-list/detail-list-geometry.ts
17491
+ const EMPTY_GEOMETRY = {
17492
+ columns: [],
17493
+ positions: /* @__PURE__ */ new Map(),
17494
+ tops: /* @__PURE__ */ new Map(),
17495
+ lefts: /* @__PURE__ */ new Map()
17496
+ };
17497
+ function contentOrigin(scroller) {
17498
+ const r = scroller.getBoundingClientRect();
17499
+ return {
17500
+ left: r.left - scroller.scrollLeft,
17501
+ top: r.top - scroller.scrollTop
17502
+ };
17503
+ }
17504
+ /** Items closer than this (px) horizontally share a column (sub-pixel layout). */
17505
+ const COLUMN_TOLERANCE_PX = 1;
17506
+ /**
17507
+ * Measure `items` (one rect read each — a single layout flush) and bucket them
17508
+ * into columns. Zero-sized items (e.g. `display:none`) are skipped.
17509
+ */
17510
+ function measureGeometry(items, origin) {
17511
+ const measured = [];
17512
+ for (const el of items) {
17513
+ const r = el.getBoundingClientRect();
17514
+ if (r.width === 0 && r.height === 0) continue;
17515
+ measured.push({
17516
+ el,
17517
+ left: r.left - origin.left,
17518
+ top: r.top - origin.top,
17519
+ width: r.width
17520
+ });
17521
+ }
17522
+ measured.sort((a, b) => a.left - b.left || a.top - b.top);
17523
+ const columns = [];
17524
+ const positions = /* @__PURE__ */ new Map();
17525
+ const tops = /* @__PURE__ */ new Map();
17526
+ const lefts = /* @__PURE__ */ new Map();
17527
+ for (const m of measured) {
17528
+ let col = columns.at(-1);
17529
+ if (!col || m.left - col.left > COLUMN_TOLERANCE_PX) {
17530
+ col = {
17531
+ left: m.left,
17532
+ width: m.width,
17533
+ items: []
17534
+ };
17535
+ columns.push(col);
17536
+ }
17537
+ positions.set(m.el, {
17538
+ col: columns.length - 1,
17539
+ row: col.items.length
17540
+ });
17541
+ col.items.push(m.el);
17542
+ col.width = Math.max(col.width, m.width);
17543
+ tops.set(m.el, m.top);
17544
+ lefts.set(m.el, m.left);
17545
+ }
17546
+ return {
17547
+ columns,
17548
+ positions,
17549
+ tops,
17550
+ lefts
17551
+ };
17552
+ }
17553
+ /**
17554
+ * Whether `geometry` still matches layout, judged by re-reading a few probe
17555
+ * elements. Layout shifts that matter (column width, row height, reflow after
17556
+ * an insert) move at least one of first / cursor / last.
17557
+ */
17558
+ function geometryMatches(geometry, probes, origin) {
17559
+ for (const el of probes) {
17560
+ if (!el) continue;
17561
+ const left = geometry.lefts.get(el);
17562
+ const top = geometry.tops.get(el);
17563
+ if (left === void 0 || top === void 0) return false;
17564
+ const r = el.getBoundingClientRect();
17565
+ if (Math.abs(r.left - origin.left - left) > .5 || Math.abs(r.top - origin.top - top) > .5) return false;
17566
+ }
17567
+ return true;
17568
+ }
17569
+ /** First index in `[lo, hi)` for which `pred` holds, given `pred` is monotonic (false… then true…). */
17570
+ function partitionPoint(lo, hi, pred) {
17571
+ while (lo < hi) {
17572
+ const mid = lo + hi >> 1;
17573
+ if (pred(mid)) hi = mid;
17574
+ else lo = mid + 1;
17575
+ }
17576
+ return lo;
17577
+ }
17578
+ /**
17579
+ * Items (in DOM order) whose box intersects the content-space span
17580
+ * `[start, end)`, found with O(log n) layout reads — no full measure.
17581
+ * Relies on column flow: DOM order is column order, so `left` only moves in
17582
+ * one direction along the list (increasing for LTR, decreasing for RTL).
17583
+ */
17584
+ function itemsInSpanByOrder(items, origin, start, end) {
17585
+ if (items.length === 0) return [];
17586
+ const box = (i) => {
17587
+ const r = items[i].getBoundingClientRect();
17588
+ return {
17589
+ left: r.left - origin.left,
17590
+ right: r.right - origin.left
17591
+ };
17592
+ };
17593
+ const first = box(0);
17594
+ const last = box(items.length - 1);
17595
+ if (first.left === last.left) return first.right > start && first.left < end ? items : [];
17596
+ const n = items.length;
17597
+ if (last.left > first.left) {
17598
+ const from = partitionPoint(0, n, (i) => box(i).right > start);
17599
+ const to = partitionPoint(from, n, (i) => box(i).left >= end);
17600
+ return items.slice(from, to);
17601
+ }
17602
+ const from = partitionPoint(0, n, (i) => box(i).left < end);
17603
+ const to = partitionPoint(from, n, (i) => box(i).right <= start);
17604
+ return items.slice(from, to);
17605
+ }
17606
+ //#endregion
17377
17607
  //#region src/list-group-header/list-group-header.ts
17378
17608
  let ListGroupHeaderElement = class ListGroupHeaderElement extends LitElement {
17379
17609
  constructor(..._args) {
@@ -17534,6 +17764,50 @@ function sameKeys$3(a, b) {
17534
17764
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
17535
17765
  return true;
17536
17766
  }
17767
+ function sameMembers(a, b) {
17768
+ if (a.size !== b.size) return false;
17769
+ for (const el of a) if (!b.has(el)) return false;
17770
+ return true;
17771
+ }
17772
+ /** Remove `attr` from elements leaving the set, add it to elements entering it. */
17773
+ function applySetDiff(prev, next, attr) {
17774
+ for (const el of prev) if (!next.has(el)) el.removeAttribute(attr);
17775
+ for (const el of next) if (!prev.has(el)) el.setAttribute(attr, ``);
17776
+ }
17777
+ /** Attributes on an item that change list membership, visibility, keys or selectability. */
17778
+ const ITEM_CACHE_ATTRS = [
17779
+ `hidden`,
17780
+ GROUP_COLLAPSED_ATTR,
17781
+ `not-checked`,
17782
+ `data-key`,
17783
+ `data-value`
17784
+ ];
17785
+ /** Whether a mutation record can change the item caches (direct children / their cache attributes). */
17786
+ function affectsItemCaches(host, record) {
17787
+ if (record.type === `childList`) return record.target === host;
17788
+ return record.target.parentElement === host;
17789
+ }
17790
+ /**
17791
+ * Make `parent`'s managed children equal `nodes`, in order, with the fewest
17792
+ * DOM moves — a single insert stays a single move instead of re-appending
17793
+ * every row. `nodes` must contain every managed child; unmanaged children
17794
+ * (text, foreign elements) are left where they are.
17795
+ */
17796
+ function syncChildOrder(parent, nodes, managed) {
17797
+ const nextManaged = (from) => {
17798
+ let el = from;
17799
+ while (el && !managed(el)) el = el.nextElementSibling;
17800
+ return el;
17801
+ };
17802
+ let cursor = nextManaged(parent.firstElementChild);
17803
+ for (const node of nodes) {
17804
+ if (node === cursor) {
17805
+ cursor = nextManaged(cursor.nextElementSibling);
17806
+ continue;
17807
+ }
17808
+ parent.insertBefore(node, cursor);
17809
+ }
17810
+ }
17537
17811
  let DetailListElement = class DetailListElement extends LitElement {
17538
17812
  constructor(..._args) {
17539
17813
  super(..._args);
@@ -17554,10 +17828,15 @@ let DetailListElement = class DetailListElement extends LitElement {
17554
17828
  this.crossGroupSelection = false;
17555
17829
  this._collapsedGroups = /* @__PURE__ */ new Set();
17556
17830
  this.#itemData = /* @__PURE__ */ new WeakMap();
17557
- this.#grid = [];
17558
- this.#positions = /* @__PURE__ */ new Map();
17559
- this.#tops = /* @__PURE__ */ new Map();
17831
+ this.#itemIndex = /* @__PURE__ */ new WeakMap();
17832
+ this.#geometry = EMPTY_GEOMETRY;
17833
+ this.#geometryDirty = true;
17560
17834
  this.#geometryScheduled = false;
17835
+ this.#appliedSelected = /* @__PURE__ */ new Set();
17836
+ this.#appliedChecked = /* @__PURE__ */ new Set();
17837
+ this.#itemStatesFullSync = true;
17838
+ this.#hasCheckboxes = false;
17839
+ this.#draggableApplied = false;
17561
17840
  this.#sortRaf = 0;
17562
17841
  this.#viewportScrollRaf = 0;
17563
17842
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
@@ -17596,10 +17875,14 @@ let DetailListElement = class DetailListElement extends LitElement {
17596
17875
  }
17597
17876
  });
17598
17877
  this.tickled = new TickledController(this, {
17599
- getItems: () => this._getVisibleItems().map((el) => ({
17600
- id: el,
17601
- enabled: !el.hasAttribute(`not-checked`)
17602
- })),
17878
+ getItems: () => {
17879
+ const visible = this._getVisibleItems();
17880
+ this.#tickledItemsCache ??= visible.map((el) => ({
17881
+ id: el,
17882
+ enabled: !el.hasAttribute(`not-checked`)
17883
+ }));
17884
+ return this.#tickledItemsCache;
17885
+ },
17603
17886
  onTickle: (id) => {
17604
17887
  if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
17605
17888
  },
@@ -17703,7 +17986,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17703
17986
  }
17704
17987
  return;
17705
17988
  }
17706
- if (NAV_KEYS.has(e.key)) this.setAttribute(`data-keyboard-nav`, ``);
17989
+ if (NAV_KEYS.has(e.key) && !this.hasAttribute(`data-keyboard-nav`)) this.setAttribute(`data-keyboard-nav`, ``);
17707
17990
  switch (e.key) {
17708
17991
  case `ArrowDown`:
17709
17992
  e.preventDefault();
@@ -17782,11 +18065,32 @@ let DetailListElement = class DetailListElement extends LitElement {
17782
18065
  };
17783
18066
  }
17784
18067
  #itemData;
17785
- #grid;
17786
- #positions;
17787
- /** Viewport-relative `top` per item at the last geometry pass — lines rows up across columns. */
17788
- #tops;
18068
+ /** The `index` each object item was formatted with — reuse is only safe at the same index. */
18069
+ #itemIndex;
18070
+ /** Measured column grid (content coordinates). See `#ensureGeometry`. */
18071
+ #geometry;
18072
+ /** Layout may have changed since `#geometry` was measured. */
18073
+ #geometryDirty;
17789
18074
  #geometryScheduled;
18075
+ /**
18076
+ * Watches child / visibility / key changes so the item caches below can be
18077
+ * validated synchronously (via `takeRecords()`) on every read — they are
18078
+ * never stale, even within the same task as the mutation.
18079
+ */
18080
+ #cacheObserver;
18081
+ #allItemsCache;
18082
+ #visibleCache;
18083
+ #visibleCachePredicate;
18084
+ #tickledItemsCache;
18085
+ #renderedKeysCache;
18086
+ #keyIndexCache;
18087
+ /** Item states as last written to the DOM, so `_syncItemStates` can apply just the diff. */
18088
+ #appliedTickled;
18089
+ #appliedSelected;
18090
+ #appliedChecked;
18091
+ #itemStatesFullSync;
18092
+ #hasCheckboxes;
18093
+ #draggableApplied;
17790
18094
  #sortRaf;
17791
18095
  #viewportScrollRaf;
17792
18096
  /** Key sets as of the last viewport-change dispatch, for dedupe. */
@@ -17840,6 +18144,8 @@ let DetailListElement = class DetailListElement extends LitElement {
17840
18144
  }
17841
18145
  _syncNativeDraggable() {
17842
18146
  if (!this.isConnected) return;
18147
+ if (!this.itemsDraggable && !this.#draggableApplied) return;
18148
+ this.#draggableApplied = this.itemsDraggable;
17843
18149
  for (const el of this.querySelectorAll(`li`)) if (el instanceof HTMLElement) el.draggable = this.itemsDraggable && this._isCheckable(el);
17844
18150
  }
17845
18151
  #startPostDragSuppressTimer() {
@@ -17921,7 +18227,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17921
18227
  this.#resizeObserver = new ResizeObserver(() => this.#scheduleGeometry());
17922
18228
  this.#resizeObserver.observe(this);
17923
18229
  this.#mutationObserver = new MutationObserver((records) => {
17924
- this.#processChildren();
18230
+ this.#processAddedChildren(records);
17925
18231
  this.#animateChildMutations(records);
17926
18232
  this.#scheduleGeometry();
17927
18233
  this.#scheduleGroups();
@@ -17932,6 +18238,16 @@ let DetailListElement = class DetailListElement extends LitElement {
17932
18238
  attributeFilter: [`hidden`]
17933
18239
  });
17934
18240
  for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#animatedItems.add(el);
18241
+ this.#cacheObserver = new MutationObserver((records) => {
18242
+ if (records.some((r) => affectsItemCaches(this, r))) this.#dropItemCaches();
18243
+ });
18244
+ this.#cacheObserver.observe(this, {
18245
+ childList: true,
18246
+ subtree: true,
18247
+ attributes: true,
18248
+ attributeFilter: ITEM_CACHE_ATTRS
18249
+ });
18250
+ this.#dropItemCaches();
17935
18251
  this.#searchRegistry ??= createCommandRegistry();
17936
18252
  this.#syncSearchCommands();
17937
18253
  this.#scheduleGroups();
@@ -17951,7 +18267,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17951
18267
  fresh.push(node);
17952
18268
  }
17953
18269
  if (fresh.length === 0) return;
17954
- const total = this.querySelectorAll(ITEM_SELECTOR$3).length;
18270
+ const total = this.#allItems().length;
17955
18271
  if (fresh.length >= total) return;
17956
18272
  for (const el of fresh) animateListItemIn(el);
17957
18273
  window.setTimeout(() => this.#scheduleGeometry(), 230);
@@ -17978,6 +18294,9 @@ let DetailListElement = class DetailListElement extends LitElement {
17978
18294
  this.#mutationObserver = void 0;
17979
18295
  this.#groupObserver?.disconnect();
17980
18296
  this.#groupObserver = void 0;
18297
+ this.#cacheObserver?.disconnect();
18298
+ this.#cacheObserver = void 0;
18299
+ this.#dropItemCaches();
17981
18300
  this.#unregisterSearchCommands?.();
17982
18301
  this.#unregisterSearchCommands = void 0;
17983
18302
  this.#searchRegistry = void 0;
@@ -17990,6 +18309,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17990
18309
  this.#processChildren();
17991
18310
  if (this.filterPredicate) this.#applyFilterPredicate();
17992
18311
  this.#recomputeGeometry();
18312
+ this.#scheduleViewportChangeEmit();
17993
18313
  this._syncNativeDraggable();
17994
18314
  }
17995
18315
  willUpdate(changed) {
@@ -18018,7 +18338,8 @@ let DetailListElement = class DetailListElement extends LitElement {
18018
18338
  });
18019
18339
  return;
18020
18340
  }
18021
- if ((changed.has(`items`) || changed.has(`displayProperty`) || changed.has(`formatter`) || changed.has(`iconProperty`) || changed.has(`iconColorProperty`) || changed.has(`textColorProperty`) || changed.has(`badgeProperty`) || changed.has(`keyProperty`)) && this.provider === void 0 && this.items.length > 0) this.#rebuildFromObjects(this.items);
18341
+ const shapeChanged = changed.has(`displayProperty`) || changed.has(`formatter`) || changed.has(`iconProperty`) || changed.has(`iconColorProperty`) || changed.has(`textColorProperty`) || changed.has(`badgeProperty`) || changed.has(`keyProperty`);
18342
+ if ((changed.has(`items`) || shapeChanged) && this.provider === void 0 && this.items.length > 0) this.#rebuildFromObjects(this.items, !shapeChanged);
18022
18343
  this.#syncFromProvider();
18023
18344
  if (changed.has(`sort`) || changed.has(`sortBy`) || changed.has(`sortComparator`)) this.#applySort();
18024
18345
  if (changed.has(`filterPredicate`)) {
@@ -18028,6 +18349,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18028
18349
  }
18029
18350
  if (changed.has(`interactionMode`)) {
18030
18351
  this.selection.clearAnchor();
18352
+ this.#itemStatesFullSync = true;
18031
18353
  this._syncItemStates();
18032
18354
  this._syncNativeDraggable();
18033
18355
  } else if (changed.has(`checkboxRenderer`) && this.interactionMode === `checked`) this._syncCheckboxes();
@@ -18051,6 +18373,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18051
18373
  */
18052
18374
  refreshLayout() {
18053
18375
  this.#recomputeGeometry();
18376
+ this.#scheduleViewportChangeEmit();
18054
18377
  }
18055
18378
  addItem(item, options) {
18056
18379
  let el;
@@ -18076,7 +18399,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18076
18399
  item.remove();
18077
18400
  this.#scheduleGroups();
18078
18401
  if (this.interactionMode === `checked`) this._syncCheckboxes();
18079
- this.#recomputeGeometry();
18402
+ this.#scheduleGeometry();
18080
18403
  });
18081
18404
  }
18082
18405
  /** {@link removeItem} for several items at once. */
@@ -18125,13 +18448,12 @@ let DetailListElement = class DetailListElement extends LitElement {
18125
18448
  visibleKeys() {
18126
18449
  const container = this._listEl;
18127
18450
  if (!container) return [];
18128
- const box = container.getBoundingClientRect();
18451
+ const start = container.scrollLeft;
18452
+ const end = start + container.getBoundingClientRect().width;
18129
18453
  const out = [];
18130
- for (const el of this._getVisibleItems()) {
18454
+ for (const el of itemsInSpanByOrder(this._getVisibleItems(), contentOrigin(container), start, end)) {
18131
18455
  const key = keyForElement(el);
18132
- if (key === void 0) continue;
18133
- const r = el.getBoundingClientRect();
18134
- if (r.right > box.left && r.left < box.right) out.push(key);
18456
+ if (key !== void 0) out.push(key);
18135
18457
  }
18136
18458
  return out;
18137
18459
  }
@@ -18190,7 +18512,15 @@ let DetailListElement = class DetailListElement extends LitElement {
18190
18512
  });
18191
18513
  }
18192
18514
  #itemForKey(key) {
18193
- for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) if (keyForElement(el) === key) return el;
18515
+ if (!this.#keyIndexCache) {
18516
+ const index = /* @__PURE__ */ new Map();
18517
+ for (const el of this.#allItems()) {
18518
+ const k = keyForElement(el);
18519
+ if (k !== void 0 && !index.has(k)) index.set(k, el);
18520
+ }
18521
+ this.#keyIndexCache = index;
18522
+ }
18523
+ return this.#keyIndexCache.get(key);
18194
18524
  }
18195
18525
  *selectedFilter(kind) {
18196
18526
  const wanted = (el) => {
@@ -18205,11 +18535,12 @@ let DetailListElement = class DetailListElement extends LitElement {
18205
18535
  if (wanted(el)) yield el;
18206
18536
  }
18207
18537
  }
18208
- #rebuildFromObjects(items) {
18538
+ #rebuildFromObjects(items, reuseRows = false) {
18209
18539
  const keyOf = (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? null;
18210
18540
  const previousKeys = new Set([...this.selection.selected].map(keyOf).filter((k) => k !== null));
18211
18541
  const anchorKey = this.selection.anchor ? keyOf(this.selection.anchor) : null;
18212
- this.replaceChildren(...items.map((item, index) => this.#createItemEl(item, index)));
18542
+ if (reuseRows) this.#reconcileObjectRows(items);
18543
+ else this.replaceChildren(...items.map((item, index) => this.#createItemEl(item, index)));
18213
18544
  if (previousKeys.size > 0 || anchorKey !== null) {
18214
18545
  const next = [];
18215
18546
  let anchor;
@@ -18226,9 +18557,38 @@ let DetailListElement = class DetailListElement extends LitElement {
18226
18557
  this.#scheduleGeometry();
18227
18558
  this.#scheduleGroups();
18228
18559
  }
18560
+ /**
18561
+ * Update the rows to match `items`, keeping the `<li>` of every object that
18562
+ * is still present (by identity) instead of rebuilding all of them — so
18563
+ * `items = [...items, next]` creates one row, not thousands. A row built by
18564
+ * a `formatter` is only kept at the same index, since the formatter receives
18565
+ * the index.
18566
+ */
18567
+ #reconcileObjectRows(items) {
18568
+ const reusable = /* @__PURE__ */ new Map();
18569
+ for (const li of this.#allItems()) if (this.#itemData.has(li) && !reusable.has(this.#itemData.get(li))) reusable.set(this.#itemData.get(li), li);
18570
+ const next = items.map((item, index) => {
18571
+ const li = reusable.get(item);
18572
+ if (li && (!this.formatter || this.#itemIndex.get(li) === index)) {
18573
+ reusable.delete(item);
18574
+ return li;
18575
+ }
18576
+ return this.#createItemEl(item, index);
18577
+ });
18578
+ const keep = new Set(next);
18579
+ for (const li of this.#allItems()) if (!keep.has(li)) li.remove();
18580
+ if (this.#groupingActive()) {
18581
+ for (const li of next) this.#seq.set(li, this.#seqNext++);
18582
+ this.append(...next.filter((li) => li.parentElement !== this));
18583
+ this.#scheduleGroups();
18584
+ return;
18585
+ }
18586
+ syncChildOrder(this, next, (el) => el.localName === `li`);
18587
+ }
18229
18588
  #createItemEl(item, index) {
18230
18589
  const li = document.createElement(`li`);
18231
18590
  this.#itemData.set(li, item);
18591
+ this.#itemIndex.set(li, index);
18232
18592
  const content = this.formatter ? this.formatter(item, index) : this.displayProperty ? this.#propertyValue(item, this.displayProperty) : item;
18233
18593
  const textColor = this.#colorValue(item, this.textColorProperty);
18234
18594
  const iconColor = this.#colorValue(item, this.iconColorProperty);
@@ -18299,42 +18659,40 @@ let DetailListElement = class DetailListElement extends LitElement {
18299
18659
  this.#scheduleGroups();
18300
18660
  return;
18301
18661
  }
18302
- for (const el of this.#sortRows(rows)) this.appendChild(el);
18662
+ syncChildOrder(this, this.#sortRows(rows), (el) => el.localName === `li`);
18303
18663
  this.#scheduleGeometry();
18304
18664
  this.#scheduleGroups();
18305
18665
  }
18306
18666
  /** Stable sort of `rows` by the current `sort` / `sortBy` / `sortComparator`. */
18307
18667
  #sortRows(rows) {
18308
18668
  const dir = this.sort === `descending` ? -1 : 1;
18309
- const meta = (el) => ({
18669
+ const metas = rows.map((el) => ({
18310
18670
  element: el,
18311
18671
  data: this.getItemData(el),
18312
18672
  label: el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``
18313
- });
18673
+ }));
18674
+ const collator = new Intl.Collator(void 0, { numeric: true });
18314
18675
  let cmp;
18315
18676
  if (this.sortComparator) {
18316
18677
  const user = this.sortComparator;
18317
- cmp = (a, b) => user(meta(a), meta(b));
18678
+ cmp = (a, b) => user(metas[a], metas[b]);
18318
18679
  } else if (this.sortBy) {
18319
18680
  const prop = this.sortBy;
18320
- const key = (el) => {
18321
- const v = this.#propertyValue(this.getItemData(el), prop);
18681
+ const keys = metas.map((m) => {
18682
+ const v = this.#propertyValue(m.data, prop);
18322
18683
  if (typeof v === `number`) return v;
18323
18684
  return v === void 0 || v === null ? `` : String(v);
18324
- };
18685
+ });
18325
18686
  cmp = (a, b) => {
18326
- const ka = key(a);
18327
- const kb = key(b);
18687
+ const ka = keys[a];
18688
+ const kb = keys[b];
18328
18689
  if (typeof ka === `number` && typeof kb === `number`) return ka - kb;
18329
- return String(ka).localeCompare(String(kb), void 0, { numeric: true });
18690
+ return collator.compare(String(ka), String(kb));
18330
18691
  };
18331
- } else cmp = (a, b) => meta(a).label.localeCompare(meta(b).label, void 0, { numeric: true });
18332
- const indexed = rows.map((el, i) => ({
18333
- el,
18334
- i
18335
- }));
18336
- indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
18337
- return indexed.map(({ el }) => el);
18692
+ } else cmp = (a, b) => collator.compare(metas[a].label, metas[b].label);
18693
+ const order = rows.map((_, i) => i);
18694
+ order.sort((a, b) => cmp(a, b) * dir || a - b);
18695
+ return order.map((i) => rows[i]);
18338
18696
  }
18339
18697
  #groupingActive() {
18340
18698
  return this.groupBy !== null && this.groupBy !== void 0 && this.groupBy !== ``;
@@ -18378,7 +18736,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18378
18736
  #syncGroups() {
18379
18737
  const active = this.#groupingActive();
18380
18738
  if (!active && !this.#wasGrouped) return;
18381
- const items = [...this.querySelectorAll(ITEM_SELECTOR$3)];
18739
+ const items = this.#allItems();
18382
18740
  for (const el of items) if (!this.#seq.has(el)) this.#seq.set(el, this.#seqNext++);
18383
18741
  let ordered = items.toSorted((a, b) => this.#seq.get(a) - this.#seq.get(b));
18384
18742
  if (this.sort !== `none` && ordered.length > 1) ordered = this.#sortRows(ordered);
@@ -18423,8 +18781,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18423
18781
  this.#groupIndex = index;
18424
18782
  this.#groupList = info;
18425
18783
  this.#wasGrouped = active;
18426
- const current = [...this.children].filter((c) => c.matches(`li`) || c.hasAttribute(GROUP_HEADER_ATTR));
18427
- if (current.length !== nodes.length || current.some((c, i) => c !== nodes[i])) this.append(...nodes);
18784
+ syncChildOrder(this, nodes, (c) => c.localName === `li` || c.hasAttribute(GROUP_HEADER_ATTR));
18428
18785
  if (this.tickled.tickledId?.hasAttribute(GROUP_COLLAPSED_ATTR)) this.tickled.focus(void 0);
18429
18786
  this.#syncGroupObserver(active);
18430
18787
  this.#scheduleGeometry();
@@ -18475,59 +18832,65 @@ let DetailListElement = class DetailListElement extends LitElement {
18475
18832
  if (target instanceof HTMLElement && target.parentElement === this && target.hasAttribute(GROUP_HEADER_ATTR)) return target;
18476
18833
  }
18477
18834
  }
18835
+ /**
18836
+ * Mark the layout dirty and, next frame, check for a viewport change.
18837
+ * Geometry moves (resize, filter, sort, DOM changes) can change the visible
18838
+ * set without a scroll, so every layout-changing path comes through here.
18839
+ * The grid itself is re-measured lazily, by whoever needs it next.
18840
+ */
18478
18841
  #scheduleGeometry() {
18842
+ this.#geometryDirty = true;
18479
18843
  if (this.#geometryScheduled) return;
18480
18844
  this.#geometryScheduled = true;
18481
18845
  requestAnimationFrame(() => {
18482
18846
  this.#geometryScheduled = false;
18483
- this.#recomputeGeometry();
18847
+ this.#emitViewportChangeIfChanged();
18484
18848
  });
18485
18849
  }
18850
+ /** Re-measure the column grid now (one layout read per visible item). */
18486
18851
  #recomputeGeometry() {
18487
- this.#grid = [];
18488
- this.#positions.clear();
18489
- this.#tops.clear();
18490
- if (!this.isConnected) return;
18491
- const columns = [];
18492
- for (const el of this._getVisibleItems()) {
18493
- const rect = el.getBoundingClientRect();
18494
- if (rect.width === 0 && rect.height === 0) continue;
18495
- this.#tops.set(el, rect.top);
18496
- let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
18497
- if (ci === -1) {
18498
- columns.push({
18499
- left: rect.left,
18500
- items: []
18501
- });
18502
- ci = columns.length - 1;
18503
- }
18504
- columns[ci].items.push(el);
18852
+ this.#geometryDirty = false;
18853
+ this.#geometry = this.isConnected && this._listEl ? measureGeometry(this._getVisibleItems(), contentOrigin(this._listEl)) : EMPTY_GEOMETRY;
18854
+ }
18855
+ /**
18856
+ * The column grid, re-measured only if it's known dirty or a few probe items
18857
+ * have moved (catches layout changes nothing told us about, e.g. a CSS
18858
+ * variable change). Cheap enough to call per keypress / scroll frame.
18859
+ */
18860
+ #ensureGeometry() {
18861
+ const visible = this._getVisibleItems();
18862
+ if (this.#geometryDirty || !this._listEl) {
18863
+ this.#recomputeGeometry();
18864
+ return this.#geometry;
18505
18865
  }
18506
- columns.sort((a, b) => a.left - b.left);
18507
- for (const col of columns) col.items.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18508
- this.#grid = columns.map((c) => c.items);
18509
- this.#grid.forEach((col, colIdx) => col.forEach((el, rowIdx) => this.#positions.set(el, {
18510
- col: colIdx,
18511
- row: rowIdx
18512
- })));
18513
- this.#scheduleViewportChangeEmit();
18866
+ const cursor = this.tickled.tickledId;
18867
+ const probes = [
18868
+ visible[0],
18869
+ cursor && this.#geometry.positions.has(cursor) ? cursor : void 0,
18870
+ visible.at(-1)
18871
+ ];
18872
+ if (!geometryMatches(this.#geometry, probes, contentOrigin(this._listEl))) this.#recomputeGeometry();
18873
+ return this.#geometry;
18514
18874
  }
18515
18875
  #positionOf(el) {
18516
- let pos = this.#positions.get(el);
18876
+ let pos = this.#geometry.positions.get(el);
18517
18877
  if (pos === void 0) {
18518
18878
  this.#recomputeGeometry();
18519
- pos = this.#positions.get(el);
18879
+ pos = this.#geometry.positions.get(el);
18520
18880
  }
18521
18881
  return pos;
18522
18882
  }
18883
+ #column(col) {
18884
+ return this.#geometry.columns[col]?.items;
18885
+ }
18523
18886
  #itemAt(col, row) {
18524
- return this.#grid[col]?.[row];
18887
+ return this.#column(col)?.[row];
18525
18888
  }
18526
18889
  #gridSize() {
18527
- if (this.#grid.length === 0) this.#recomputeGeometry();
18890
+ const { columns } = this.#ensureGeometry();
18528
18891
  return {
18529
- cols: this.#grid.length,
18530
- rows: Math.max(0, ...this.#grid.map((c) => c.length))
18892
+ cols: columns.length,
18893
+ rows: Math.max(0, ...columns.map((c) => c.items.length))
18531
18894
  };
18532
18895
  }
18533
18896
  #navigateTo(target) {
@@ -18545,7 +18908,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18545
18908
  let row = deltaRow !== 0 ? fromRow + deltaRow : fromRow;
18546
18909
  let wrappedColumns = false;
18547
18910
  for (let iterations = 0; iterations < maxIterations; iterations++) {
18548
- const colLength = this.#grid[col]?.length ?? 0;
18911
+ const colLength = this.#column(col)?.length ?? 0;
18549
18912
  if (row < 0 || row >= colLength) {
18550
18913
  if (deltaCol !== 0 || wrappedColumns) {
18551
18914
  if (this.itemWrap === `wrap`) {
@@ -18567,15 +18930,15 @@ let DetailListElement = class DetailListElement extends LitElement {
18567
18930
  } else if (deltaRow < 0) {
18568
18931
  if (col - 1 >= 0) {
18569
18932
  col = col - 1;
18570
- row = (this.#grid[col]?.length ?? 1) - 1;
18933
+ row = (this.#column(col)?.length ?? 1) - 1;
18571
18934
  wrappedColumns = true;
18572
18935
  } else if (this.itemWrap === `wrap`) {
18573
18936
  col = cols - 1;
18574
- row = (this.#grid[col]?.length ?? 1) - 1;
18937
+ row = (this.#column(col)?.length ?? 1) - 1;
18575
18938
  wrappedColumns = true;
18576
18939
  } else return;
18577
18940
  } else if (deltaCol !== 0) {
18578
- const targetColLength = this.#grid[col]?.length ?? 0;
18941
+ const targetColLength = this.#column(col)?.length ?? 0;
18579
18942
  if (row >= targetColLength && targetColLength > 0) row = targetColLength - 1;
18580
18943
  }
18581
18944
  if (col === fromCol && row === fromRow + deltaRow) return;
@@ -18594,7 +18957,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18594
18957
  for (let i = visibleItems.length - 1; i >= 0; i--) if (this._isCheckable(visibleItems[i])) return visibleItems[i];
18595
18958
  }
18596
18959
  #navigateDown() {
18597
- this.#recomputeGeometry();
18960
+ this.#ensureGeometry();
18598
18961
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
18599
18962
  if (!cur) {
18600
18963
  this.#navigateTo(this.#findFirstCheckable());
@@ -18604,7 +18967,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18604
18967
  if (next) this.#navigateTo(next);
18605
18968
  }
18606
18969
  #navigateUp() {
18607
- this.#recomputeGeometry();
18970
+ this.#ensureGeometry();
18608
18971
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
18609
18972
  if (!cur) {
18610
18973
  this.#navigateTo(this.#findLastCheckable());
@@ -18621,15 +18984,15 @@ let DetailListElement = class DetailListElement extends LitElement {
18621
18984
  */
18622
18985
  #horizontalTarget(from, dir) {
18623
18986
  if (this.#groupList.length === 0) return this.#findNextCheckable(from.col, from.row, dir, 0);
18624
- const column = this.#grid[from.col + dir];
18987
+ const column = this.#column(from.col + dir);
18625
18988
  const current = this.tickled.tickledId;
18626
- const top = current ? this.#tops.get(current) : void 0;
18989
+ const top = current ? this.#geometry.tops.get(current) : void 0;
18627
18990
  if (!column || top === void 0) return void 0;
18628
18991
  let best;
18629
18992
  let bestDelta = Number.POSITIVE_INFINITY;
18630
18993
  for (const el of column) {
18631
18994
  if (!this._isCheckable(el)) continue;
18632
- const delta = Math.abs((this.#tops.get(el) ?? Number.POSITIVE_INFINITY) - top);
18995
+ const delta = Math.abs((this.#geometry.tops.get(el) ?? Number.POSITIVE_INFINITY) - top);
18633
18996
  if (delta < bestDelta) {
18634
18997
  best = el;
18635
18998
  bestDelta = delta;
@@ -18638,7 +19001,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18638
19001
  return best;
18639
19002
  }
18640
19003
  #navigateRight() {
18641
- this.#recomputeGeometry();
19004
+ this.#ensureGeometry();
18642
19005
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
18643
19006
  if (!cur) {
18644
19007
  this.#navigateTo(this.#findFirstCheckable());
@@ -18649,7 +19012,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18649
19012
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
18650
19013
  }
18651
19014
  #navigateLeft() {
18652
- this.#recomputeGeometry();
19015
+ this.#ensureGeometry();
18653
19016
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
18654
19017
  if (!cur) {
18655
19018
  this.#navigateTo(this.#findFirstCheckable());
@@ -18659,13 +19022,49 @@ let DetailListElement = class DetailListElement extends LitElement {
18659
19022
  if (next) this.#navigateTo(next);
18660
19023
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
18661
19024
  }
19025
+ /**
19026
+ * Visible items in DOM order. Cached — every per-hover / per-frame path
19027
+ * reads it — and validated on each call, so it is never stale.
19028
+ */
18662
19029
  _getVisibleItems() {
18663
- return Array.from(this.querySelectorAll(ITEM_SELECTOR$3)).filter((el) => {
19030
+ this.#validateItemCaches();
19031
+ if (this.#visibleCachePredicate !== this.filterPredicate) {
19032
+ this.#dropVisibleCaches();
19033
+ this.#visibleCachePredicate = this.filterPredicate;
19034
+ }
19035
+ this.#visibleCache ??= this.#allItems().filter((el) => {
18664
19036
  if (el.hidden) return false;
18665
19037
  if (el.hasAttribute(GROUP_COLLAPSED_ATTR)) return false;
18666
19038
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
18667
19039
  return true;
18668
19040
  });
19041
+ return this.#visibleCache;
19042
+ }
19043
+ /** Every item (`<li>` child), in DOM order. Cached like `_getVisibleItems`. */
19044
+ #allItems() {
19045
+ this.#validateItemCaches();
19046
+ this.#allItemsCache ??= [...this.querySelectorAll(ITEM_SELECTOR$3)];
19047
+ return this.#allItemsCache;
19048
+ }
19049
+ /** Drop the item caches if anything relevant changed since they were built. */
19050
+ #validateItemCaches() {
19051
+ if (!this.#cacheObserver) {
19052
+ this.#dropItemCaches();
19053
+ return;
19054
+ }
19055
+ if (this.#cacheObserver.takeRecords().some((r) => affectsItemCaches(this, r))) this.#dropItemCaches();
19056
+ }
19057
+ #dropItemCaches() {
19058
+ this.#allItemsCache = void 0;
19059
+ this.#keyIndexCache = void 0;
19060
+ this.#itemStatesFullSync = true;
19061
+ this.#dropVisibleCaches();
19062
+ }
19063
+ #dropVisibleCaches() {
19064
+ this.#visibleCache = void 0;
19065
+ this.#tickledItemsCache = void 0;
19066
+ this.#renderedKeysCache = void 0;
19067
+ this.#geometryDirty = true;
18669
19068
  }
18670
19069
  /**
18671
19070
  * Set the column width to fit the widest visible item.
@@ -18738,21 +19137,48 @@ let DetailListElement = class DetailListElement extends LitElement {
18738
19137
  }
18739
19138
  /** Scan light-DOM children for unprocessed `data-icon` items. */
18740
19139
  #processChildren() {
18741
- for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#ensureIcon(el);
19140
+ for (const el of this.#allItems()) this.#ensureIcon(el);
19141
+ }
19142
+ /** `#processChildren` for just the items a batch of mutations added. */
19143
+ #processAddedChildren(records) {
19144
+ for (const record of records) for (const node of record.addedNodes) if (node instanceof HTMLLIElement && node.parentElement === this) this.#ensureIcon(node);
18742
19145
  }
19146
+ /**
19147
+ * Reflect cursor / selection / checked state onto the items. After the item
19148
+ * set changes (or the interaction mode does) every item is visited; otherwise
19149
+ * only the items whose state changed are touched, so hovering is O(1).
19150
+ */
18743
19151
  _syncItemStates() {
18744
- for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) {
18745
- el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
18746
- el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
18747
- if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !el.hasAttribute(`not-checked`) && this.selection.selected.has(el));
18748
- else el.removeAttribute(`data-checked`);
19152
+ const selected = this.selection.selected;
19153
+ const checked = this.interactionMode === `checked` ? new Set([...selected].filter((el) => !el.hasAttribute(`not-checked`))) : /* @__PURE__ */ new Set();
19154
+ const tickled = this.tickled.tickledId;
19155
+ const selectionChanged = this.#itemStatesFullSync || !sameMembers(this.#appliedSelected, selected) || !sameMembers(this.#appliedChecked, checked);
19156
+ if (this.#itemStatesFullSync) {
19157
+ for (const el of this.#allItems()) {
19158
+ el.toggleAttribute(`data-tickled`, el === tickled);
19159
+ el.toggleAttribute(`data-selected`, selected.has(el));
19160
+ el.toggleAttribute(`data-checked`, checked.has(el));
19161
+ }
19162
+ this.#itemStatesFullSync = false;
19163
+ } else {
19164
+ if (tickled !== this.#appliedTickled) {
19165
+ this.#appliedTickled?.removeAttribute(`data-tickled`);
19166
+ tickled?.setAttribute(`data-tickled`, ``);
19167
+ }
19168
+ applySetDiff(this.#appliedSelected, selected, `data-selected`);
19169
+ applySetDiff(this.#appliedChecked, checked, `data-checked`);
18749
19170
  }
18750
- this.dataset.inputMode = this.tickled.inputMode;
18751
- this._syncCheckboxes();
19171
+ this.#appliedTickled = tickled;
19172
+ this.#appliedSelected = new Set(selected);
19173
+ this.#appliedChecked = checked;
19174
+ if (this.dataset.inputMode !== this.tickled.inputMode) this.dataset.inputMode = this.tickled.inputMode;
19175
+ if (selectionChanged) this._syncCheckboxes();
18752
19176
  }
18753
19177
  _syncCheckboxes() {
18754
19178
  const inCheckedMode = this.interactionMode === `checked`;
18755
19179
  const wantsCustom = typeof this.checkboxRenderer === `function`;
19180
+ if (!inCheckedMode && !this.#hasCheckboxes) return;
19181
+ this.#hasCheckboxes = inCheckedMode;
18756
19182
  for (const el of this.querySelectorAll(`li`)) {
18757
19183
  const existing = el.querySelector(`.ixfx-list-checkbox`);
18758
19184
  if (!inCheckedMode) {
@@ -18865,12 +19291,12 @@ let DetailListElement = class DetailListElement extends LitElement {
18865
19291
  * `ixfx-vertical-list.renderedKeys()` in its non-virtualized mode.
18866
19292
  */
18867
19293
  #renderedKeys() {
18868
- const out = [];
18869
- for (const el of this._getVisibleItems()) {
19294
+ const visible = this._getVisibleItems();
19295
+ this.#renderedKeysCache ??= visible.flatMap((el) => {
18870
19296
  const key = keyForElement(el);
18871
- if (key !== void 0) out.push(key);
18872
- }
18873
- return out;
19297
+ return key === void 0 ? [] : [key];
19298
+ });
19299
+ return this.#renderedKeysCache;
18874
19300
  }
18875
19301
  /**
18876
19302
  * Dispatch `detail-list-viewport-change` with the current key sets — unless
@@ -18970,6 +19396,12 @@ let DetailListElement = class DetailListElement extends LitElement {
18970
19396
  display: flex;
18971
19397
  align-items: center;
18972
19398
  flex: none;
19399
+ /* Paint-isolates each row, so restyling one row (hover, selection)
19400
+ doesn't repaint the whole list: ~3.5x less render work per hover at
19401
+ 10k rows, for ~0.6ms more compositor commit per scroll frame. Paint
19402
+ only: layout/style containment add nothing here, and no size
19403
+ containment — fitColumnWidth() measures rows at max-content. */
19404
+ contain: paint;
18973
19405
  width: var(--detail-list-column-width, 220px);
18974
19406
  height: var(--detail-list-item-height, 28px);
18975
19407
  padding: var(--_detail-list-item-padding);
@@ -20850,6 +21282,11 @@ let SplitLayoutElement = class SplitLayoutElement extends LitElement {
20850
21282
  height: 100%;
20851
21283
  }
20852
21284
 
21285
+ /* ::part() doesn't match inside our own shadow root, so target directly. */
21286
+ [part="panel"] {
21287
+ contain: strict;
21288
+ }
21289
+
20853
21290
  .gutter {
20854
21291
  z-index: 2;
20855
21292
  background: var(--split-gutter-color);
@@ -21045,9 +21482,20 @@ let TabListElement = class TabListElement extends LitElement {
21045
21482
  window.addEventListener(`pointerup`, this.#onPointerUp);
21046
21483
  window.addEventListener(`pointercancel`, this.#onPointerCancel);
21047
21484
  };
21485
+ this.#pendingMove = null;
21486
+ this.#moveRaf = null;
21048
21487
  this.#onPointerMove = (event) => {
21049
21488
  const state = this.#dragState;
21050
21489
  if (!state || event.pointerId !== state.pointerId) return;
21490
+ this.#pendingMove = event;
21491
+ this.#moveRaf ??= requestAnimationFrame(this.#flushMove);
21492
+ };
21493
+ this.#flushMove = () => {
21494
+ const event = this.#pendingMove;
21495
+ this.#pendingMove = null;
21496
+ this.#moveRaf = null;
21497
+ const state = this.#dragState;
21498
+ if (!state || !event) return;
21051
21499
  const dx = event.clientX - state.startX;
21052
21500
  const dy = event.clientY - state.startY;
21053
21501
  const distance = Math.hypot(dx, dy);
@@ -21232,7 +21680,17 @@ let TabListElement = class TabListElement extends LitElement {
21232
21680
  return item;
21233
21681
  }
21234
21682
  #onPointerDown;
21683
+ #pendingMove;
21684
+ #moveRaf;
21235
21685
  #onPointerMove;
21686
+ #cancelPendingMove() {
21687
+ if (this.#moveRaf !== null) {
21688
+ cancelAnimationFrame(this.#moveRaf);
21689
+ this.#moveRaf = null;
21690
+ }
21691
+ this.#pendingMove = null;
21692
+ }
21693
+ #flushMove;
21236
21694
  #onPointerUp;
21237
21695
  #onPointerCancel;
21238
21696
  #beginDrag(event) {
@@ -21258,6 +21716,7 @@ let TabListElement = class TabListElement extends LitElement {
21258
21716
  #endDrag(pointerId) {
21259
21717
  const state = this.#dragState;
21260
21718
  if (!state) return;
21719
+ this.#cancelPendingMove();
21261
21720
  state.ghost?.destroy();
21262
21721
  state.ghost = null;
21263
21722
  this.#dragState = null;
@@ -21580,6 +22039,22 @@ __decorate([property({
21580
22039
  TabListElement = __decorate([safeCustomElement(`ixfx-tab-list`)], TabListElement);
21581
22040
  //#endregion
21582
22041
  //#region src/tabs/tab-list-item.ts
22042
+ const resizeHandlers = /* @__PURE__ */ new WeakMap();
22043
+ const sharedResizeObserver = new ResizeObserver((entries) => {
22044
+ for (const entry of entries) resizeHandlers.get(entry.target)?.(entry.contentRect.width);
22045
+ });
22046
+ const mutationHandlers = /* @__PURE__ */ new Map();
22047
+ const sharedMutationObserver = new MutationObserver((records) => {
22048
+ const touched = /* @__PURE__ */ new Set();
22049
+ for (const record of records) for (let node = record.target; node; node = node.parentNode) {
22050
+ const handler = mutationHandlers.get(node);
22051
+ if (handler) {
22052
+ touched.add(handler);
22053
+ break;
22054
+ }
22055
+ }
22056
+ for (const handler of touched) handler();
22057
+ });
21583
22058
  let TabListItemElement = class TabListItemElement extends LitElement {
21584
22059
  constructor(..._args) {
21585
22060
  super(..._args);
@@ -21593,7 +22068,11 @@ let TabListItemElement = class TabListItemElement extends LitElement {
21593
22068
  this._compact = false;
21594
22069
  this._hasToolbarContent = false;
21595
22070
  this._hasStatusContent = false;
21596
- this._labelObserver = null;
22071
+ this._onLabelMutation = () => {
22072
+ this._syncLabelSizer();
22073
+ this._updateToolbarContent();
22074
+ this._updateStatusContent();
22075
+ };
21597
22076
  this._onToolbarSlotChange = () => {
21598
22077
  this._updateToolbarContent();
21599
22078
  };
@@ -21606,17 +22085,13 @@ let TabListItemElement = class TabListItemElement extends LitElement {
21606
22085
  }
21607
22086
  connectedCallback() {
21608
22087
  super.connectedCallback();
21609
- this._resizeObserver = new ResizeObserver(([entry]) => {
21610
- this._compact = entry.contentRect.width < 64;
22088
+ resizeHandlers.set(this, (width) => {
22089
+ this._compact = width < 64;
21611
22090
  });
21612
- this._resizeObserver.observe(this);
22091
+ sharedResizeObserver.observe(this);
21613
22092
  this._wireSlotListeners();
21614
- this._labelObserver = new MutationObserver(() => {
21615
- this._syncLabelSizer();
21616
- this._updateToolbarContent();
21617
- this._updateStatusContent();
21618
- });
21619
- this._labelObserver.observe(this, {
22093
+ mutationHandlers.set(this, this._onLabelMutation);
22094
+ sharedMutationObserver.observe(this, {
21620
22095
  childList: true,
21621
22096
  characterData: true,
21622
22097
  subtree: true,
@@ -21626,12 +22101,11 @@ let TabListItemElement = class TabListItemElement extends LitElement {
21626
22101
  }
21627
22102
  disconnectedCallback() {
21628
22103
  super.disconnectedCallback();
21629
- this._resizeObserver?.disconnect();
21630
- this._resizeObserver = void 0;
22104
+ sharedResizeObserver.unobserve(this);
22105
+ resizeHandlers.delete(this);
21631
22106
  this._commandCleanup?.();
21632
22107
  this._commandCleanup = void 0;
21633
- this._labelObserver?.disconnect();
21634
- this._labelObserver = null;
22108
+ mutationHandlers.delete(this);
21635
22109
  this._unwireSlotListeners();
21636
22110
  }
21637
22111
  updated(changed) {
@@ -24726,8 +25200,8 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24726
25200
  });
24727
25201
  }
24728
25202
  });
24729
- this.tickled = new TickledController(this, {
24730
- getItems: () => this._getVisibleItems().map((el) => ({
25203
+ this.tickled = ((self) => new TickledController(self, {
25204
+ getItems: () => self.#selectionOrder().map((el) => ({
24731
25205
  id: el,
24732
25206
  enabled: !el.hasAttribute(`not-checked`)
24733
25207
  })),
@@ -24735,9 +25209,11 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24735
25209
  if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
24736
25210
  },
24737
25211
  onStateChange: () => this._syncItemStates(),
24738
- itemWrap: this.itemWrap,
25212
+ get itemWrap() {
25213
+ return self.itemWrap;
25214
+ },
24739
25215
  hoverDisabled: () => this.#dragSelect.isDragging || this.#nativeDrag.isDragging
24740
- });
25216
+ }))(this);
24741
25217
  this.#nativeDragging = false;
24742
25218
  this.#postDragSuppressTimer = 0;
24743
25219
  this.#nativeDrag = new ListNativeDragController(this, {
@@ -24769,6 +25245,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24769
25245
  this.toggleAttribute(`data-native-dragging`, false);
24770
25246
  this.removeAttribute(`aria-label`);
24771
25247
  for (const el of this._getVisibleItems()) el.removeAttribute(`data-drag-origin`);
25248
+ if (this.#virtualActive()) for (const el of this.#allItems) el.removeAttribute(`data-drag-origin`);
24772
25249
  this.#startPostDragSuppressTimer();
24773
25250
  },
24774
25251
  syncLightDomDraggable: () => this._syncNativeDraggable()
@@ -24797,7 +25274,9 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24797
25274
  this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
24798
25275
  itemSelector: ITEM_SELECTOR$2,
24799
25276
  getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
24800
- highlightKey: `vertical-list-search`
25277
+ highlightKey: `vertical-list-search`,
25278
+ getAllItems: () => this.#selectionOrder(),
25279
+ observeContainer: false
24801
25280
  }) });
24802
25281
  this.#onScroll = () => {
24803
25282
  if (this.#scrollRaf !== 0) return;
@@ -24836,10 +25315,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24836
25315
  case `ArrowDown`:
24837
25316
  e.preventDefault();
24838
25317
  this.tickled.handleKeyboardNavigation(`next`);
24839
- this.tickled.tickledId?.scrollIntoView({
24840
- block: `nearest`,
24841
- inline: `nearest`
24842
- });
25318
+ this.#revealCursor();
24843
25319
  if (e.shiftKey && this.interactionMode === `vscode`) {
24844
25320
  const cur = this.tickled.tickledId;
24845
25321
  if (cur) this.selection.rangeTo(cur);
@@ -24848,15 +25324,30 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24848
25324
  case `ArrowUp`:
24849
25325
  e.preventDefault();
24850
25326
  this.tickled.handleKeyboardNavigation(`previous`);
24851
- this.tickled.tickledId?.scrollIntoView({
24852
- block: `nearest`,
24853
- inline: `nearest`
24854
- });
25327
+ this.#revealCursor();
24855
25328
  if (e.shiftKey && this.interactionMode === `vscode`) {
24856
25329
  const cur = this.tickled.tickledId;
24857
25330
  if (cur) this.selection.rangeTo(cur);
24858
25331
  }
24859
25332
  break;
25333
+ case `Home`:
25334
+ e.preventDefault();
25335
+ this.tickled.handleKeyboardNavigation(`first`);
25336
+ this.#revealCursor();
25337
+ break;
25338
+ case `End`:
25339
+ e.preventDefault();
25340
+ this.tickled.handleKeyboardNavigation(`last`);
25341
+ this.#revealCursor();
25342
+ break;
25343
+ case `PageDown`:
25344
+ e.preventDefault();
25345
+ this.#handlePage(1);
25346
+ break;
25347
+ case `PageUp`:
25348
+ e.preventDefault();
25349
+ this.#handlePage(-1);
25350
+ break;
24860
25351
  case `Enter`: {
24861
25352
  const id = this.tickled.tickledId;
24862
25353
  if (id) {
@@ -24978,16 +25469,17 @@ let VerticalListElement = class VerticalListElement extends LitElement {
24978
25469
  #postDragSuppressTimer;
24979
25470
  #nativeDrag;
24980
25471
  #prepareNativeDrag(originKey) {
24981
- const visible = this._getVisibleItems();
24982
- const originEl = visible.find((el) => keyForElement(el) === originKey);
25472
+ const originEl = this.#itemForKey(originKey);
24983
25473
  if (!originEl || !this._isCheckable(originEl)) return void 0;
24984
25474
  if (!this.selection.selected.has(originEl)) this.selection.replace(originEl);
25475
+ const ordered = this.#selectionOrder();
24985
25476
  const selected = [];
24986
- visible.forEach((el, index) => {
25477
+ ordered.forEach((el) => {
24987
25478
  if (!this.selection.selected.has(el)) return;
25479
+ const idx = this.#registryIndexOf(el);
24988
25480
  selected.push({
24989
25481
  key: keyForElement(el) ?? originKey,
24990
- index,
25482
+ index: idx,
24991
25483
  item: el,
24992
25484
  element: el
24993
25485
  });
@@ -25214,6 +25706,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25214
25706
  return;
25215
25707
  }
25216
25708
  if (this.interactionMode === `checked`) this._syncCheckboxes();
25709
+ this.search.refresh();
25217
25710
  }
25218
25711
  /**
25219
25712
  * Collapse + fade `item` out, then detach it. The rows below slide up to
@@ -25250,6 +25743,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25250
25743
  this.replaceChildren();
25251
25744
  this.selection.reset();
25252
25745
  this._syncItemStates();
25746
+ this.search.refresh();
25253
25747
  if (this.#virtualActive()) this.#syncVirtualWindow(true);
25254
25748
  }
25255
25749
  /**
@@ -25265,11 +25759,13 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25265
25759
  this.#allItems = [...items];
25266
25760
  for (const el of this.#allItems) this.#animatedItems.add(el);
25267
25761
  this.selection.reset();
25762
+ this.search.refresh();
25268
25763
  this.#syncVirtualWindow(true);
25269
25764
  return;
25270
25765
  }
25271
25766
  for (const item of items) this.appendChild(item);
25272
25767
  if (this.interactionMode === `checked`) this._syncCheckboxes();
25768
+ this.search.refresh();
25273
25769
  }
25274
25770
  /**
25275
25771
  * Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
@@ -25326,7 +25822,16 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25326
25822
  if (index < 0) return;
25327
25823
  const ul = this.renderRoot.querySelector(`ul`);
25328
25824
  if (ul) {
25329
- ul.scrollTop = index * this.itemHeight;
25825
+ const stride = this.itemHeight;
25826
+ const viewportH = ul.clientHeight;
25827
+ const top = index * stride;
25828
+ const bottom = (index + 1) * stride;
25829
+ const vt = ul.scrollTop;
25830
+ const vh = viewportH;
25831
+ let next = vt;
25832
+ if (top < vt) next = top;
25833
+ else if (bottom > vt + vh) next = bottom - vh;
25834
+ if (next !== vt) ul.scrollTop = next;
25330
25835
  this.#syncVirtualWindow(true);
25331
25836
  }
25332
25837
  }
@@ -25338,6 +25843,33 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25338
25843
  inline: `nearest`
25339
25844
  });
25340
25845
  }
25846
+ #revealCursor() {
25847
+ const el = this.tickled.tickledId;
25848
+ const key = el ? keyForElement(el) : void 0;
25849
+ if (key !== void 0) this.focusKey(key);
25850
+ }
25851
+ #pageSize() {
25852
+ const ul = this.#scrollList();
25853
+ if (!ul) return 1;
25854
+ const viewportH = ul.clientHeight;
25855
+ if (this.#virtualActive()) return Math.max(1, Math.floor(viewportH / this.itemHeight));
25856
+ const first = this.querySelector(`li[data-key]`);
25857
+ if (!first) return 1;
25858
+ const firstH = first.offsetHeight || this.itemHeight;
25859
+ return Math.max(1, Math.floor(viewportH / firstH));
25860
+ }
25861
+ /** Move the cursor one page up (-1) or down (1) across the whole registry. */
25862
+ #handlePage(direction) {
25863
+ const page = this.#pageSize();
25864
+ const items = this.#selectionOrder().filter((el) => !el.hasAttribute(`not-checked`));
25865
+ if (items.length === 0) return;
25866
+ const currentKey = this.tickled.tickledId ? keyForElement(this.tickled.tickledId) : void 0;
25867
+ const currentIdx = currentKey !== void 0 ? items.findIndex((i) => keyForElement(i) === currentKey) : -1;
25868
+ const fallback = direction === 1 ? page - 1 : 0;
25869
+ const nextIdx = currentIdx < 0 ? fallback : currentIdx + direction * page;
25870
+ this.tickled.handleKeyboardNavigation(items[Math.min(items.length - 1, Math.max(0, nextIdx))]);
25871
+ this.#revealCursor();
25872
+ }
25341
25873
  #itemForKey(key) {
25342
25874
  if (this.#virtualActive()) return this.#allItems.find((el) => keyForElement(el) === key);
25343
25875
  for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
@@ -25686,6 +26218,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25686
26218
  else this.#pendingAnimate.push(...fresh);
25687
26219
  }
25688
26220
  if (changed && this.interactionMode === `checked`) this._syncCheckboxes();
26221
+ if (changed) this.search.refresh();
25689
26222
  return changed;
25690
26223
  }
25691
26224
  /** Consume queued observer records first so a sync never drops a row. */
@@ -25763,9 +26296,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25763
26296
  this.#windowEnd = end;
25764
26297
  if (this.interactionMode === `checked`) this._syncCheckboxes();
25765
26298
  this._syncItemStates();
26299
+ const totalCount = this.#selectionOrder().length;
25766
26300
  for (let i = start; i < end; i++) {
25767
26301
  const el = this.#allItems[i];
25768
- if (el) this.#applyRowState(el);
26302
+ if (el) this.#applyRowState(el, i + 1, totalCount);
25769
26303
  }
25770
26304
  const pending = this.#pendingAnimate;
25771
26305
  this.#pendingAnimate = [];
@@ -25773,12 +26307,25 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25773
26307
  this.#animatedItems.add(el);
25774
26308
  if (el.parentNode === this) animateListItemIn(el);
25775
26309
  }
26310
+ this.search.repaintHighlights();
25776
26311
  }
25777
26312
  /** Per-attach state the attached sweep can't cover (it only sees the window). */
25778
- #applyRowState(el) {
25779
- if (el instanceof HTMLElement) el.draggable = this.itemsDraggable && this._isCheckable(el) && !el.hasAttribute(`data-editing`);
26313
+ #applyRowState(el, posInSet, setSize) {
26314
+ if (el instanceof HTMLElement) {
26315
+ el.draggable = this.itemsDraggable && this._isCheckable(el) && !el.hasAttribute(`data-editing`);
26316
+ if (posInSet !== void 0 && setSize !== void 0) {
26317
+ el.setAttribute(`aria-setsize`, String(setSize));
26318
+ el.setAttribute(`aria-posinset`, String(posInSet));
26319
+ if (!el.hasAttribute(`role`) || el.hasAttribute(`data-aria-role`)) {
26320
+ el.setAttribute(`role`, `option`);
26321
+ el.setAttribute(`data-aria-role`, ``);
26322
+ el.setAttribute(`aria-selected`, String(this.selection.selected.has(el)));
26323
+ }
26324
+ }
26325
+ }
25780
26326
  el.removeAttribute(`data-marquee`);
25781
26327
  el.removeAttribute(`data-drag-origin`);
26328
+ el.removeAttribute(`data-drop-target`);
25782
26329
  }
25783
26330
  /**
25784
26331
  * Leave virtualization: reattach every registry row in order and drop the
@@ -25843,6 +26390,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25843
26390
  const item = this.#customWrappers.get(el) ?? el;
25844
26391
  el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
25845
26392
  el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
26393
+ if (el.hasAttribute(`data-aria-role`)) el.setAttribute(`aria-selected`, String(this.selection.selected.has(el)));
25846
26394
  if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !item.hasAttribute(`not-checked`) && this.selection.selected.has(el));
25847
26395
  else el.removeAttribute(`data-checked`);
25848
26396
  }
@@ -25986,6 +26534,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25986
26534
  if (this.interactionMode !== `manual`) this.selection.replace(item);
25987
26535
  this._dispatch(`list-activate`, { item });
25988
26536
  }
26537
+ activateKey(key) {
26538
+ const item = this.#itemForKey(key);
26539
+ if (item) this._activateItem(item);
26540
+ }
25989
26541
  /** The row (or `undefined` for empty space) the last `list-drag-over` was fired for. */
25990
26542
  #dragOver;
25991
26543
  /** Tell the consumer the drag left its last target and drop the highlight. */
@@ -25995,9 +26547,11 @@ let VerticalListElement = class VerticalListElement extends LitElement {
25995
26547
  this.#highlightDropTarget(void 0, false);
25996
26548
  if (last) this._dispatch(`list-drag-leave`, { key: last.item ? keyForElement(last.item) : void 0 });
25997
26549
  }
26550
+ #dropTargetEl;
25998
26551
  /** Mark one row (or, for empty space, the host) as the drop target. */
25999
26552
  #highlightDropTarget(item, active) {
26000
- for (const el of this.querySelectorAll(`[data-drop-target]`)) el.removeAttribute(`data-drop-target`);
26553
+ this.#dropTargetEl?.removeAttribute(`data-drop-target`);
26554
+ this.#dropTargetEl = active ? item : void 0;
26001
26555
  this.toggleAttribute(`data-drop-target`, active && item === void 0);
26002
26556
  if (active && item) item.setAttribute(`data-drop-target`, ``);
26003
26557
  }
@@ -26282,9 +26836,17 @@ let VerticalListElement = class VerticalListElement extends LitElement {
26282
26836
  ];
26283
26837
  }
26284
26838
  render() {
26839
+ const role = this.getAttribute(`role`) ? void 0 : `listbox`;
26840
+ const ariaMulti = this.selectionMode === `multiple`;
26285
26841
  return html`
26286
26842
  <div class="container">
26287
- <ul part="list" class="scroll-fade-y" @scroll=${this.#onScroll}>
26843
+ <ul
26844
+ part="list"
26845
+ class="scroll-fade-y"
26846
+ role=${ifDefined(role)}
26847
+ aria-multiselectable=${ariaMulti ? `true` : `false`}
26848
+ @scroll=${this.#onScroll}
26849
+ >
26288
26850
  <slot></slot>
26289
26851
  </ul>
26290
26852
  ${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
@@ -26753,12 +27315,16 @@ let FlatListElement = class FlatListElement extends LitElement {
26753
27315
  #onKeyDown;
26754
27316
  /** A crumb was chosen (or the Up button used): go to that node, or to its parent and select it if it is a leaf. */
26755
27317
  #onCrumbSelect;
27318
+ #markedEl;
26756
27319
  /** Flag the marked row. Rows are rebuilt with the level, so this runs after each rebuild. */
26757
27320
  async #applyMarked() {
26758
27321
  const list = this.list;
26759
27322
  if (!list) return;
26760
27323
  await list.updateComplete;
26761
- for (const row of list.querySelectorAll(`li[data-key]`)) row.toggleAttribute(`data-marked`, row.getAttribute(`data-key`) === this.markedKey);
27324
+ this.#markedEl?.removeAttribute(`data-marked`);
27325
+ const key = this.markedKey;
27326
+ this.#markedEl = key === void 0 ? void 0 : list.getItemByKey(key);
27327
+ this.#markedEl?.setAttribute(`data-marked`, ``);
26762
27328
  }
26763
27329
  /** After the inner list has rebuilt, place the cursor / selection / focus. */
26764
27330
  async #settleList() {
@@ -27526,16 +28092,24 @@ function warnDuplicateKeys(keys) {
27526
28092
  }
27527
28093
  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.`);
27528
28094
  }
28095
+ /** Key → first effective index, built from `keys` (first occurrence wins on duplicates). */
28096
+ function indexByKey(keys) {
28097
+ const index = /* @__PURE__ */ new Map();
28098
+ keys.forEach((k, i) => {
28099
+ if (!index.has(k)) index.set(k, i);
28100
+ });
28101
+ return index;
28102
+ }
27529
28103
  var ElementGridListSource = class {
27530
28104
  #host;
27531
28105
  #items = [];
27532
- #slotNames = /* @__PURE__ */ new WeakMap();
27533
28106
  #syntheticKeys = /* @__PURE__ */ new WeakMap();
27534
28107
  #seq = 0;
27535
28108
  #predicate;
27536
28109
  #searchKeys;
27537
28110
  #effectiveIndices = [];
27538
28111
  #keysCache;
28112
+ #indexCache;
27539
28113
  #grouping;
27540
28114
  #groups = [];
27541
28115
  #groupByKey = /* @__PURE__ */ new Map();
@@ -27554,7 +28128,8 @@ var ElementGridListSource = class {
27554
28128
  return eff === void 0 ? `` : this.#itemKey(this.#items[eff]);
27555
28129
  }
27556
28130
  indexOfKey(key) {
27557
- return this.#effectiveIndices.findIndex((i) => this.#itemKey(this.#items[i]) === key);
28131
+ this.#indexCache ??= indexByKey(this.keys());
28132
+ return this.#indexCache.get(key) ?? -1;
27558
28133
  }
27559
28134
  keys() {
27560
28135
  this.#keysCache ??= this.#effectiveIndices.map((i) => this.#itemKey(this.#items[i]));
@@ -27577,11 +28152,11 @@ var ElementGridListSource = class {
27577
28152
  const eff = this.#effectiveIndices[index];
27578
28153
  return eff !== void 0 && !this.#items[eff].hasAttribute(`not-checked`);
27579
28154
  }
27580
- renderCellContent(index) {
27581
- const eff = this.#effectiveIndices[index];
27582
- if (eff === void 0) return void 0;
27583
- return html`<slot name=${this.slotNameFor(this.#items[eff])}></slot>`;
27584
- }
28155
+ /**
28156
+ * Element mode cells project their item through a per-cell `<slot>` that
28157
+ * `GridListElement` renders and assigns itself (see its `#applyCellSlots`).
28158
+ */
28159
+ renderCellContent(_index) {}
27585
28160
  iconAt(_index) {}
27586
28161
  iconColorAt(_index) {}
27587
28162
  textColorAt(_index) {}
@@ -27603,10 +28178,6 @@ var ElementGridListSource = class {
27603
28178
  }
27604
28179
  refresh() {
27605
28180
  this.#items = Array.from(this.#host.querySelectorAll(ITEM_SELECTOR$1)).filter((el) => !el.hidden);
27606
- for (const el of this.#items) {
27607
- const name = this.slotNameFor(el);
27608
- if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
27609
- }
27610
28181
  warnDuplicateKeys(this.#items.map((el) => this.#itemKey(el)));
27611
28182
  this.#recomputeEffective();
27612
28183
  }
@@ -27618,16 +28189,6 @@ var ElementGridListSource = class {
27618
28189
  rawElementAt(index) {
27619
28190
  return this.#items[index];
27620
28191
  }
27621
- /** Stable per-element `<slot>` name. */
27622
- slotNameFor(el) {
27623
- let name = this.#slotNames.get(el);
27624
- if (name === void 0) {
27625
- this.#seq += 1;
27626
- name = `gl-item-${this.#seq}`;
27627
- this.#slotNames.set(el, name);
27628
- }
27629
- return name;
27630
- }
27631
28192
  resolveKeyToElement(key) {
27632
28193
  if (isSyntheticKey(key)) return this.#items.find((el) => this.#syntheticKeys.get(el) === key);
27633
28194
  return this.#items.find((el) => keyForElement(el) === key);
@@ -27645,6 +28206,7 @@ var ElementGridListSource = class {
27645
28206
  }
27646
28207
  this.#applyGroupingTo(effective);
27647
28208
  this.#keysCache = void 0;
28209
+ this.#indexCache = void 0;
27648
28210
  }
27649
28211
  #applyGroupingTo(effective) {
27650
28212
  if (this.#grouping === void 0) {
@@ -27680,6 +28242,7 @@ var DataGridListSource = class {
27680
28242
  #searchKeys;
27681
28243
  #effectiveIndices = [];
27682
28244
  #keysCache;
28245
+ #indexCache;
27683
28246
  #grouping;
27684
28247
  #groups = [];
27685
28248
  #groupByKey = /* @__PURE__ */ new Map();
@@ -27700,7 +28263,8 @@ var DataGridListSource = class {
27700
28263
  return eff === void 0 ? `` : this.#adapter.key(this.#items[eff], eff);
27701
28264
  }
27702
28265
  indexOfKey(key) {
27703
- return this.#effectiveIndices.findIndex((i) => this.#adapter.key(this.#items[i], i) === key);
28266
+ this.#indexCache ??= indexByKey(this.keys());
28267
+ return this.#indexCache.get(key) ?? -1;
27704
28268
  }
27705
28269
  keys() {
27706
28270
  this.#keysCache ??= this.#effectiveIndices.map((i) => this.#adapter.key(this.#items[i], i));
@@ -27793,6 +28357,7 @@ var DataGridListSource = class {
27793
28357
  this.#groupByKey = grouped.groupByKey;
27794
28358
  }
27795
28359
  this.#keysCache = void 0;
28360
+ this.#indexCache = void 0;
27796
28361
  }
27797
28362
  };
27798
28363
  //#endregion
@@ -27819,6 +28384,15 @@ function sameKeys(a, b) {
27819
28384
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
27820
28385
  return true;
27821
28386
  }
28387
+ /**
28388
+ * Set `attr` on exactly the elements in `now`, removing it from those in
28389
+ * `previous` that no longer qualify. Returns the new marked set.
28390
+ */
28391
+ function syncMarker(attr, previous, now) {
28392
+ for (const el of previous) if (!now.has(el)) el.removeAttribute(attr);
28393
+ for (const el of now) if (!el.hasAttribute(attr)) el.setAttribute(attr, ``);
28394
+ return now;
28395
+ }
27822
28396
  let GridListElement = class GridListElement extends LitElement {
27823
28397
  constructor(..._args) {
27824
28398
  super(..._args);
@@ -27853,6 +28427,11 @@ let GridListElement = class GridListElement extends LitElement {
27853
28427
  this.#snapTimer = 0;
27854
28428
  this.#items = [];
27855
28429
  this.#itemWrap = `stop`;
28430
+ this.#cellIds = /* @__PURE__ */ new Map();
28431
+ this.#nextCellId = 0;
28432
+ this.#recycledKeys = /* @__PURE__ */ new Set();
28433
+ this.#windowSlots = [];
28434
+ this.#slottedItems = /* @__PURE__ */ new Map();
27856
28435
  this.#entering = /* @__PURE__ */ new Set();
27857
28436
  this.#reflowTimer = 0;
27858
28437
  this.#sourceElement = new ElementGridListSource(this);
@@ -27976,6 +28555,8 @@ let GridListElement = class GridListElement extends LitElement {
27976
28555
  }
27977
28556
  });
27978
28557
  this.#groupByChanged = false;
28558
+ this.#markedTickled = /* @__PURE__ */ new Set();
28559
+ this.#markedSelected = /* @__PURE__ */ new Set();
27979
28560
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
27980
28561
  this.#onScroll = (e) => {
27981
28562
  const el = e.target;
@@ -28465,6 +29046,14 @@ let GridListElement = class GridListElement extends LitElement {
28465
29046
  .cell.entering {
28466
29047
  animation: ixfx-list-item-in var(--grid-list-item-move-duration, 200ms) ease both;
28467
29048
  }
29049
+ /* A recycled cell has just been handed from one item to another (see
29050
+ #assignCellIds); it must not animate from the previous item's state
29051
+ or position. */
29052
+ .cell.recycled,
29053
+ .cell.recycled > .thumb,
29054
+ .cell.recycled > .thumb::after {
29055
+ transition: none !important;
29056
+ }
28468
29057
  @media (prefers-reduced-motion: reduce) {
28469
29058
  :host([reflowing]) .group-header { transition: none; }
28470
29059
  :host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
@@ -28496,11 +29085,39 @@ let GridListElement = class GridListElement extends LitElement {
28496
29085
  #viewportRef;
28497
29086
  #resizeObserver;
28498
29087
  #mutationObserver;
29088
+ /**
29089
+ * Watches item (direct child) attributes that change the item set: `hidden`
29090
+ * drops an item from layout, `data-key` / `data-value` change its identity.
29091
+ * Separate from {@link #mutationObserver} because attribute observation of
29092
+ * children needs `subtree`, which must not apply to the child list.
29093
+ */
29094
+ #itemAttrObserver;
28499
29095
  #scrollRaf;
28500
29096
  /** Debounce handle for {@link snapToItem} — fires once scrolling settles. */
28501
29097
  #snapTimer;
28502
29098
  #items;
28503
29099
  #itemWrap;
29100
+ /**
29101
+ * Cell recycling. Each rendered key holds a stable cell id, and `repeat` is
29102
+ * keyed by that id rather than the item key, so a cell scrolled out of the
29103
+ * window hands its DOM to an item scrolling in instead of being destroyed and
29104
+ * recreated. Item-keyed `repeat` gets steadily slower in Chrome when every key
29105
+ * in the window is new (a scrollbar drag / fling), and recreating ~100 cells a
29106
+ * frame is costly in any case. See {@link #assignCellIds}.
29107
+ */
29108
+ #cellIds;
29109
+ #nextCellId;
29110
+ /** Keys whose cell was taken over from another item in the latest render. */
29111
+ #recycledKeys;
29112
+ /**
29113
+ * Element mode: each cell has a fixed `<slot name="gl-cell-<id>">`, and the
29114
+ * light-DOM item it shows is pointed at it via its `slot` attribute. Moving
29115
+ * that attribute is far cheaper than renaming slots, which makes the browser
29116
+ * re-match every light-DOM child. `#windowSlots` is the assignment the latest
29117
+ * render wants (applied in `updated()`); `#slottedItems` is what's applied.
29118
+ */
29119
+ #windowSlots;
29120
+ #slottedItems;
28504
29121
  /** Items added mid-list that are still playing their enter animation. */
28505
29122
  #entering;
28506
29123
  /** Timer that ends the `[reflowing]` window opened by a mid-list insertion. */
@@ -28688,9 +29305,15 @@ let GridListElement = class GridListElement extends LitElement {
28688
29305
  this.addEventListener(`pointermove`, this.#onHostPointerMove, { passive: true });
28689
29306
  this.addEventListener(`pointerleave`, this.#onHostPointerLeave);
28690
29307
  this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
28691
- this.#mutationObserver.observe(this, {
28692
- childList: true,
28693
- attributeFilter: [`hidden`]
29308
+ this.#mutationObserver.observe(this, { childList: true });
29309
+ this.#itemAttrObserver ??= new MutationObserver((records) => this.#onItemAttributesChanged(records));
29310
+ this.#itemAttrObserver.observe(this, {
29311
+ subtree: true,
29312
+ attributeFilter: [
29313
+ `hidden`,
29314
+ `data-key`,
29315
+ `data-value`
29316
+ ]
28694
29317
  });
28695
29318
  this.#searchRegistry ??= createCommandRegistry();
28696
29319
  this.#ensureSearch();
@@ -28706,6 +29329,7 @@ let GridListElement = class GridListElement extends LitElement {
28706
29329
  this.#resizeObserver?.disconnect();
28707
29330
  this.#resizeObserver = void 0;
28708
29331
  this.#mutationObserver?.disconnect();
29332
+ this.#itemAttrObserver?.disconnect();
28709
29333
  this.#cancelScrollRaf();
28710
29334
  this.#cancelSnapTimer();
28711
29335
  this.#unregisterSearchCommands?.();
@@ -28717,6 +29341,10 @@ let GridListElement = class GridListElement extends LitElement {
28717
29341
  }
28718
29342
  if (this.#nativeDragging || this.#nativeDrag.isDragging) this.#clearNativeDragState();
28719
29343
  }
29344
+ /** Only attribute changes on items themselves matter, not on their content. */
29345
+ #onItemAttributesChanged(records) {
29346
+ if (records.some((r) => r.target.parentNode === this)) this.#onChildrenChanged([]);
29347
+ }
28720
29348
  #onChildrenChanged(records) {
28721
29349
  const previousLength = this.#items.length;
28722
29350
  this.#syncItems();
@@ -28733,7 +29361,7 @@ let GridListElement = class GridListElement extends LitElement {
28733
29361
  const tickledId = this.tickled.tickledId;
28734
29362
  if (tickledId && gone.includes(tickledId)) this.tickled.focus(void 0);
28735
29363
  }
28736
- this._syncItemStates();
29364
+ this._syncItemStates(true);
28737
29365
  this.#noteChildMutation(previousLength, records);
28738
29366
  this.requestUpdate();
28739
29367
  this.updateComplete.then(() => this.#emitViewportChange());
@@ -28805,6 +29433,11 @@ let GridListElement = class GridListElement extends LitElement {
28805
29433
  const w = entry.contentRect.width;
28806
29434
  const h = entry.contentRect.height;
28807
29435
  if (w <= 0 || h <= 0) return;
29436
+ if (w === this._viewportWidth && h === this._viewportHeight) continue;
29437
+ const horizontal = this.orientation === `row`;
29438
+ const prevOffset = horizontal ? this._scrollLeft : this._scrollTop;
29439
+ const prevExtent = horizontal ? this._viewportWidth : this._viewportHeight;
29440
+ if (prevExtent > 0 && prevOffset > 1) this.#captureCentreAnchor(prevOffset, prevExtent, this.itemSize, this.gap, this.captionStyle, this.captionHeight, this.orientation);
28808
29441
  this._viewportWidth = w;
28809
29442
  this._viewportHeight = h;
28810
29443
  }
@@ -28966,7 +29599,11 @@ let GridListElement = class GridListElement extends LitElement {
28966
29599
  }
28967
29600
  updated(changed) {
28968
29601
  super.updated(changed);
28969
- this.#syncItems();
29602
+ const pending = this.#mutationObserver?.takeRecords();
29603
+ const pendingAttrs = this.#itemAttrObserver?.takeRecords();
29604
+ if (pending !== void 0 && pending.length > 0) this.#onChildrenChanged(pending);
29605
+ else if (pendingAttrs !== void 0 && pendingAttrs.length > 0) this.#onItemAttributesChanged(pendingAttrs);
29606
+ if (this.#source instanceof ElementGridListSource) this.#applyCellSlots();
28970
29607
  if (changed.has(`selectionMode`) || changed.has(`interactionMode`)) {
28971
29608
  this.selection.clearAnchor();
28972
29609
  this.#syncAriaMultiselectable();
@@ -29318,13 +29955,36 @@ let GridListElement = class GridListElement extends LitElement {
29318
29955
  this._scrollTop = next;
29319
29956
  }
29320
29957
  }
29321
- _syncItemStates() {
29958
+ /** Light-DOM items currently carrying `data-tickled` / `data-selected`. */
29959
+ #markedTickled;
29960
+ #markedSelected;
29961
+ /**
29962
+ * Mirror cursor / selection onto the light-DOM items as `data-tickled` /
29963
+ * `data-selected`. Only the items whose state changed are touched, so a
29964
+ * cursor move costs O(selection), not O(items). `full` re-checks every item —
29965
+ * used when the child list changes, so markers set before the item was
29966
+ * ours (or left on a removed item) are corrected.
29967
+ */
29968
+ _syncItemStates(full = false) {
29322
29969
  const selected = this.selection.selectedKeys;
29323
- for (const el of this.#items) {
29324
- const key = keyForElement(el);
29325
- el.toggleAttribute(`data-tickled`, key !== void 0 && this.tickled.isTickled(key));
29326
- el.toggleAttribute(`data-selected`, key !== void 0 && selected.has(key));
29970
+ const tickledId = this.tickled.tickledId;
29971
+ const elementFor = (key) => {
29972
+ if (isSyntheticKey(key)) return void 0;
29973
+ const index = this.#source.indexOfKey(key);
29974
+ return index < 0 ? void 0 : this.#items[index];
29975
+ };
29976
+ const tickledEl = tickledId === void 0 ? void 0 : elementFor(tickledId);
29977
+ const selectedEls = /* @__PURE__ */ new Set();
29978
+ for (const key of selected) {
29979
+ const el = elementFor(key);
29980
+ if (el) selectedEls.add(el);
29327
29981
  }
29982
+ if (full) for (const el of this.#items) {
29983
+ this.#markedTickled.add(el);
29984
+ this.#markedSelected.add(el);
29985
+ }
29986
+ this.#markedTickled = syncMarker(`data-tickled`, this.#markedTickled, tickledEl ? /* @__PURE__ */ new Set([tickledEl]) : /* @__PURE__ */ new Set());
29987
+ this.#markedSelected = syncMarker(`data-selected`, this.#markedSelected, selectedEls);
29328
29988
  this.setAttribute(`data-input-mode`, this.tickled.inputMode);
29329
29989
  }
29330
29990
  #emitSelect(selected, previous) {
@@ -29394,8 +30054,57 @@ let GridListElement = class GridListElement extends LitElement {
29394
30054
  `;
29395
30055
  }
29396
30056
  #renderDataCells(win) {
30057
+ const slice = this.#windowSlice(win);
30058
+ return repeat(slice, (index) => this.#cellIds.get(this.#source.keyAt(index)), (index) => this.#renderDataCell(index, win));
30059
+ }
30060
+ #cellSlotName(key) {
30061
+ return `gl-cell-${this.#cellIds.get(key)}`;
30062
+ }
30063
+ /**
30064
+ * Element mode: point each rendered item's `slot` at its cell, and clear it on
30065
+ * items that left the window (or the DOM) so they project nowhere.
30066
+ */
30067
+ #applyCellSlots() {
30068
+ const next = /* @__PURE__ */ new Map();
30069
+ for (const [el, name] of this.#windowSlots) {
30070
+ next.set(el, name);
30071
+ if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
30072
+ }
30073
+ for (const el of this.#slottedItems.keys()) if (!next.has(el)) el.removeAttribute(`slot`);
30074
+ this.#slottedItems = next;
30075
+ }
30076
+ /** Effective indices in the rendered window, with cell ids assigned to their keys. */
30077
+ #windowSlice(win) {
29397
30078
  const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
29398
- return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderDataCell(index, win));
30079
+ this.#assignCellIds(slice.map((index) => this.#source.keyAt(index)));
30080
+ return slice;
30081
+ }
30082
+ /**
30083
+ * Give every key in the window a cell id: keys already rendered keep theirs;
30084
+ * new keys take the ids freed by keys that left the window (recycling that
30085
+ * cell's DOM), and only mint a fresh id when none is free.
30086
+ */
30087
+ #assignCellIds(keys) {
30088
+ const inWindow = new Set(keys);
30089
+ const free = [];
30090
+ for (const [key, id] of this.#cellIds) if (!inWindow.has(key)) free.push(id);
30091
+ const next = /* @__PURE__ */ new Map();
30092
+ for (const key of keys) {
30093
+ const id = this.#cellIds.get(key);
30094
+ if (id !== void 0) next.set(key, id);
30095
+ }
30096
+ const recycled = /* @__PURE__ */ new Set();
30097
+ for (const key of keys) {
30098
+ if (next.has(key)) continue;
30099
+ const id = free.pop();
30100
+ if (id === void 0) next.set(key, this.#nextCellId++);
30101
+ else {
30102
+ next.set(key, id);
30103
+ recycled.add(key);
30104
+ }
30105
+ }
30106
+ this.#cellIds = next;
30107
+ this.#recycledKeys = recycled;
29399
30108
  }
29400
30109
  #renderDataCell(index, win) {
29401
30110
  const key = this.#source.keyAt(index);
@@ -29415,10 +30124,12 @@ let GridListElement = class GridListElement extends LitElement {
29415
30124
  const icon = appearanceAbsent(rawIcon) ? void 0 : rawIcon;
29416
30125
  return html`
29417
30126
  <div
29418
- class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
30127
+ class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${marquee ? ` marquee` : ``}${this.#recycledKeys.has(key) ? ` recycled` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
29419
30128
  part="item${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
29420
30129
  role="option"
29421
30130
  aria-selected=${selected ? `true` : `false`}
30131
+ aria-setsize=${this.#source.count}
30132
+ aria-posinset=${index + 1}
29422
30133
  style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
29423
30134
  data-index=${index}
29424
30135
  data-key=${key}
@@ -29495,8 +30206,9 @@ let GridListElement = class GridListElement extends LitElement {
29495
30206
  current.startLoop?.();
29496
30207
  }
29497
30208
  #renderElementCells(win) {
29498
- const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
29499
- return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderCell(this.#items[index], index, win));
30209
+ const slice = this.#windowSlice(win);
30210
+ this.#windowSlots = slice.map((index) => [this.#items[index], this.#cellSlotName(this.#source.keyAt(index))]);
30211
+ return repeat(slice, (index) => this.#cellIds.get(this.#source.keyAt(index)), (index) => this.#renderCell(this.#items[index], index, win));
29500
30212
  }
29501
30213
  #renderHeaders(win) {
29502
30214
  if (win.headers.length === 0) return nothing;
@@ -29505,6 +30217,7 @@ let GridListElement = class GridListElement extends LitElement {
29505
30217
  <ixfx-list-group-header
29506
30218
  class="group-header"
29507
30219
  part="group-header"
30220
+ role="presentation"
29508
30221
  data-group-key=${h.key}
29509
30222
  label=${h.label}
29510
30223
  count=${h.count}
@@ -29533,10 +30246,12 @@ let GridListElement = class GridListElement extends LitElement {
29533
30246
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
29534
30247
  return html`
29535
30248
  <div
29536
- class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
30249
+ class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}${marquee ? ` marquee` : ``}${this.#recycledKeys.has(key) ? ` recycled` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
29537
30250
  part="item${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
29538
30251
  role="option"
29539
30252
  aria-selected=${selected ? `true` : `false`}
30253
+ aria-setsize=${this.#source.count}
30254
+ aria-posinset=${index + 1}
29540
30255
  style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
29541
30256
  data-index=${index}
29542
30257
  data-key=${key}
@@ -29549,7 +30264,7 @@ let GridListElement = class GridListElement extends LitElement {
29549
30264
  @pointerenter=${() => this.#handleCellPointerEnter(key)}
29550
30265
  >
29551
30266
  <div class="thumb" part="thumb" style=${thumbStyle}>
29552
- ${this.#source.renderCellContent(index)}
30267
+ <slot name=${this.#cellSlotName(key)}></slot>
29553
30268
  </div>
29554
30269
  ${showCaption ? this.#renderCaption(caption, captionCss, tickled) : ``}
29555
30270
  </div>
@@ -30401,15 +31116,10 @@ var TreeDropController = class {
30401
31116
  if (clientY > rows[rows.length - 1].rect.bottom) return rows[rows.length - 1];
30402
31117
  return rows.find((r) => clientY >= r.rect.top && clientY <= r.rect.bottom) ?? rows[rows.length - 1];
30403
31118
  }
30404
- #parentRow(rows, index) {
30405
- const target = rows[index];
30406
- for (let i = index - 1; i >= 0; i--) if (rows[i].depth === target.depth - 1) return rows[i];
30407
- }
30408
31119
  #resolveIntent(event, rows, source) {
30409
31120
  if (rows.length === 0) return void 0;
30410
31121
  const row = this.#rowAtPointer(rows, event.clientY);
30411
31122
  if (!row) return void 0;
30412
- const rowIndex = rows.indexOf(row);
30413
31123
  const f = clamp$4((event.clientY - row.rect.top) / row.rect.height, 0, 1);
30414
31124
  let position = f < .25 ? `before` : f > .75 ? `after` : `inside`;
30415
31125
  const isLeaf = row.node.item.isLeaf === true;
@@ -30432,18 +31142,21 @@ var TreeDropController = class {
30432
31142
  const indent = this.#config.getIndentPx() || 1;
30433
31143
  const treeLeft = rows[0].rect.left;
30434
31144
  const effDepth = clamp$4(Math.round((event.clientX - treeLeft) / indent), 0, row.depth);
30435
- let anchorIndex = rowIndex;
30436
- for (let up = row.depth - effDepth; up > 0; up--) {
30437
- const parent = this.#parentRow(rows, anchorIndex);
30438
- if (!parent) break;
30439
- anchorIndex = rows.indexOf(parent);
30440
- }
30441
- const anchor = rows[anchorIndex];
30442
- referenceKey = anchor.key;
31145
+ let anchor = {
31146
+ node: row.node,
31147
+ depth: row.depth,
31148
+ parentKey: this.#config.getRowInfo(row.key)?.parentKey
31149
+ };
31150
+ referenceKey = row.key;
31151
+ for (let up = row.depth - effDepth; up > 0 && anchor.parentKey !== void 0; up--) {
31152
+ const parentInfo = this.#config.getRowInfo(anchor.parentKey);
31153
+ if (!parentInfo) break;
31154
+ referenceKey = anchor.parentKey;
31155
+ anchor = parentInfo;
31156
+ }
30443
31157
  depth = anchor.depth;
30444
- const parent = this.#parentRow(rows, anchorIndex);
30445
- parentKey = parent?.key;
30446
- const refIdx = (parent ? parent.node.children ?? [] : rows.filter((r) => r.depth === 0).map((r) => r.node)).findIndex((n) => n.item.key === anchor.key);
31158
+ parentKey = anchor.parentKey;
31159
+ const refIdx = this.#config.getChildren(parentKey).findIndex((n) => n.item.key === referenceKey);
30447
31160
  index = (refIdx < 0 ? 0 : refIdx) + (position === `after` ? 1 : 0);
30448
31161
  }
30449
31162
  if (source === `internal`) {
@@ -30518,9 +31231,333 @@ var TreeDropController = class {
30518
31231
  }
30519
31232
  };
30520
31233
  //#endregion
31234
+ //#region src/tree/tree-flat.ts
31235
+ /**
31236
+ * Flatten the visible rows under `root` (root itself excluded).
31237
+ *
31238
+ * Iterative implementation using an explicit stack to avoid recursion depth
31239
+ * limits with deep trees. `posInSet` / `setSize` count only shown siblings.
31240
+ *
31241
+ * @param root - The root node (never included in output; its children are depth 0)
31242
+ * @param options - Callbacks determining expansion and visibility
31243
+ * @returns FlatTree with rows in display order and a key→index map
31244
+ */
31245
+ function flattenTree(root, options) {
31246
+ const rows = [];
31247
+ const indexByKey = /* @__PURE__ */ new Map();
31248
+ const stack = [];
31249
+ const pushChildren = (children, depth, parentKey) => {
31250
+ if (!children || children.length === 0) return;
31251
+ const shown = children.filter((c) => options.isShown(c, depth));
31252
+ for (let i = shown.length - 1; i >= 0; i--) stack.push({
31253
+ node: shown[i],
31254
+ depth,
31255
+ parentKey,
31256
+ posInSet: i + 1,
31257
+ setSize: shown.length
31258
+ });
31259
+ };
31260
+ pushChildren(root.children, 0, void 0);
31261
+ while (stack.length > 0) {
31262
+ const { node, depth, parentKey, posInSet, setSize } = stack.pop();
31263
+ indexByKey.set(node.item.key, rows.length);
31264
+ rows.push({
31265
+ node,
31266
+ depth,
31267
+ parentKey,
31268
+ posInSet,
31269
+ setSize
31270
+ });
31271
+ if (node.children && node.children.length > 0 && options.isExpanded(node, depth)) pushChildren(node.children, depth + 1, node.item.key);
31272
+ }
31273
+ return {
31274
+ rows,
31275
+ indexByKey
31276
+ };
31277
+ }
31278
+ /**
31279
+ * Build a whole-tree index over the currently loaded nodes.
31280
+ * This walks the entire loaded tree once and is cached on structure version.
31281
+ *
31282
+ * @param root - The root node
31283
+ * @returns TreeIndex with parent map and icon depth set
31284
+ */
31285
+ function indexTree(root) {
31286
+ const parentByKey = /* @__PURE__ */ new Map();
31287
+ const depthsWithIcons = /* @__PURE__ */ new Set();
31288
+ const stack = [];
31289
+ if (root.children) for (const child of root.children) {
31290
+ parentByKey.set(child.item.key, root.item.key);
31291
+ stack.push({
31292
+ node: child,
31293
+ depth: 0,
31294
+ parentKey: root.item.key
31295
+ });
31296
+ }
31297
+ while (stack.length > 0) {
31298
+ const { node, depth, parentKey } = stack.pop();
31299
+ parentByKey.set(node.item.key, parentKey);
31300
+ if (node.item.icon) depthsWithIcons.add(depth);
31301
+ if (node.children) for (let i = node.children.length - 1; i >= 0; i--) {
31302
+ const child = node.children[i];
31303
+ stack.push({
31304
+ node: child,
31305
+ depth: depth + 1,
31306
+ parentKey: node.item.key
31307
+ });
31308
+ }
31309
+ }
31310
+ return {
31311
+ parentByKey,
31312
+ depthsWithIcons
31313
+ };
31314
+ }
31315
+ /**
31316
+ * Compute which nodes match a filter predicate.
31317
+ * Returns the set of keys for nodes that match OR have a matching descendant.
31318
+ *
31319
+ * One iterative post-order pass; every node is visited (no short-circuiting).
31320
+ */
31321
+ function computeFilterMatches(root, predicate) {
31322
+ const matches = /* @__PURE__ */ new Set();
31323
+ if (!root.children) return matches;
31324
+ const stack = [];
31325
+ const enter = (node, depth) => {
31326
+ stack.push({
31327
+ node,
31328
+ depth,
31329
+ next: 0,
31330
+ matched: predicate(node, depth)
31331
+ });
31332
+ };
31333
+ for (const child of root.children) {
31334
+ enter(child, 0);
31335
+ while (stack.length > 0) {
31336
+ const frame = stack[stack.length - 1];
31337
+ const children = frame.node.children;
31338
+ if (children && frame.next < children.length) {
31339
+ enter(children[frame.next++], frame.depth + 1);
31340
+ continue;
31341
+ }
31342
+ stack.pop();
31343
+ if (frame.matched) {
31344
+ matches.add(frame.node.item.key);
31345
+ if (stack.length > 0) stack[stack.length - 1].matched = true;
31346
+ }
31347
+ }
31348
+ }
31349
+ return matches;
31350
+ }
31351
+ /**
31352
+ * Merge ghost rows back near their previous position.
31353
+ * Each ghost goes immediately before the first row that followed it last render
31354
+ * and still exists; otherwise at the end. Ghosts sharing an anchor keep their
31355
+ * previous relative order. O(rows + prevKeys).
31356
+ *
31357
+ * @param rows - Current flat rows (with node and depth)
31358
+ * @param prevKeys - Previous render's row keys in order
31359
+ * @param ghosts - Map of ghost keys to their row data
31360
+ * @returns Rows with ghosts woven in at their original relative positions
31361
+ */
31362
+ function weaveGhosts(rows, prevKeys, ghosts) {
31363
+ if (ghosts.size === 0) return [...rows];
31364
+ const present = /* @__PURE__ */ new Set();
31365
+ for (const r of rows) present.add(r.node.item.key);
31366
+ const END = Symbol(`end`);
31367
+ const before = /* @__PURE__ */ new Map();
31368
+ let nextSurvivor = END;
31369
+ for (let i = prevKeys.length - 1; i >= 0; i--) {
31370
+ const key = prevKeys[i];
31371
+ if (present.has(key)) {
31372
+ nextSurvivor = key;
31373
+ continue;
31374
+ }
31375
+ const ghost = ghosts.get(key);
31376
+ if (!ghost) continue;
31377
+ const list = before.get(nextSurvivor);
31378
+ if (list) list.push(ghost);
31379
+ else before.set(nextSurvivor, [ghost]);
31380
+ }
31381
+ const result = [];
31382
+ for (const row of rows) {
31383
+ const list = before.get(row.node.item.key);
31384
+ if (list) for (let i = list.length - 1; i >= 0; i--) result.push(list[i]);
31385
+ result.push(row);
31386
+ }
31387
+ const tail = before.get(END);
31388
+ if (tail) for (let i = tail.length - 1; i >= 0; i--) result.push(tail[i]);
31389
+ return result;
31390
+ }
31391
+ /**
31392
+ * Compute checkbox states for all nodes in a single iterative post-order pass.
31393
+ * A leaf (or a branch with no children) uses `selected.has(node)`; otherwise a
31394
+ * branch is checked if all children are checked, unchecked if all are
31395
+ * unchecked, else indeterminate.
31396
+ *
31397
+ * @param root - The root node
31398
+ * @param selected - Set of selected nodes
31399
+ * @returns Map from node key to checkbox state
31400
+ */
31401
+ function computeCheckboxStates(root, selected) {
31402
+ const states = /* @__PURE__ */ new Map();
31403
+ if (!root.children) return states;
31404
+ const stack = [];
31405
+ for (const top of root.children) {
31406
+ stack.push({
31407
+ node: top,
31408
+ next: 0,
31409
+ checked: 0,
31410
+ unchecked: 0
31411
+ });
31412
+ while (stack.length > 0) {
31413
+ const frame = stack[stack.length - 1];
31414
+ const { node } = frame;
31415
+ const children = node.children;
31416
+ const isLeafLike = node.item.isLeaf || !children?.length;
31417
+ if (!isLeafLike && frame.next < children.length) {
31418
+ stack.push({
31419
+ node: children[frame.next++],
31420
+ next: 0,
31421
+ checked: 0,
31422
+ unchecked: 0
31423
+ });
31424
+ continue;
31425
+ }
31426
+ stack.pop();
31427
+ let state;
31428
+ if (isLeafLike) state = selected.has(node) ? `checked` : `unchecked`;
31429
+ else if (frame.checked === children.length) state = `checked`;
31430
+ else if (frame.unchecked === children.length) state = `unchecked`;
31431
+ else state = `indeterminate`;
31432
+ states.set(node.item.key, state);
31433
+ if (stack.length > 0) {
31434
+ const parent = stack[stack.length - 1];
31435
+ if (state === `checked`) parent.checked++;
31436
+ else if (state === `unchecked`) parent.unchecked++;
31437
+ }
31438
+ }
31439
+ }
31440
+ return states;
31441
+ }
31442
+ function withKey(set, key) {
31443
+ if (set.has(key)) return set;
31444
+ const next = new Set(set);
31445
+ next.add(key);
31446
+ return next;
31447
+ }
31448
+ function withoutKey(set, key) {
31449
+ if (!set.has(key)) return set;
31450
+ const next = new Set(set);
31451
+ next.delete(key);
31452
+ return next;
31453
+ }
31454
+ /** Remove every key for which `pred` returns true. */
31455
+ function withoutKeys(set, pred) {
31456
+ let next;
31457
+ for (const key of set) if (pred(key)) {
31458
+ next ??= new Set(set);
31459
+ next.delete(key);
31460
+ }
31461
+ return next ?? set;
31462
+ }
31463
+ function treeStride(params) {
31464
+ return Math.max(1, params.rowHeight + params.gap);
31465
+ }
31466
+ /** Content-space top of the row box at `index`. */
31467
+ function treeRowTop(params, index) {
31468
+ return params.listOffset + index * treeStride(params) + params.gap;
31469
+ }
31470
+ function treeRowBottom(params, index) {
31471
+ return treeRowTop(params, index) + params.rowHeight;
31472
+ }
31473
+ /** The window of rows to render for a given scroll offset. Always `0 <= start <= end <= count`. */
31474
+ function computeTreeWindow(params, scrollTop, overscan = 5) {
31475
+ const { count } = params;
31476
+ if (count <= 0) return {
31477
+ start: 0,
31478
+ end: 0
31479
+ };
31480
+ const stride = treeStride(params);
31481
+ const top = Math.max(0, scrollTop - params.listOffset);
31482
+ const firstVisible = Math.floor(top / stride);
31483
+ const lastVisible = Math.ceil((top + params.viewportHeight) / stride);
31484
+ const start = Math.min(count, Math.max(0, firstVisible - overscan));
31485
+ return {
31486
+ start,
31487
+ end: Math.min(count, Math.max(start, lastVisible + overscan))
31488
+ };
31489
+ }
31490
+ /**
31491
+ * The scroll offset that brings row `index` fully into view with the least
31492
+ * movement, leaving `topInset` px at the top clear (the sticky-header overlay).
31493
+ * Returns `scrollTop` unchanged when the row is already visible.
31494
+ */
31495
+ function treeScrollTopToReveal(params, index, scrollTop, topInset = 0) {
31496
+ if (index < 0 || index >= params.count) return scrollTop;
31497
+ const top = treeRowTop(params, index);
31498
+ const bottom = top + params.rowHeight;
31499
+ if (top < scrollTop + topInset) return index === 0 ? 0 : Math.max(0, top - topInset);
31500
+ if (bottom > scrollTop + params.viewportHeight) return Math.max(0, bottom - params.viewportHeight);
31501
+ return scrollTop;
31502
+ }
31503
+ /** Rows that fit one viewport (PageUp / PageDown step), at least 1. */
31504
+ function treePageSize(params) {
31505
+ return Math.max(1, Math.floor(params.viewportHeight / treeStride(params)) - 1);
31506
+ }
31507
+ /** Arithmetic `RowGeometryReader` over flat-row indices (the windowed counterpart of reading `offsetTop`). */
31508
+ function treeRowReader(params) {
31509
+ return {
31510
+ count: params.count,
31511
+ topOf: (i) => treeRowTop(params, i),
31512
+ bottomOf: (i) => treeRowBottom(params, i)
31513
+ };
31514
+ }
31515
+ //#endregion
31516
+ //#region src/tree/tree-row-geometry.ts
31517
+ /**
31518
+ * First index whose bottom is greater than `y` (binary search; rows are
31519
+ * monotonic). Returns `reader.count` when no row qualifies.
31520
+ */
31521
+ function firstRowBelow(reader, y) {
31522
+ let lo = 0;
31523
+ let hi = reader.count;
31524
+ while (lo < hi) {
31525
+ const mid = lo + hi >>> 1;
31526
+ if (reader.bottomOf(mid) > y) hi = mid;
31527
+ else lo = mid + 1;
31528
+ }
31529
+ return lo;
31530
+ }
31531
+ /**
31532
+ * Inclusive index range of rows that overlap `[top, bottom]`, widened by
31533
+ * `margin` rows on each side. Returns `undefined` when there are no rows.
31534
+ */
31535
+ function rowRangeOverlapping(reader, top, bottom, margin = 0) {
31536
+ if (reader.count === 0) return void 0;
31537
+ const firstHit = Math.min(firstRowBelow(reader, top), reader.count - 1);
31538
+ let lo = firstHit;
31539
+ let hi = reader.count;
31540
+ while (lo < hi) {
31541
+ const mid = lo + hi >>> 1;
31542
+ if (reader.topOf(mid) >= bottom) hi = mid;
31543
+ else lo = mid + 1;
31544
+ }
31545
+ const lastHit = Math.max(firstHit, lo - 1);
31546
+ return {
31547
+ first: Math.max(0, firstHit - margin),
31548
+ last: Math.min(reader.count - 1, lastHit + margin)
31549
+ };
31550
+ }
31551
+ //#endregion
30521
31552
  //#region src/tree/tree-base.ts
30522
31553
  /** Above this many rows entering / leaving at once, skip the animation entirely. */
30523
31554
  const MAX_ANIMATED_ROWS = 50;
31555
+ const EMPTY_FLAT = {
31556
+ rows: [],
31557
+ indexByKey: /* @__PURE__ */ new Map()
31558
+ };
31559
+ /** Viewport height assumed until the container has been measured (or when it reads 0). */
31560
+ const FALLBACK_VIEWPORT_HEIGHT = 600;
30524
31561
  let TreeBaseElement = class TreeBaseElement extends LitElement {
30525
31562
  constructor(..._args) {
30526
31563
  super(..._args);
@@ -30534,11 +31571,33 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30534
31571
  this.dndExpandHoverMs = 500;
30535
31572
  this.reorderable = true;
30536
31573
  this.stickyHeaders = true;
31574
+ this.virtualize = `auto`;
31575
+ this.virtualizeThreshold = 200;
30537
31576
  this.applyInternalDrop = true;
30538
31577
  this._expandedKeys = /* @__PURE__ */ new Set();
31578
+ this.#structureVersion = 0;
31579
+ this._selectionVersion = 0;
31580
+ this._scrollTop = 0;
31581
+ this.#metrics = {
31582
+ rowHeight: 30,
31583
+ gap: 1,
31584
+ listOffset: 0
31585
+ };
31586
+ this.#viewportH = 0;
31587
+ this.#virtualScrollRaf = 0;
31588
+ this.#winStart = 0;
31589
+ this.#winEnd = 0;
31590
+ this.#scrollOnlyUpdate = false;
31591
+ this.#onVirtualScroll = () => {
31592
+ if (this.#virtualScrollRaf !== 0) return;
31593
+ this.#virtualScrollRaf = requestAnimationFrame(() => {
31594
+ this.#virtualScrollRaf = 0;
31595
+ this.#syncWindowToScroll();
31596
+ });
31597
+ };
30539
31598
  this.tickled = new TickledController(this, {
30540
31599
  getItems: () => {
30541
- return this.#getAllNodes().map(({ node }) => ({
31600
+ return this.#getFlat().rows.map(({ node }) => ({
30542
31601
  id: node.item.key,
30543
31602
  enabled: true
30544
31603
  }));
@@ -30555,6 +31614,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30555
31614
  }) });
30556
31615
  this.#config = { indent: 20 };
30557
31616
  this.#renderedKeys = [];
31617
+ this.#renderedRowKeys = [];
31618
+ this.#renderedRowIndex = /* @__PURE__ */ new Map();
30558
31619
  this.#lastNodeByKey = /* @__PURE__ */ new Map();
30559
31620
  this.#enteringKeys = /* @__PURE__ */ new Set();
30560
31621
  this.#leaving = /* @__PURE__ */ new Map();
@@ -30578,9 +31639,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30578
31639
  this.#stickyDirty = true;
30579
31640
  this.#stickyRaf = 0;
30580
31641
  this.#stickyScrollBound = false;
31642
+ this.#revealToken = 0;
30581
31643
  this.#onKeyDown = (event) => {
30582
31644
  if (this.search.isOpen) return;
30583
- const nodes = this.#getAllNodes();
31645
+ const nodes = this.#getFlat().rows;
30584
31646
  if (nodes.length === 0) return;
30585
31647
  if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
30586
31648
  event.preventDefault();
@@ -30592,29 +31654,41 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30592
31654
  `ArrowUp`,
30593
31655
  `ArrowLeft`,
30594
31656
  `ArrowRight`,
31657
+ `Home`,
31658
+ `End`,
31659
+ `PageUp`,
31660
+ `PageDown`,
30595
31661
  `Enter`,
30596
31662
  ` `
30597
31663
  ].includes(event.key)) this.tickled.setInputMode(`keyboard`);
30598
31664
  const currentIdx = this.#getFocusedIndex();
30599
31665
  switch (event.key) {
30600
- case `ArrowDown`: {
31666
+ case `ArrowDown`:
30601
31667
  event.preventDefault();
30602
- const nextIdx = Math.min(currentIdx + 1, nodes.length - 1);
30603
- if (this.interactionMode === `implicit`) this.select(nodes[nextIdx].node);
30604
- this.#setFocusedIndex(nextIdx);
30605
- if (event.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, nextIdx);
30606
- else if (this.interactionMode === `vscode`) this.#anchor = void 0;
30607
- this.requestUpdate();
31668
+ this.#navigateToIndex(nodes, currentIdx, Math.min(currentIdx + 1, nodes.length - 1), event.shiftKey);
31669
+ break;
31670
+ case `ArrowUp`:
31671
+ event.preventDefault();
31672
+ this.#navigateToIndex(nodes, currentIdx, Math.max(currentIdx - 1, 0), event.shiftKey);
31673
+ break;
31674
+ case `Home`:
31675
+ event.preventDefault();
31676
+ this.#navigateToIndex(nodes, currentIdx, 0, event.shiftKey);
31677
+ break;
31678
+ case `End`:
31679
+ event.preventDefault();
31680
+ this.#navigateToIndex(nodes, currentIdx, nodes.length - 1, event.shiftKey);
31681
+ break;
31682
+ case `PageDown`: {
31683
+ event.preventDefault();
31684
+ const page = this.#pageSize();
31685
+ this.#navigateToIndex(nodes, currentIdx, Math.min(currentIdx + page, nodes.length - 1), event.shiftKey);
30608
31686
  break;
30609
31687
  }
30610
- case `ArrowUp`: {
31688
+ case `PageUp`: {
30611
31689
  event.preventDefault();
30612
- const prevIdx = Math.max(currentIdx - 1, 0);
30613
- if (this.interactionMode === `implicit`) this.select(nodes[prevIdx].node);
30614
- this.#setFocusedIndex(prevIdx);
30615
- if (event.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, prevIdx);
30616
- else if (this.interactionMode === `vscode`) this.#anchor = void 0;
30617
- this.requestUpdate();
31690
+ const page = this.#pageSize();
31691
+ this.#navigateToIndex(nodes, currentIdx, Math.max(currentIdx - page, 0), event.shiftKey);
30618
31692
  break;
30619
31693
  }
30620
31694
  case `ArrowRight`: {
@@ -30631,7 +31705,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30631
31705
  event.preventDefault();
30632
31706
  const current = nodes[currentIdx];
30633
31707
  if (current && this._expandedKeys.has(current.node.item.key)) {
30634
- this._expandedKeys.delete(current.node.item.key);
31708
+ this.#setExpanded(withoutKey(this._expandedKeys, current.node.item.key));
30635
31709
  this.dispatchEvent(new CustomEvent(`collapse`, {
30636
31710
  detail: {
30637
31711
  node: current.node,
@@ -30685,7 +31759,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30685
31759
  if (!li) return;
30686
31760
  const key = li.dataset.key;
30687
31761
  if (!key) return;
30688
- const node = this.#getAllNodes().find((n) => n.node.item.key === key);
31762
+ const flat = this.#getFlat();
31763
+ const idx = flat.indexByKey.get(key);
31764
+ const node = idx === void 0 ? void 0 : flat.rows[idx];
30689
31765
  if (!node) return;
30690
31766
  const detail = {
30691
31767
  node: node.node,
@@ -30706,6 +31782,206 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30706
31782
  get selectedNodes() {
30707
31783
  return this._selectedNodes;
30708
31784
  }
31785
+ #setExpanded(next) {
31786
+ if (next === this._expandedKeys) return;
31787
+ this._expandedKeys = next;
31788
+ this.#flat = void 0;
31789
+ this.#stickyDirty = true;
31790
+ }
31791
+ #flat;
31792
+ #flatRoot;
31793
+ #flatFilter;
31794
+ #structureVersion;
31795
+ #treeIndexCache;
31796
+ #filterCache;
31797
+ /**
31798
+ * Call after editing `node.children` / `node.item` in place (outside
31799
+ * `TreeDataModel`). Drops the cached whole-tree indexes and re-renders.
31800
+ */
31801
+ invalidateStructure() {
31802
+ this.#structureVersion++;
31803
+ this.#flat = void 0;
31804
+ this.requestUpdate();
31805
+ }
31806
+ /** Visible rows in display order (cached for the current update). */
31807
+ #getFlat() {
31808
+ if (!this.root) return EMPTY_FLAT;
31809
+ if (this.#flat && this.#flatRoot === this.root && this.#flatFilter === this.filterPredicate) return this.#flat;
31810
+ const filter = this.#getFilter();
31811
+ this.#flat = flattenTree(this.root, {
31812
+ isExpanded: (node, depth) => this._isExpanded(node, depth),
31813
+ isShown: filter ? (node) => filter.matches.has(node.item.key) : () => true
31814
+ });
31815
+ this.#flatRoot = this.root;
31816
+ this.#flatFilter = this.filterPredicate;
31817
+ return this.#flat;
31818
+ }
31819
+ /** Whole-loaded-tree index (parents, icon depths); cached on `(root, structureVersion)`. */
31820
+ #getTreeIndex() {
31821
+ const root = this.root;
31822
+ if (!root) return {
31823
+ parentByKey: /* @__PURE__ */ new Map(),
31824
+ depthsWithIcons: /* @__PURE__ */ new Set()
31825
+ };
31826
+ const cached = this.#treeIndexCache;
31827
+ if (cached && cached.root === root && cached.version === this.#structureVersion) return cached.index;
31828
+ const index = indexTree(root);
31829
+ this.#treeIndexCache = {
31830
+ root,
31831
+ version: this.#structureVersion,
31832
+ index
31833
+ };
31834
+ return index;
31835
+ }
31836
+ /** Row height / gap / list offset as last measured from CSS + layout. */
31837
+ #metrics;
31838
+ /** Last non-zero container height (a hidden dock tab reads 0). */
31839
+ #viewportH;
31840
+ #virtualScrollRaf;
31841
+ #virtualScrollContainer;
31842
+ #resizeObserver;
31843
+ #winStart;
31844
+ #winEnd;
31845
+ /** Set in `willUpdate` when only the scroll offset changed; consumed in `updated`. */
31846
+ #scrollOnlyUpdate;
31847
+ /** True when the current visible row count should be windowed. */
31848
+ #virtualActive() {
31849
+ if (this.virtualize === `off`) return false;
31850
+ if (this.virtualize === `on`) return true;
31851
+ return this.#getFlat().rows.length > this.virtualizeThreshold;
31852
+ }
31853
+ #layoutParams(count) {
31854
+ return {
31855
+ count,
31856
+ rowHeight: this.#metrics.rowHeight,
31857
+ gap: this.#metrics.gap,
31858
+ listOffset: this.#metrics.listOffset,
31859
+ viewportHeight: this.#viewportH > 0 ? this.#viewportH : FALLBACK_VIEWPORT_HEIGHT
31860
+ };
31861
+ }
31862
+ /**
31863
+ * Re-read the CSS metrics and viewport height. Returns true when anything
31864
+ * that changes row positions moved (the caller re-renders).
31865
+ */
31866
+ #measureMetrics() {
31867
+ const list = this.renderRoot.querySelector(`ul.tree-list`);
31868
+ const sample = list?.querySelector(`li.item:not(.leaving)`);
31869
+ const measuredHeight = sample?.getBoundingClientRect().height ?? 0;
31870
+ const rowHeight = measuredHeight > 0 ? measuredHeight : this.#getItemHeight();
31871
+ const measuredGap = sample ? Number.parseFloat(getComputedStyle(sample).marginTop) : NaN;
31872
+ const parsedGap = Number.isNaN(measuredGap) ? Number.parseFloat(getComputedStyle(this).getPropertyValue(`--tree-item-gap`)) : measuredGap;
31873
+ const gap = Number.isNaN(parsedGap) ? 1 : parsedGap;
31874
+ let listOffset = this.#metrics.listOffset;
31875
+ if (list) {
31876
+ const padTop = Number.parseFloat(getComputedStyle(list).paddingTop);
31877
+ listOffset = list.offsetTop + (Number.isNaN(padTop) ? 0 : padTop);
31878
+ }
31879
+ const height = this.#container()?.clientHeight ?? 0;
31880
+ const heightChanged = height > 0 && height !== this.#viewportH;
31881
+ if (height > 0) this.#viewportH = height;
31882
+ const prev = this.#metrics;
31883
+ const changed = prev.rowHeight !== rowHeight || prev.gap !== gap || prev.listOffset !== listOffset;
31884
+ this.#metrics = {
31885
+ rowHeight,
31886
+ gap,
31887
+ listOffset
31888
+ };
31889
+ return changed || heightChanged;
31890
+ }
31891
+ /** The window for the current scroll offset. */
31892
+ #currentWindow(count) {
31893
+ return computeTreeWindow(this.#layoutParams(count), this._scrollTop);
31894
+ }
31895
+ /** Pull the container's real scroll offset into state (programmatic and clamped scrolls included). */
31896
+ #syncScrollTopFromContainer() {
31897
+ const top = this.#container()?.scrollTop;
31898
+ if (top !== void 0 && top !== this._scrollTop) this._scrollTop = top;
31899
+ }
31900
+ #onVirtualScroll;
31901
+ /** Re-render only when the scroll has moved the window; scrolling inside it is free. */
31902
+ #syncWindowToScroll() {
31903
+ const container = this.#container();
31904
+ if (!container || !this.root || !this.#virtualActive()) return;
31905
+ const top = container.scrollTop;
31906
+ if (top === this._scrollTop) return;
31907
+ const next = computeTreeWindow(this.#layoutParams(this.#getFlat().rows.length), top);
31908
+ if (next.start !== this.#winStart || next.end !== this.#winEnd) this._scrollTop = top;
31909
+ }
31910
+ /** Bind the window's scroll listener and resize observer to the container once it exists. */
31911
+ #syncVirtualObservers() {
31912
+ const container = this.#container();
31913
+ if (container === this.#virtualScrollContainer) return;
31914
+ this.#virtualScrollContainer?.removeEventListener(`scroll`, this.#onVirtualScroll);
31915
+ this.#resizeObserver?.disconnect();
31916
+ this.#resizeObserver = void 0;
31917
+ this.#virtualScrollContainer = container;
31918
+ if (!container) return;
31919
+ container.addEventListener(`scroll`, this.#onVirtualScroll, { passive: true });
31920
+ if (typeof ResizeObserver !== `undefined`) {
31921
+ this.#resizeObserver = new ResizeObserver((entries) => {
31922
+ const height = entries.at(-1)?.contentRect.height ?? 0;
31923
+ if (height > 0 && height !== this.#viewportH && this.#virtualActive()) this.requestUpdate();
31924
+ });
31925
+ this.#resizeObserver.observe(container);
31926
+ }
31927
+ }
31928
+ #teardownVirtualObservers() {
31929
+ this.#virtualScrollContainer?.removeEventListener(`scroll`, this.#onVirtualScroll);
31930
+ this.#virtualScrollContainer = void 0;
31931
+ this.#resizeObserver?.disconnect();
31932
+ this.#resizeObserver = void 0;
31933
+ if (this.#virtualScrollRaf !== 0) {
31934
+ cancelAnimationFrame(this.#virtualScrollRaf);
31935
+ this.#virtualScrollRaf = 0;
31936
+ }
31937
+ }
31938
+ /**
31939
+ * Scroll flat row `index` into view (below the sticky overlay). Writes the
31940
+ * container's `scrollTop` and `_scrollTop` together, so the very next render
31941
+ * already contains the target row — no frame where it is missing.
31942
+ */
31943
+ #revealIndex(index) {
31944
+ const container = this.#container();
31945
+ if (!container) return;
31946
+ const params = this.#layoutParams(this.#getFlat().rows.length);
31947
+ const inset = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
31948
+ const next = treeScrollTopToReveal(params, index, container.scrollTop, inset);
31949
+ if (next === container.scrollTop && next === this._scrollTop) return;
31950
+ container.scrollTop = next;
31951
+ this._scrollTop = next;
31952
+ }
31953
+ /** Filter match sets, or `undefined` when no filter is active. */
31954
+ #getFilter() {
31955
+ const predicate = this.filterPredicate;
31956
+ const root = this.root;
31957
+ if (!predicate || !root) return void 0;
31958
+ const cached = this.#filterCache;
31959
+ if (cached && cached.predicate === predicate && cached.root === root && cached.version === this.#structureVersion) return cached;
31960
+ const matches = computeFilterMatches(root, predicate);
31961
+ const { parentByKey } = this.#getTreeIndex();
31962
+ const autoExpanded = /* @__PURE__ */ new Set();
31963
+ for (const key of matches) {
31964
+ const parent = parentByKey.get(key);
31965
+ if (parent !== void 0) autoExpanded.add(parent);
31966
+ }
31967
+ const next = {
31968
+ predicate,
31969
+ root,
31970
+ version: this.#structureVersion,
31971
+ matches,
31972
+ autoExpanded
31973
+ };
31974
+ this.#filterCache = next;
31975
+ return next;
31976
+ }
31977
+ /** Structure version for cache keys in subclasses (bumped on root / model change and lazy loads). */
31978
+ get _structureVersion() {
31979
+ return this.#structureVersion;
31980
+ }
31981
+ /** Depths at which any loaded node carries an icon (stable across expansion). */
31982
+ get _depthsWithIcons() {
31983
+ return this.#getTreeIndex().depthsWithIcons;
31984
+ }
30709
31985
  #unregisterSearchCommands;
30710
31986
  #effectiveSearchModes() {
30711
31987
  const raw = this.searchModes?.trim();
@@ -30726,6 +32002,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30726
32002
  #ownedRegistry;
30727
32003
  #config;
30728
32004
  #renderedKeys;
32005
+ /** Keys in the exact DOM order of the last render (ghost rows woven in). */
32006
+ #renderedRowKeys;
32007
+ #renderedRowIndex;
30729
32008
  #lastNodeByKey;
30730
32009
  #enteringKeys;
30731
32010
  #leaving;
@@ -30760,73 +32039,65 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30760
32039
  }
30761
32040
  }
30762
32041
  #onStickyScroll;
30763
- /** Real (non-sticky) row element for `key`, or `null`. */
30764
- #stickyRowEl(key) {
30765
- return this.renderRoot.querySelector(`ul.tree-list li[data-key="${CSS.escape(key)}"]`);
32042
+ /**
32043
+ * Row geometry over the rendered `<li>` elements (ghost rows included), in
32044
+ * container content coordinates. `#container` is `position: relative`, so
32045
+ * `offsetTop` is scroll-independent. Reads only — no layout writes.
32046
+ */
32047
+ #rowReader(rows) {
32048
+ return {
32049
+ count: rows.length,
32050
+ topOf: (i) => rows[i].offsetTop,
32051
+ bottomOf: (i) => rows[i].offsetTop + rows[i].offsetHeight
32052
+ };
30766
32053
  }
30767
32054
  /**
30768
32055
  * Recompute which ancestors stick to the top: the ancestors of the first
30769
32056
  * row whose bottom clears the container top, minus any ancestor that is
30770
32057
  * itself still visible below the overlay. Synchronous — reads current
30771
- * geometry, renders nothing, expands nothing.
32058
+ * geometry, renders nothing, expands nothing. O(log N) offset reads + O(depth).
30772
32059
  */
30773
32060
  #updateStickyHeaders() {
30774
32061
  this.#stickyDirty = false;
30775
32062
  const empty = /* @__PURE__ */ new Map();
30776
- if (!this.stickyHeaders || !this.root) {
30777
- this.#setStickyKeys([], empty);
30778
- return;
30779
- }
32063
+ const clear = () => this.#setStickyKeys([], empty);
32064
+ if (!this.stickyHeaders || !this.root) return clear();
30780
32065
  const container = this.#container();
30781
- if (!container) {
30782
- this.#setStickyKeys([], empty);
30783
- return;
30784
- }
30785
- const nodes = this.#getAllNodes();
30786
- if (nodes.length === 0) {
30787
- this.#setStickyKeys([], empty);
30788
- return;
30789
- }
30790
- const parent = /* @__PURE__ */ new Map();
30791
- const walk = (n) => {
30792
- for (const child of n.children ?? []) {
30793
- parent.set(child.item.key, n.item.key);
30794
- walk(child);
30795
- }
30796
- };
30797
- walk(this.root);
30798
- const containerTop = container.getBoundingClientRect().top;
30799
- const visibleThreshold = containerTop + (this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0);
30800
- let topKey;
30801
- for (const { node } of nodes) {
30802
- const li = this.#stickyRowEl(node.item.key);
30803
- if (!li || li.classList.contains(`leaving`)) continue;
30804
- if (li.getBoundingClientRect().bottom > containerTop) {
30805
- topKey = node.item.key;
30806
- break;
30807
- }
30808
- }
30809
- if (topKey === void 0) {
30810
- this.#setStickyKeys([], empty);
30811
- return;
30812
- }
30813
- const rootKey = this.root.item.key;
32066
+ const list = this.renderRoot.querySelector(`ul.tree-list`);
32067
+ if (!container || !list) return clear();
32068
+ const flat = this.#getFlat();
32069
+ const virtual = this.#virtualActive();
32070
+ const rows = list.children;
32071
+ const keys = this.#renderedRowKeys;
32072
+ if (flat.rows.length === 0) return clear();
32073
+ if (!virtual && (rows.length === 0 || rows.length !== keys.length)) return clear();
32074
+ const reader = virtual ? treeRowReader(this.#layoutParams(flat.rows.length)) : this.#rowReader(rows);
32075
+ const scrollTop = container.scrollTop;
32076
+ const visibleThreshold = scrollTop + (this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0);
32077
+ let topIdx = firstRowBelow(reader, scrollTop);
32078
+ if (!virtual) while (topIdx < rows.length && rows[topIdx].classList.contains(`leaving`)) topIdx++;
32079
+ if (topIdx >= reader.count) return clear();
32080
+ const topFlat = virtual ? topIdx : flat.indexByKey.get(keys[topIdx]);
32081
+ if (topFlat === void 0) return clear();
30814
32082
  const chain = [];
30815
- let cursor = parent.get(topKey);
30816
- while (cursor !== void 0 && cursor !== rootKey) {
30817
- chain.unshift(cursor);
30818
- cursor = parent.get(cursor);
32083
+ let cursor = flat.rows[topFlat].parentKey;
32084
+ while (cursor !== void 0) {
32085
+ const idx = flat.indexByKey.get(cursor);
32086
+ if (idx === void 0) break;
32087
+ chain.unshift(flat.rows[idx]);
32088
+ cursor = flat.rows[idx].parentKey;
30819
32089
  }
30820
- const byKey = new Map(nodes.map((entry) => [entry.node.item.key, entry]));
30821
32090
  const entries = /* @__PURE__ */ new Map();
30822
32091
  const stuck = [];
30823
- for (const key of chain) {
30824
- const li = this.#stickyRowEl(key);
30825
- if (!li || li.getBoundingClientRect().bottom > visibleThreshold) continue;
30826
- const entry = byKey.get(key);
30827
- if (!entry) continue;
32092
+ for (const row of chain) {
32093
+ const key = row.node.item.key;
32094
+ const domIdx = virtual ? flat.indexByKey.get(key) : this.#renderedRowIndex.get(key);
32095
+ if (domIdx === void 0 || reader.bottomOf(domIdx) > visibleThreshold) continue;
30828
32096
  stuck.push(key);
30829
- entries.set(key, entry);
32097
+ entries.set(key, {
32098
+ node: row.node,
32099
+ depth: row.depth
32100
+ });
30830
32101
  }
30831
32102
  this.#setStickyKeys(stuck, entries);
30832
32103
  }
@@ -30839,12 +32110,14 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30839
32110
  #renderStickyHeaders() {
30840
32111
  if (!this.stickyHeaders || this._stickyKeys.length === 0) return nothing;
30841
32112
  return html`
30842
- <div class="sticky-headers" part="sticky-headers">
30843
- ${this._stickyKeys.map((key) => {
32113
+ <div class="sticky-anchor">
32114
+ <div class="sticky-headers" part="sticky-headers">
32115
+ ${this._stickyKeys.map((key) => {
30844
32116
  const entry = this.#stickyEntries.get(key);
30845
32117
  if (!entry) return nothing;
30846
32118
  return this.#renderStickyHeader(entry.node, entry.depth);
30847
32119
  })}
32120
+ </div>
30848
32121
  </div>
30849
32122
  `;
30850
32123
  }
@@ -30916,15 +32189,44 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30916
32189
  }
30917
32190
  willUpdate(changedProps) {
30918
32191
  super.willUpdate(changedProps);
30919
- this.#diffVisibleNodes();
32192
+ const scrollOnly = this.#flat !== void 0 && changedProps.size === 1 && changedProps.has(`_scrollTop`) && this.#virtualActive();
32193
+ this.#scrollOnlyUpdate = scrollOnly;
32194
+ if (scrollOnly) {
32195
+ this.#syncScrollTopFromContainer();
32196
+ return;
32197
+ }
32198
+ this.#flat = void 0;
32199
+ if (changedProps.has(`root`) || changedProps.has(`model`)) this.#structureVersion++;
32200
+ this.#diffVisibleNodes(changedProps.has(`filterPredicate`));
32201
+ const virtual = this.#virtualActive();
32202
+ this.toggleAttribute(`data-virtual`, virtual);
32203
+ if (virtual) {
32204
+ this.#measureMetrics();
32205
+ this.#syncScrollTopFromContainer();
32206
+ }
30920
32207
  if (changedProps.has(`root`) || changedProps.has(`model`) || changedProps.has(`filterPredicate`) || changedProps.has(`stickyHeaders`)) this.#stickyDirty = true;
30921
32208
  if (this.#stickyDirty) this.#updateStickyHeaders();
30922
32209
  }
30923
- #diffVisibleNodes() {
30924
- const nodes = this.root ? this.#getAllNodes() : [];
32210
+ /**
32211
+ * `filterChanged`: typing in a filter adds / removes whole rows from the DOM.
32212
+ * That is not an expand / collapse, so skip the enter / leave animations
32213
+ * (and any ghosts) for this update; `#renderedKeys` is still refreshed.
32214
+ */
32215
+ #diffVisibleNodes(filterChanged) {
32216
+ const nodes = this.#getFlat().rows;
32217
+ if (this.#virtualActive()) {
32218
+ this.#enteringKeys.clear();
32219
+ this.#cancelAllLeavingAnimations();
32220
+ this.#leaving.clear();
32221
+ this.#renderedKeys = [];
32222
+ return;
32223
+ }
30925
32224
  const nowKeys = new Set(nodes.map((n) => n.node.item.key));
30926
32225
  this.#enteringKeys.clear();
30927
- if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
32226
+ if (filterChanged) {
32227
+ this.#cancelAllLeavingAnimations();
32228
+ this.#leaving.clear();
32229
+ } else if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
30928
32230
  const prevSet = new Set(this.#renderedKeys);
30929
32231
  for (const { node } of nodes) if (!prevSet.has(node.item.key)) this.#enteringKeys.add(node.item.key);
30930
32232
  if (this.#enteringKeys.size > MAX_ANIMATED_ROWS) this.#enteringKeys.clear();
@@ -30947,30 +32249,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
30947
32249
  return this.#leaving.has(node.item.key);
30948
32250
  }
30949
32251
  /**
30950
- * True when `filterPredicate` is set and neither `node` nor any descendant
30951
- * matches — the row should carry the `filtered` class (`display: none`).
30952
- */
30953
- _isFilteredOut(node, depth) {
30954
- return this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
30955
- }
30956
- /** Splice the fading ghost rows back into the visible list near where they were. */
30957
- #weaveLeaving(nodes) {
30958
- if (this.#leaving.size === 0) return nodes;
30959
- const result = [...nodes];
30960
- const indexOfKey = (key) => result.findIndex((e) => e.node.item.key === key);
30961
- for (const [key, entry] of this.#leaving) {
30962
- const prevIdx = this.#renderedKeys.indexOf(key);
30963
- let insertAt = result.length;
30964
- for (let j = prevIdx + 1; j < this.#renderedKeys.length; j++) {
30965
- const at = indexOfKey(this.#renderedKeys[j]);
30966
- if (at !== -1) {
30967
- insertAt = at;
30968
- break;
30969
- }
30970
- }
30971
- result.splice(insertAt, 0, entry);
30972
- }
30973
- return result;
32252
+ * Kept for subclass compatibility. Filtered-out rows are no longer rendered
32253
+ * at all (they are omitted from the flat list), so this is `false` for every
32254
+ * rendered row.
32255
+ */
32256
+ _isFilteredOut(_node, _depth) {
32257
+ return false;
30974
32258
  }
30975
32259
  #runItemAnimations() {
30976
32260
  if (this.#enteringKeys.size > 0) {
@@ -31010,7 +32294,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31010
32294
  });
31011
32295
  });
31012
32296
  }
31013
- const visible = this.root ? this.#getAllNodes() : [];
32297
+ const visible = this.#getFlat().rows;
31014
32298
  this.#renderedKeys = visible.map((n) => n.node.item.key);
31015
32299
  this.#lastNodeByKey.clear();
31016
32300
  for (const entry of visible) this.#lastNodeByKey.set(entry.node.item.key, entry);
@@ -31018,10 +32302,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31018
32302
  }
31019
32303
  updated(changedProps) {
31020
32304
  this.#rowsCache = void 0;
32305
+ this.#syncVirtualObservers();
32306
+ if (this.#scrollOnlyUpdate) {
32307
+ this.#scrollOnlyUpdate = false;
32308
+ this.#repaintSearchHighlights();
32309
+ return;
32310
+ }
31021
32311
  this.#syncStickyScroll();
31022
32312
  if (changedProps.has(`root`) && this.root) {
31023
32313
  this.#abortPending();
31024
- this._expandedKeys.clear();
32314
+ this.#setExpanded(/* @__PURE__ */ new Set());
31025
32315
  this.#clearSelectionInternal();
31026
32316
  this.tickled.clearTickled();
31027
32317
  this._focusedDepth = 0;
@@ -31041,8 +32331,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31041
32331
  this.requestUpdate();
31042
32332
  } else if (event.type === `invalidate`) {
31043
32333
  if (this._expandedKeys.has(event.key)) {
31044
- this._expandedKeys.delete(event.key);
31045
- this.#stickyDirty = true;
32334
+ this.#setExpanded(withoutKey(this._expandedKeys, event.key));
31046
32335
  this.requestUpdate();
31047
32336
  }
31048
32337
  }
@@ -31056,7 +32345,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31056
32345
  if (changedProps.has(`selectedNode`) && this.selectedNode) {
31057
32346
  if (changedProps.get(`selectedNode`) !== this.selectedNode) this.tickled.setTickled(this.selectedNode.item.key);
31058
32347
  }
31059
- this.#runItemAnimations();
32348
+ if (this.#virtualActive()) {
32349
+ this.#renderedKeys = [];
32350
+ this.#lastNodeByKey.clear();
32351
+ if (this.#measureMetrics()) queueMicrotask(() => this.requestUpdate());
32352
+ this.#repaintSearchHighlights();
32353
+ } else this.#runItemAnimations();
32354
+ }
32355
+ /** Highlights are painted on rendered labels, so they move with the window. */
32356
+ #repaintSearchHighlights() {
32357
+ if (this.search.isOpen) this.search.repaintHighlights();
31060
32358
  }
31061
32359
  connectedCallback() {
31062
32360
  super.connectedCallback();
@@ -31088,8 +32386,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31088
32386
  return target.closest(`ul.tree-list li[data-key]`)?.getAttribute(`data-key`) ?? void 0;
31089
32387
  },
31090
32388
  prepareSelection: (originKey) => {
31091
- const allNodes = this.#getAllNodes();
31092
- const originEntry = allNodes.find((n) => n.node.item.key === originKey);
32389
+ const flat = this.#getFlat();
32390
+ const allNodes = flat.rows;
32391
+ const originIdx = flat.indexByKey.get(originKey);
32392
+ const originEntry = originIdx === void 0 ? void 0 : allNodes[originIdx];
31093
32393
  if (!originEntry) return void 0;
31094
32394
  const selectedNodes = new Set(this._selectedNodes);
31095
32395
  if (!selectedNodes.has(originEntry.node)) selectedNodes.clear();
@@ -31178,6 +32478,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31178
32478
  getDndMode: () => this.itemDnd,
31179
32479
  getExpandHoverMs: () => this.dndExpandHoverMs,
31180
32480
  getVisibleRows: () => this.#getVisibleRows(),
32481
+ getRowInfo: (key) => {
32482
+ const flat = this.#getFlat();
32483
+ const idx = flat.indexByKey.get(key);
32484
+ if (idx === void 0) return void 0;
32485
+ const { node, depth, parentKey } = flat.rows[idx];
32486
+ return {
32487
+ node,
32488
+ depth,
32489
+ parentKey
32490
+ };
32491
+ },
32492
+ getChildren: (parentKey) => this.#childrenOfKey(parentKey),
31181
32493
  getIndentPx: () => this.#getIndentPx(),
31182
32494
  getScrollContainer: () => this.#container(),
31183
32495
  isSelfOrDescendant: (ancestorKey, key) => this.#isSelfOrDescendant(ancestorKey, key),
@@ -31250,44 +32562,121 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31250
32562
  const parsed = Number.parseInt(getComputedStyle(this).getPropertyValue(`--tree-item-height`), 10);
31251
32563
  return Number.isNaN(parsed) ? 30 : parsed;
31252
32564
  }
31253
- /** Measured rows, cached for the duration of a drag and invalidated on scroll / update. */
31254
- #getVisibleRows() {
31255
- if (this.#rowsCache) return this.#rowsCache;
31256
- const rows = [];
31257
- for (const { node, depth } of this.#getAllNodes()) {
31258
- const li = this.shadowRoot?.querySelector(`ul.tree-list li[data-key="${CSS.escape(node.item.key)}"]`);
31259
- if (li) rows.push({
32565
+ /** Children of `parentKey` (the root's children when undefined), straight from the tree. */
32566
+ #childrenOfKey(parentKey) {
32567
+ if (parentKey === void 0) return this.root?.children ?? [];
32568
+ const flat = this.#getFlat();
32569
+ const idx = flat.indexByKey.get(parentKey);
32570
+ return idx === void 0 ? [] : flat.rows[idx].node.children ?? [];
32571
+ }
32572
+ /**
32573
+ * Windowed counterpart of the measured rows: rects come from the layout
32574
+ * arithmetic and one rendered row's horizontal extent, so nothing is looked
32575
+ * up per row and rows outside the window still resolve.
32576
+ */
32577
+ #collectVirtualDropRows(container, list, out) {
32578
+ const flat = this.#getFlat();
32579
+ const params = this.#layoutParams(flat.rows.length);
32580
+ const range = rowRangeOverlapping(treeRowReader(params), container.scrollTop, container.scrollTop + container.clientHeight, 1);
32581
+ if (!range) return;
32582
+ const containerRect = container.getBoundingClientRect();
32583
+ const sample = list.querySelector(`li.item`)?.getBoundingClientRect() ?? containerRect;
32584
+ for (let i = range.first; i <= range.last; i++) {
32585
+ const { node, depth } = flat.rows[i];
32586
+ const top = containerRect.top - container.scrollTop + treeRowTop(params, i);
32587
+ const rect = {
32588
+ x: sample.left,
32589
+ y: top,
32590
+ left: sample.left,
32591
+ right: sample.left + sample.width,
32592
+ top,
32593
+ bottom: top + params.rowHeight,
32594
+ width: sample.width,
32595
+ height: params.rowHeight,
32596
+ toJSON: () => ({})
32597
+ };
32598
+ out.push({
31260
32599
  key: node.item.key,
31261
32600
  node,
31262
32601
  depth,
31263
- rect: li.getBoundingClientRect()
32602
+ index: i,
32603
+ rect
31264
32604
  });
31265
32605
  }
32606
+ }
32607
+ /**
32608
+ * Measured rows for the part of the list that intersects the scroll
32609
+ * container (plus one row of margin each side), cached for the duration of a
32610
+ * drag and invalidated on scroll / update. Each row carries its flat index;
32611
+ * structural lookups (parents, siblings) come from `getRowInfo` / `getChildren`.
32612
+ */
32613
+ #getVisibleRows() {
32614
+ if (this.#rowsCache) return this.#rowsCache;
32615
+ const rows = [];
32616
+ const container = this.#container();
32617
+ const list = this.renderRoot.querySelector(`ul.tree-list`);
32618
+ const keys = this.#renderedRowKeys;
32619
+ if (container && list && this.#virtualActive()) this.#collectVirtualDropRows(container, list, rows);
32620
+ else if (container && list && list.children.length === keys.length) {
32621
+ const flat = this.#getFlat();
32622
+ const items = list.children;
32623
+ const range = rowRangeOverlapping(this.#rowReader(items), container.scrollTop, container.scrollTop + container.clientHeight, 1);
32624
+ if (range) for (let i = range.first; i <= range.last; i++) {
32625
+ const li = items[i];
32626
+ if (li.classList.contains(`leaving`)) continue;
32627
+ const flatIdx = flat.indexByKey.get(keys[i]);
32628
+ if (flatIdx === void 0) continue;
32629
+ const { node, depth } = flat.rows[flatIdx];
32630
+ rows.push({
32631
+ key: node.item.key,
32632
+ node,
32633
+ depth,
32634
+ index: flatIdx,
32635
+ rect: li.getBoundingClientRect()
32636
+ });
32637
+ }
32638
+ }
31266
32639
  this.#rowsCache = rows;
31267
32640
  return rows;
31268
32641
  }
31269
32642
  /** Route spring-load through the real expand path so lazy children load and `expand` fires with the right depth. */
31270
32643
  #springLoadExpand(key) {
31271
32644
  if (this._expandedKeys.has(key)) return;
31272
- const entry = this.#getAllNodes().find((n) => n.node.item.key === key);
32645
+ const flat = this.#getFlat();
32646
+ const idx = flat.indexByKey.get(key);
32647
+ const entry = idx === void 0 ? void 0 : flat.rows[idx];
31273
32648
  if (!entry || entry.node.item.isLeaf === true) return;
31274
32649
  this.#toggleExpand(entry.node, entry.depth);
31275
32650
  this.#rowsCache = void 0;
31276
32651
  this.requestUpdate();
31277
32652
  }
32653
+ /** O(depth): walk up the whole-tree parent map. */
31278
32654
  #isSelfOrDescendant(ancestorKey, key) {
31279
32655
  if (ancestorKey === key) return true;
31280
- let node = this.#getAllNodes().find((n) => n.node.item.key === key)?.node;
31281
- let parent = node ? this.#findParentByKey(node.item.key) : void 0;
31282
- while (parent) {
31283
- if (parent.item.key === ancestorKey) return true;
31284
- node = parent;
31285
- parent = this.#findParentByKey(node.item.key);
32656
+ const { parentByKey } = this.#getTreeIndex();
32657
+ let cursor = parentByKey.get(key);
32658
+ while (cursor !== void 0) {
32659
+ if (cursor === ancestorKey) return true;
32660
+ cursor = parentByKey.get(cursor);
31286
32661
  }
31287
32662
  return false;
31288
32663
  }
31289
32664
  #findParentByKey(key) {
31290
- for (const { node } of this.#getAllNodes()) if (node.children?.some((c) => c.item.key === key)) return node;
32665
+ const parentKey = this.#getTreeIndex().parentByKey.get(key);
32666
+ if (parentKey === void 0 || parentKey === this.root?.item.key) return void 0;
32667
+ return this.#nodeByKey(parentKey);
32668
+ }
32669
+ /** Node for a key anywhere in the loaded tree (via the flat list when visible). */
32670
+ #nodeByKey(key) {
32671
+ const flat = this.#getFlat();
32672
+ const idx = flat.indexByKey.get(key);
32673
+ if (idx !== void 0) return flat.rows[idx].node;
32674
+ const { parentByKey } = this.#getTreeIndex();
32675
+ const path = [];
32676
+ for (let cursor = key; cursor !== void 0 && cursor !== this.root?.item.key; cursor = parentByKey.get(cursor)) path.unshift(cursor);
32677
+ let node = this.root;
32678
+ for (const k of path) node = node?.children?.find((c) => c.item.key === k);
32679
+ return node;
31291
32680
  }
31292
32681
  /**
31293
32682
  * Decide whether a DataTransfer is acceptable and resolve the dragged set.
@@ -31311,7 +32700,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31311
32700
  return {
31312
32701
  source: `internal`,
31313
32702
  keys,
31314
- nodes: keys.map((k) => this.#getAllNodes().find((n) => n.node.item.key === k)?.node).filter((n) => n !== void 0)
32703
+ nodes: keys.map((k) => this.#nodeByKey(k)).filter((n) => n !== void 0)
31315
32704
  };
31316
32705
  }
31317
32706
  if (phase === `drop`) try {
@@ -31355,7 +32744,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31355
32744
  }
31356
32745
  #anyRowDraggable() {
31357
32746
  if (this.itemDnd === `none`) return false;
31358
- const allNodes = this.#getAllNodes();
32747
+ const allNodes = this.#getFlat().rows;
31359
32748
  if (allNodes.length === 0) return false;
31360
32749
  if (!this.canDrag) return true;
31361
32750
  return allNodes.some(({ node }) => this.canDrag({
@@ -31377,6 +32766,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31377
32766
  this.#postDragSuppressTimer = void 0;
31378
32767
  }
31379
32768
  this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
32769
+ this.#teardownVirtualObservers();
31380
32770
  if (this.#stickyScrollBound) {
31381
32771
  this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
31382
32772
  this.#stickyScrollContainer = void 0;
@@ -31430,12 +32820,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31430
32820
  }
31431
32821
  selectAll() {
31432
32822
  const prev = new Set(this._selectedNodes);
31433
- if (this.interactionMode === `checked`) for (const { node } of this.#getAllNodes()) {
32823
+ if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
31434
32824
  if (!this.#canSelect(node)) continue;
31435
32825
  this.#applyCheckedToggle(node, true);
31436
32826
  }
31437
32827
  else {
31438
- for (const { node } of this.#getAllNodes()) {
32828
+ for (const { node } of this.#getFlat().rows) {
31439
32829
  if (!this.#canSelect(node)) continue;
31440
32830
  this._selectedNodes.add(node);
31441
32831
  }
@@ -31446,7 +32836,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31446
32836
  }
31447
32837
  deselectAll() {
31448
32838
  const prev = new Set(this._selectedNodes);
31449
- if (this.interactionMode === `checked`) for (const { node } of this.#getAllNodes()) {
32839
+ if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
31450
32840
  if (!this.#canSelect(node)) continue;
31451
32841
  this.#applyCheckedToggle(node, false);
31452
32842
  }
@@ -31467,7 +32857,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31467
32857
  const found = children.find((n) => n.item.key === key);
31468
32858
  if (!found) return false;
31469
32859
  this.#applyExclusivity(found.item.key);
31470
- this._expandedKeys.add(found.item.key);
32860
+ this.#setExpanded(withKey(this._expandedKeys, found.item.key));
31471
32861
  if (key === keys.at(-1)) {
31472
32862
  this.select(found);
31473
32863
  this.tickled.setTickled(found.item.key);
@@ -31492,7 +32882,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31492
32882
  if (!path) return false;
31493
32883
  for (let i = 0; i < path.length - 1; i++) {
31494
32884
  this.#applyExclusivity(path[i]);
31495
- this._expandedKeys.add(path[i]);
32885
+ this.#setExpanded(withKey(this._expandedKeys, path[i]));
31496
32886
  }
31497
32887
  this.tickled.setTickled(key);
31498
32888
  this.#stickyDirty = true;
@@ -31528,10 +32918,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31528
32918
  return true;
31529
32919
  }
31530
32920
  #clearSelectionInternal() {
32921
+ this._selectionVersion++;
31531
32922
  this._selectedNodes.clear();
31532
32923
  this.selectedNode = void 0;
31533
32924
  }
31534
32925
  #dispatchSelect(previous) {
32926
+ this._selectionVersion++;
31535
32927
  const detail = {
31536
32928
  selected: new Set(this._selectedNodes),
31537
32929
  previous
@@ -31637,9 +33029,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31637
33029
  return;
31638
33030
  }
31639
33031
  if (isShift && this.#anchor) {
31640
- const visible = this.#getAllNodes();
31641
- const anchorIdx = visible.findIndex((n) => n.node === this.#anchor.node);
31642
- const targetIdx = visible.findIndex((n) => n.node === node);
33032
+ const flat = this.#getFlat();
33033
+ const visible = flat.rows;
33034
+ const anchorIdx = flat.indexByKey.get(this.#anchor.node.item.key) ?? -1;
33035
+ const targetIdx = flat.indexByKey.get(node.item.key) ?? -1;
31643
33036
  if (anchorIdx >= 0 && targetIdx >= 0) {
31644
33037
  const prev = new Set(this._selectedNodes);
31645
33038
  const from = Math.min(anchorIdx, targetIdx);
@@ -31689,9 +33082,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31689
33082
  return;
31690
33083
  }
31691
33084
  if (isShift && this.#anchor) {
31692
- const visible = this.#getAllNodes();
31693
- const anchorIdx = visible.findIndex((n) => n.node === this.#anchor.node);
31694
- const targetIdx = visible.findIndex((n) => n.node === node);
33085
+ const flat = this.#getFlat();
33086
+ const visible = flat.rows;
33087
+ const anchorIdx = flat.indexByKey.get(this.#anchor.node.item.key) ?? -1;
33088
+ const targetIdx = flat.indexByKey.get(node.item.key) ?? -1;
31695
33089
  if (anchorIdx >= 0 && targetIdx >= 0) {
31696
33090
  const prev = new Set(this._selectedNodes);
31697
33091
  const from = Math.min(anchorIdx, targetIdx);
@@ -31734,7 +33128,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31734
33128
  const isExpanded = this._expandedKeys.has(node.item.key);
31735
33129
  this.#stickyDirty = true;
31736
33130
  if (isExpanded) {
31737
- this._expandedKeys.delete(node.item.key);
33131
+ this.#setExpanded(withoutKey(this._expandedKeys, node.item.key));
31738
33132
  this.dispatchEvent(new CustomEvent(`collapse`, {
31739
33133
  detail: {
31740
33134
  node,
@@ -31753,7 +33147,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31753
33147
  }
31754
33148
  } else {
31755
33149
  this.#applyExclusivity(node.item.key);
31756
- this._expandedKeys.add(node.item.key);
33150
+ this.#setExpanded(withKey(this._expandedKeys, node.item.key));
31757
33151
  this.dispatchEvent(new CustomEvent(`expand`, {
31758
33152
  detail: {
31759
33153
  node,
@@ -31777,7 +33171,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31777
33171
  if (node.children) for (const child of node.children) this.#applyCheckedToggle(child, selected);
31778
33172
  }
31779
33173
  #getFlatIndexOf(node) {
31780
- return this.#getAllNodes().findIndex((n) => n.node === node);
33174
+ return this.#getFlat().indexByKey.get(node.item.key) ?? -1;
31781
33175
  }
31782
33176
  #extendRangeSelection(nodes, fromIdx, toIdx) {
31783
33177
  if (!this.#anchor) {
@@ -31788,7 +33182,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31788
33182
  };
31789
33183
  }
31790
33184
  if (!this.#anchor) return;
31791
- const anchorIdx = nodes.findIndex((n) => n.node === this.#anchor.node);
33185
+ const anchorIdx = this.#getFlat().indexByKey.get(this.#anchor.node.item.key) ?? -1;
31792
33186
  if (anchorIdx < 0) return;
31793
33187
  const from = Math.min(anchorIdx, toIdx);
31794
33188
  const to = Math.max(anchorIdx, toIdx);
@@ -31824,8 +33218,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31824
33218
  }, controller.signal), controller.signal)];
31825
33219
  if (controller.signal.aborted) return;
31826
33220
  node.children = children;
33221
+ this.#structureVersion++;
31827
33222
  this.#applyExclusivity(node.item.key);
31828
- this._expandedKeys.add(node.item.key);
33223
+ this.#setExpanded(withKey(this._expandedKeys, node.item.key));
33224
+ this.#flat = void 0;
31829
33225
  this.#stickyDirty = true;
31830
33226
  this.#pendingNode = void 0;
31831
33227
  this.#pendingController = void 0;
@@ -31841,6 +33237,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31841
33237
  } catch {
31842
33238
  if (controller.signal.aborted) return;
31843
33239
  node.children = [];
33240
+ this.#structureVersion++;
33241
+ this.#flat = void 0;
31844
33242
  this.#pendingNode = void 0;
31845
33243
  this.#pendingController = void 0;
31846
33244
  this.requestUpdate();
@@ -31852,33 +33250,44 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31852
33250
  _isFocused(node) {
31853
33251
  return this.tickled.isTickled(node.item.key);
31854
33252
  }
31855
- _isExpanded(node, depth = 0) {
33253
+ _isExpanded(node, _depth = 0) {
31856
33254
  if (this._expandedKeys.has(node.item.key)) return true;
31857
- if (this.filterPredicate && node.children) return node.children.some((child) => this.#matchesFilter(child, depth + 1));
31858
- return false;
31859
- }
31860
- #matchesFilter(node, depth) {
31861
- if (!this.filterPredicate) return true;
31862
- if (this.filterPredicate(node, depth)) return true;
31863
- return node.children?.some((child) => this.#matchesFilter(child, depth + 1)) ?? false;
33255
+ return this.#getFilter()?.autoExpanded.has(node.item.key) ?? false;
31864
33256
  }
31865
33257
  get _pendingNode() {
31866
33258
  return this.#pendingNode;
31867
33259
  }
31868
33260
  #scrollKeyIntoView(key) {
33261
+ if (this.#virtualActive()) {
33262
+ this.updateComplete.then(() => {
33263
+ const idx = this.#getFlat().indexByKey.get(key);
33264
+ if (idx !== void 0) this.#revealIndex(idx);
33265
+ });
33266
+ return;
33267
+ }
33268
+ const token = ++this.#revealToken;
31869
33269
  requestAnimationFrame(() => {
31870
- const container = this.#container();
31871
- const el = this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(key)}"]`);
31872
- if (!el || !container) return;
31873
- const elRect = el.getBoundingClientRect();
31874
- const containerRect = container.getBoundingClientRect();
31875
- const relativeTop = elRect.top - containerRect.top;
31876
- const relativeBottom = relativeTop + elRect.height;
31877
- const overlayHeight = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
31878
- if (relativeTop < overlayHeight) container.scrollTop += relativeTop - overlayHeight;
31879
- else if (relativeBottom > container.clientHeight) container.scrollTop += relativeBottom - container.clientHeight;
33270
+ this.#revealRowElement(key);
33271
+ const running = this.#container()?.getAnimations?.({ subtree: true }) ?? [];
33272
+ if (running.length > 0) Promise.allSettled(running.map((a) => a.finished)).then(() => {
33273
+ if (token === this.#revealToken) this.#revealRowElement(key);
33274
+ });
31880
33275
  });
31881
33276
  }
33277
+ #revealToken;
33278
+ /** Scroll the rendered row for `key` into the container, below the sticky overlay. No-op if it isn't rendered or is already visible. */
33279
+ #revealRowElement(key) {
33280
+ const container = this.#container();
33281
+ const el = this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(key)}"]`);
33282
+ if (!el || !container) return;
33283
+ const elRect = el.getBoundingClientRect();
33284
+ const containerRect = container.getBoundingClientRect();
33285
+ const relativeTop = elRect.top - containerRect.top;
33286
+ const relativeBottom = relativeTop + elRect.height;
33287
+ const overlayHeight = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
33288
+ if (relativeTop < overlayHeight) container.scrollTop += relativeTop - overlayHeight;
33289
+ else if (relativeBottom > container.clientHeight) container.scrollTop += relativeBottom - container.clientHeight;
33290
+ }
31882
33291
  #abortPending() {
31883
33292
  if (this.#pendingController) {
31884
33293
  this.#pendingController.abort();
@@ -31892,14 +33301,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31892
33301
  if (this.exclusivity === `depth`) this.#collapseSiblingsAtDepth(expandKey);
31893
33302
  else if (this.exclusivity === `global`) {
31894
33303
  const ancestorKeys = this.#findAncestorKeys(expandKey);
31895
- for (const key of [...this._expandedKeys]) if (!ancestorKeys.has(key) && key !== expandKey) this._expandedKeys.delete(key);
33304
+ this.#setExpanded(withoutKeys(this._expandedKeys, (key) => !ancestorKeys.has(key) && key !== expandKey));
31896
33305
  }
31897
33306
  }
31898
33307
  #collapseSiblingsAtDepth(expandKey) {
31899
33308
  if (!this.root) return;
31900
33309
  const parent = this.#findParent(this.root, expandKey);
31901
33310
  if (!parent?.children) return;
31902
- for (const child of parent.children) if (child.item.key !== expandKey && this._expandedKeys.has(child.item.key)) this._expandedKeys.delete(child.item.key);
33311
+ const siblingKeys = new Set(parent.children.map((c) => c.item.key));
33312
+ this.#setExpanded(withoutKeys(this._expandedKeys, (key) => key !== expandKey && siblingKeys.has(key)));
31903
33313
  }
31904
33314
  #findAncestorKeys(targetKey) {
31905
33315
  const keys = /* @__PURE__ */ new Set();
@@ -31923,30 +33333,13 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31923
33333
  if (found) return found;
31924
33334
  }
31925
33335
  }
31926
- #getVisibleNodes(node, depth) {
31927
- const result = [];
31928
- const visit = (n, d) => {
31929
- result.push({
31930
- node: n,
31931
- depth: d
31932
- });
31933
- if (n.children && this._isExpanded(n, d)) for (const child of n.children) visit(child, d + 1);
31934
- };
31935
- if (node.children) for (const child of node.children) visit(child, depth + 1);
31936
- return result;
31937
- }
31938
- #getAllNodes() {
31939
- if (!this.root) return [];
31940
- return this.#getVisibleNodes(this.root, -1);
31941
- }
31942
33336
  #getFocusedIndex() {
31943
- const nodes = this.#getAllNodes();
31944
33337
  const tickledKey = this.tickled.tickledId;
31945
33338
  if (!tickledKey) return 0;
31946
- return nodes.findIndex((n) => n.node.item.key === tickledKey);
33339
+ return this.#getFlat().indexByKey.get(tickledKey) ?? 0;
31947
33340
  }
31948
33341
  #setFocusedIndex(index) {
31949
- const nodes = this.#getAllNodes();
33342
+ const nodes = this.#getFlat().rows;
31950
33343
  if (index < 0 || index >= nodes.length) return;
31951
33344
  const { node, depth } = nodes[index];
31952
33345
  this.tickled.setTickled(node.item.key);
@@ -31960,6 +33353,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31960
33353
  composed: true
31961
33354
  }));
31962
33355
  this.requestUpdate();
33356
+ if (this.#virtualActive()) {
33357
+ this.#revealIndex(index);
33358
+ return;
33359
+ }
31963
33360
  this.updateComplete.then(() => this.#scrollTickledIntoView());
31964
33361
  }
31965
33362
  #scrollTickledIntoView() {
@@ -31970,12 +33367,25 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31970
33367
  behavior: `smooth`
31971
33368
  });
31972
33369
  }
33370
+ /** Rows per PageUp / PageDown: one viewport, from the current CSS metrics. */
33371
+ #pageSize() {
33372
+ this.#measureMetrics();
33373
+ return treePageSize(this.#layoutParams(this.#getFlat().rows.length));
33374
+ }
33375
+ /** Move the cursor to `nextIdx`, applying the interaction mode's selection side effects. */
33376
+ #navigateToIndex(nodes, currentIdx, nextIdx, shiftKey) {
33377
+ if (this.interactionMode === `implicit`) this.select(nodes[nextIdx].node);
33378
+ this.#setFocusedIndex(nextIdx);
33379
+ if (shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, nextIdx);
33380
+ else if (this.interactionMode === `vscode`) this.#anchor = void 0;
33381
+ this.requestUpdate();
33382
+ }
31973
33383
  #onKeyDown;
31974
33384
  renderLoadingIndicator() {
31975
33385
  return html`<span class="loading-spinner"></span>`;
31976
33386
  }
31977
33387
  render() {
31978
- const nodes = this.#getAllNodes();
33388
+ const nodes = this.#getFlat().rows;
31979
33389
  return html`
31980
33390
  <div
31981
33391
  id="container"
@@ -31998,12 +33408,51 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
31998
33408
  ${this.renderLoadingIndicator()}
31999
33409
  </div>
32000
33410
  `;
33411
+ if (this.#virtualActive()) return this.#renderWindow(nodes);
33412
+ const woven = weaveGhosts(nodes, this.#renderedRowKeys, this.#leaving);
33413
+ this.#renderedRowKeys = woven.map((e) => e.node.item.key);
33414
+ this.#renderedRowIndex = new Map(this.#renderedRowKeys.map((key, i) => [key, i]));
32001
33415
  return html`
32002
33416
  <ul class="tree-list" role="tree">
32003
- ${repeat(this.#weaveLeaving(nodes), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
33417
+ ${repeat(woven, ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
33418
+ </ul>
33419
+ `;
33420
+ }
33421
+ /**
33422
+ * Windowed render: the rows near the viewport between two spacers that hold
33423
+ * the rest of the scroll height. Keyed, so a row that stays in the window
33424
+ * keeps its element while others enter and leave at the edges.
33425
+ */
33426
+ #renderWindow(nodes) {
33427
+ const params = this.#layoutParams(nodes.length);
33428
+ const { start, end } = this.#currentWindow(nodes.length);
33429
+ this.#winStart = start;
33430
+ this.#winEnd = end;
33431
+ this.#renderedRowKeys = [];
33432
+ this.#renderedRowIndex = /* @__PURE__ */ new Map();
33433
+ const stride = treeStride(params);
33434
+ return html`
33435
+ <ul class="tree-list virtual" role="tree" aria-rowcount=${nodes.length}>
33436
+ <li class="virtual-spacer" role="presentation" aria-hidden="true" style=${`height: ${start * stride}px`}></li>
33437
+ ${repeat(nodes.slice(start, end), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
33438
+ <li class="virtual-spacer" role="presentation" aria-hidden="true" style=${`height: ${(nodes.length - end) * stride}px`}></li>
32004
33439
  </ul>
32005
33440
  `;
32006
33441
  }
33442
+ /**
33443
+ * `{ posinset, setsize }` for `aria-posinset` / `aria-setsize`. A windowed
33444
+ * list has most siblings out of the DOM, so assistive tech cannot count them.
33445
+ */
33446
+ _ariaSetInfo(node) {
33447
+ const flat = this.#getFlat();
33448
+ const idx = flat.indexByKey.get(node.item.key);
33449
+ if (idx === void 0) return void 0;
33450
+ const { posInSet, setSize } = flat.rows[idx];
33451
+ return {
33452
+ posInSet,
33453
+ setSize
33454
+ };
33455
+ }
32007
33456
  /** Shared drag-and-drop render state for a row, used by every `_renderNode` variant. */
32008
33457
  _dndRowState(node) {
32009
33458
  const draggable = this.itemDnd !== `none` && (this.root?.children?.length ?? 0) > 0 && (this.itemDnd === `internal` || this.itemDnd === `both` || this.itemDnd === `external`) && (this.canDrag?.({
@@ -32027,17 +33476,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
32027
33476
  const isBranch = node.item.isLeaf !== true;
32028
33477
  const isExpanded = this._isExpanded(node, depth);
32029
33478
  const isPending = this.#pendingNode === node;
32030
- const isFiltered = this._isFilteredOut(node, depth);
32031
33479
  const isLeaving = this._isLeaving(node);
32032
33480
  const dnd = this._dndRowState(node);
32033
33481
  return html`
32034
33482
  <li
32035
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
33483
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
32036
33484
  role="treeitem"
32037
33485
  aria-selected=${selected}
32038
33486
  aria-expanded=${isBranch ? isExpanded : nothing}
32039
33487
  aria-hidden=${isLeaving ? `true` : nothing}
32040
33488
  aria-level=${depth + 1}
33489
+ aria-setsize=${this._ariaSetInfo(node)?.setSize ?? nothing}
33490
+ aria-posinset=${this._ariaSetInfo(node)?.posInSet ?? nothing}
32041
33491
  data-key=${node.item.key}
32042
33492
  style=${`--depth: ${depth}`}
32043
33493
  draggable=${dnd.draggable ? `true` : nothing}
@@ -32078,6 +33528,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
32078
33528
  --tree-muted-text: var(--surface-muted-text);
32079
33529
  --tree-indent: 1ch;
32080
33530
  --tree-item-height: 30px;
33531
+ /* Margin above each row. A windowed list is positioned by (height + gap), so keep it in px. */
33532
+ --tree-item-gap: 1px;
32081
33533
  --tree-caret-size: 30px;
32082
33534
  --tree-radius: var(--radius-s);
32083
33535
  --tree-drop-line: var(--accent);
@@ -32101,6 +33553,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
32101
33553
  The scroll lives here, not on :host, so the search popover (a sibling of
32102
33554
  #container, positioned against :host) stays put while the list scrolls. */
32103
33555
  #container {
33556
+ /* offsetParent for rows: row offsets are scroll-independent (sticky / DnD geometry). */
33557
+ position: relative;
32104
33558
  outline: none;
32105
33559
  min-height: 100%;
32106
33560
  max-height: 100%;
@@ -32147,7 +33601,40 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
32147
33601
  font-size: var(--text-m);
32148
33602
  border-radius: var(--tree-radius);
32149
33603
  transition: background var(--transition, 0.15s ease-out);
32150
- margin: 1px 0;
33604
+ margin: var(--tree-item-gap) 0;
33605
+ }
33606
+
33607
+ /* Windowed list: rows are placed purely by stride arithmetic — a margin above
33608
+ each row only (no collapsing), spacers hold the offscreen height. */
33609
+ .tree-list.virtual .item {
33610
+ margin: var(--tree-item-gap) 0 0;
33611
+ }
33612
+
33613
+ .virtual-spacer {
33614
+ display: block;
33615
+ margin: 0;
33616
+ padding: 0;
33617
+ list-style: none;
33618
+ pointer-events: none;
33619
+ }
33620
+
33621
+ .sticky-anchor {
33622
+ display: contents;
33623
+ }
33624
+
33625
+ :host([data-virtual]) .sticky-anchor {
33626
+ position: sticky;
33627
+ top: 0;
33628
+ z-index: 5;
33629
+ display: block;
33630
+ height: 0;
33631
+ }
33632
+
33633
+ :host([data-virtual]) .sticky-headers {
33634
+ position: absolute;
33635
+ top: 0;
33636
+ left: 0;
33637
+ right: 0;
32151
33638
  }
32152
33639
 
32153
33640
  .tree-caret {
@@ -32202,10 +33689,6 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
32202
33689
  to { transform: rotate(360deg); }
32203
33690
  }
32204
33691
 
32205
- .item.filtered {
32206
- display: none;
32207
- }
32208
-
32209
33692
  /* Fading-out ghost row (see #runItemAnimations) — inert while it collapses. */
32210
33693
  .item.leaving {
32211
33694
  pointer-events: none;
@@ -32316,6 +33799,14 @@ __decorate([property({
32316
33799
  toAttribute: (value) => value ? null : `false`
32317
33800
  }
32318
33801
  })], TreeBaseElement.prototype, "stickyHeaders", void 0);
33802
+ __decorate([property({
33803
+ reflect: true,
33804
+ attribute: `virtualize`
33805
+ })], TreeBaseElement.prototype, "virtualize", void 0);
33806
+ __decorate([property({
33807
+ type: Number,
33808
+ attribute: `virtualize-threshold`
33809
+ })], TreeBaseElement.prototype, "virtualizeThreshold", void 0);
32319
33810
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewRenderer", void 0);
32320
33811
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewSource", void 0);
32321
33812
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrag", void 0);
@@ -32323,33 +33814,33 @@ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrop
32323
33814
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "parseExternalDrop", void 0);
32324
33815
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "applyInternalDrop", void 0);
32325
33816
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
33817
+ __decorate([state()], TreeBaseElement.prototype, "_scrollTop", void 0);
32326
33818
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
32327
33819
  __decorate([state()], TreeBaseElement.prototype, "_stickyKeys", void 0);
32328
33820
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
32329
33821
  //#endregion
32330
33822
  //#region src/tree/tree-list.ts
32331
- function computeCheckboxState$1(node, selectedNodes) {
32332
- if (node.item.isLeaf || !node.children?.length) return selectedNodes.has(node) ? `checked` : `unchecked`;
32333
- const childStates = node.children.map((c) => computeCheckboxState$1(c, selectedNodes));
32334
- const allChecked = childStates.every((s) => s === `checked`);
32335
- const allUnchecked = childStates.every((s) => s === `unchecked`);
32336
- if (allChecked) return `checked`;
32337
- if (allUnchecked) return `unchecked`;
32338
- return `indeterminate`;
32339
- }
32340
33823
  let TreeListElement = class TreeListElement extends TreeBaseElement {
32341
- #depthsWithIcons = /* @__PURE__ */ new Set();
32342
- render() {
32343
- this.#detectIcons();
32344
- return super.render();
33824
+ /** Checkbox tri-states for the whole loaded tree, rebuilt only when the structure or selection changes. */
33825
+ #checkboxCache;
33826
+ willUpdate(changedProps) {
33827
+ super.willUpdate(changedProps);
33828
+ this.#syncCheckboxStates();
32345
33829
  }
32346
- #detectIcons() {
32347
- this.#depthsWithIcons.clear();
32348
- const visit = (node, depth) => {
32349
- if (node.item.icon) this.#depthsWithIcons.add(depth);
32350
- if (node.children) for (const child of node.children) visit(child, depth + 1);
33830
+ #syncCheckboxStates() {
33831
+ const root = this.root;
33832
+ if (this.interactionMode !== `checked` || !root) {
33833
+ this.#checkboxCache = void 0;
33834
+ return;
33835
+ }
33836
+ const c = this.#checkboxCache;
33837
+ if (c && c.root === root && c.structureVersion === this._structureVersion && c.selectionVersion === this._selectionVersion) return;
33838
+ this.#checkboxCache = {
33839
+ root,
33840
+ structureVersion: this._structureVersion,
33841
+ selectionVersion: this._selectionVersion,
33842
+ states: computeCheckboxStates(root, this._selectedNodes)
32351
33843
  };
32352
- if (this.root) visit(this.root, -1);
32353
33844
  }
32354
33845
  renderNodeContent(node, depth) {
32355
33846
  const isBranch = node.item.isLeaf !== true;
@@ -32367,7 +33858,7 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
32367
33858
  </span>
32368
33859
  `;
32369
33860
  if (this.interactionMode === `checked`) {
32370
- const state = computeCheckboxState$1(node, this._selectedNodes);
33861
+ const state = this.#checkboxCache?.states.get(node.item.key) ?? `unchecked`;
32371
33862
  return html`${this.#renderCheckbox(node, depth, state)}${caretAndLabel}`;
32372
33863
  }
32373
33864
  return caretAndLabel;
@@ -32401,19 +33892,20 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
32401
33892
  const isBranch = node.item.isLeaf !== true;
32402
33893
  const isExpanded = this._isExpanded(node, depth);
32403
33894
  const isPending = this._pendingNode === node;
32404
- const hasIconAtDepth = this.#depthsWithIcons.has(depth);
33895
+ const hasIconAtDepth = this._depthsWithIcons.has(depth);
32405
33896
  const isLeaving = this._isLeaving(node);
32406
- const isFiltered = this._isFilteredOut(node, depth);
32407
33897
  const dnd = this._dndRowState(node);
32408
33898
  return html`
32409
33899
  <li
32410
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${isFiltered ? `filtered` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
33900
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
32411
33901
  data-key=${node.item.key}
32412
33902
  role="treeitem"
32413
33903
  aria-selected=${selected}
32414
33904
  aria-expanded=${isBranch ? isExpanded : nothing}
32415
33905
  aria-hidden=${isLeaving ? `true` : nothing}
32416
33906
  aria-level=${depth + 1}
33907
+ aria-setsize=${this._ariaSetInfo(node)?.setSize ?? nothing}
33908
+ aria-posinset=${this._ariaSetInfo(node)?.posInSet ?? nothing}
32417
33909
  style=${`--depth: ${depth}`}
32418
33910
  draggable=${dnd.draggable ? `true` : nothing}
32419
33911
  part=${dnd.dropTarget ? `drop-target` : nothing}
@@ -40919,86 +42411,30 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
40919
42411
  return;
40920
42412
  }
40921
42413
  switch (event.key) {
40922
- case `ArrowDown`: {
42414
+ case `ArrowDown`:
40923
42415
  event.preventDefault();
40924
- const current = this.#getTickledNodeAndDepth();
40925
- let targetIndex = 0;
40926
- if (current && current.depth === currentDepth) targetIndex = Math.min(current.index + 1, col.length - 1);
40927
- const targetNode = col[targetIndex];
40928
- if (!targetNode) break;
40929
- this.#setTickledNode(targetNode, currentDepth);
40930
- if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
40931
- const prevIndex = current?.index ?? 0;
40932
- if (event.shiftKey) {
40933
- if (!this.#anchor || this.#anchor.depth !== currentDepth) {
40934
- const anchorNode = col[prevIndex];
40935
- if (anchorNode) this.#anchor = {
40936
- key: anchorNode.item.key,
40937
- depth: currentDepth,
40938
- index: prevIndex,
40939
- selectionSnapshot: this.interactionMode === `vscode` ? new Set(this._selectedKeys) : void 0
40940
- };
40941
- }
40942
- if (this.#anchor) {
40943
- const from = Math.min(this.#anchor.index, targetIndex);
40944
- const to = Math.max(this.#anchor.index, targetIndex);
40945
- const rangeKeys = col.slice(from, to + 1).map((n) => n.item.key);
40946
- if (this.interactionMode === `vscode`) {
40947
- const snap = this.#anchor.selectionSnapshot;
40948
- for (const k of rangeKeys) if (snap.has(k)) this._selectedKeys.delete(k);
40949
- else this._selectedKeys.add(k);
40950
- } else this._selectedKeys = new Set(rangeKeys);
40951
- }
40952
- } else if (this.interactionMode === `standard`) this.#anchor = {
40953
- key: targetNode.item.key,
40954
- depth: currentDepth,
40955
- index: targetIndex
40956
- };
40957
- else this.#anchor = void 0;
40958
- }
40959
- this.requestUpdate();
42416
+ this.#moveCursor(col, currentDepth, event, (current) => Math.min(current + 1, col.length - 1));
40960
42417
  break;
40961
- }
40962
- case `ArrowUp`: {
42418
+ case `ArrowUp`:
40963
42419
  event.preventDefault();
40964
- const current = this.#getTickledNodeAndDepth();
40965
- let targetIndex = 0;
40966
- if (current && current.depth === currentDepth) targetIndex = Math.max(current.index - 1, 0);
40967
- const targetNode = col[targetIndex];
40968
- if (!targetNode) break;
40969
- this.#setTickledNode(targetNode, currentDepth);
40970
- if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
40971
- const prevIndex = current?.index ?? 0;
40972
- if (event.shiftKey) {
40973
- if (!this.#anchor || this.#anchor.depth !== currentDepth) {
40974
- const anchorNode = col[prevIndex];
40975
- if (anchorNode) this.#anchor = {
40976
- key: anchorNode.item.key,
40977
- depth: currentDepth,
40978
- index: prevIndex,
40979
- selectionSnapshot: this.interactionMode === `vscode` ? new Set(this._selectedKeys) : void 0
40980
- };
40981
- }
40982
- if (this.#anchor) {
40983
- const from = Math.min(this.#anchor.index, targetIndex);
40984
- const to = Math.max(this.#anchor.index, targetIndex);
40985
- const rangeKeys = col.slice(from, to + 1).map((n) => n.item.key);
40986
- if (this.interactionMode === `vscode`) {
40987
- const snap = this.#anchor.selectionSnapshot;
40988
- for (const k of rangeKeys) if (snap.has(k)) this._selectedKeys.delete(k);
40989
- else this._selectedKeys.add(k);
40990
- } else this._selectedKeys = new Set(rangeKeys);
40991
- }
40992
- } else if (this.interactionMode === `standard`) this.#anchor = {
40993
- key: targetNode.item.key,
40994
- depth: currentDepth,
40995
- index: targetIndex
40996
- };
40997
- else this.#anchor = void 0;
40998
- }
40999
- this.requestUpdate();
42420
+ this.#moveCursor(col, currentDepth, event, (current) => Math.max(current - 1, 0));
42421
+ break;
42422
+ case `Home`:
42423
+ event.preventDefault();
42424
+ this.#moveCursor(col, currentDepth, event, () => 0, true);
42425
+ break;
42426
+ case `End`:
42427
+ event.preventDefault();
42428
+ this.#moveCursor(col, currentDepth, event, () => col.length - 1, true);
42429
+ break;
42430
+ case `PageUp`:
42431
+ event.preventDefault();
42432
+ this.#moveCursor(col, currentDepth, event, (current) => Math.max(current - this.#pageSize(currentDepth), 0), true);
42433
+ break;
42434
+ case `PageDown`:
42435
+ event.preventDefault();
42436
+ this.#moveCursor(col, currentDepth, event, (current) => Math.min(current + this.#pageSize(currentDepth), col.length - 1), true);
41000
42437
  break;
41001
- }
41002
42438
  case `ArrowRight`: {
41003
42439
  event.preventDefault();
41004
42440
  const tickled = this.#getTickledNodeAndDepth();
@@ -41091,14 +42527,28 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41091
42527
  window.addEventListener(`pointermove`, this.#onResizeMove);
41092
42528
  window.addEventListener(`pointerup`, this.#onResizeEnd);
41093
42529
  };
42530
+ this.#resizeRaf = null;
42531
+ this.#resizePendingWidth = 0;
41094
42532
  this.#onResizeMove = (event) => {
41095
42533
  if (this.#resizingDepth < 0) return;
41096
- const delta = event.clientX - this.#resizeStartX;
41097
- const newWidth = Math.max(32, this.#resizeStartWidth + delta);
41098
- this.#columnWidths.set(this.#resizingDepth, newWidth);
41099
- this.requestUpdate();
42534
+ this.#resizePendingWidth = Math.max(32, this.#resizeStartWidth + (event.clientX - this.#resizeStartX));
42535
+ this.#resizeRaf ??= requestAnimationFrame(this.#applyResizeWidth);
42536
+ };
42537
+ this.#applyResizeWidth = () => {
42538
+ this.#resizeRaf = null;
42539
+ if (this.#resizingDepth < 0) return;
42540
+ (this.shadowRoot?.querySelectorAll(`.column`)[this.#resizingDepth])?.setAttribute(`style`, this.#explicitWidthStyle(this.#resizePendingWidth));
41100
42541
  };
41101
42542
  this.#onResizeEnd = () => {
42543
+ if (this.#resizeRaf !== null) {
42544
+ cancelAnimationFrame(this.#resizeRaf);
42545
+ this.#resizeRaf = null;
42546
+ }
42547
+ if (this.#resizingDepth >= 0 && this.#resizePendingWidth > 0) {
42548
+ this.#columnWidths.set(this.#resizingDepth, this.#resizePendingWidth);
42549
+ this.requestUpdate();
42550
+ }
42551
+ this.#resizePendingWidth = 0;
41102
42552
  this.#resizingDepth = -1;
41103
42553
  window.removeEventListener(`pointermove`, this.#onResizeMove);
41104
42554
  window.removeEventListener(`pointerup`, this.#onResizeEnd);
@@ -41705,7 +43155,7 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41705
43155
  this.#handleStandardClick(node, depth, event);
41706
43156
  break;
41707
43157
  case `manual`: break;
41708
- case `checked`: this.#expandNode(node, depth);
43158
+ case `checked`: this.#expandNode(node, depth, true);
41709
43159
  }
41710
43160
  }
41711
43161
  #handleImplicitClick(node, depth) {
@@ -41715,7 +43165,7 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41715
43165
  this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
41716
43166
  this._selectedNodes.clear();
41717
43167
  this._selectedNodes.add(node);
41718
- this.#expandNode(node, depth);
43168
+ this.#expandNode(node, depth, true);
41719
43169
  if (!previous.has(node)) this.#dispatchSelectEvent(previous);
41720
43170
  }
41721
43171
  #handleStandardClick(node, depth, event) {
@@ -41758,16 +43208,22 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41758
43208
  } : void 0;
41759
43209
  this._selectedByDepth.set(depth, node);
41760
43210
  this.#lastSelectedNode = node;
41761
- this.#expandNode(node, depth);
43211
+ this.#expandNode(node, depth, true);
41762
43212
  if (!previous.has(node) || previous.size !== 1) this.#dispatchSelectEvent(previous);
41763
43213
  }
41764
- #expandNode(node, depth) {
43214
+ /**
43215
+ * Open the column for `node`. `immediate` skips the lazy-load debounce: use
43216
+ * it for deliberate clicks, where the user has committed to this node. The
43217
+ * debounce is kept for keyboard navigation, which can sweep over many
43218
+ * branches in quick succession.
43219
+ */
43220
+ #expandNode(node, depth, immediate = false) {
41765
43221
  if (node.item.isLeaf) {
41766
43222
  this._columns = this._columns.slice(0, depth + 1);
41767
43223
  this.requestUpdate();
41768
43224
  return;
41769
43225
  }
41770
- if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true);
43226
+ if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true, immediate);
41771
43227
  else {
41772
43228
  const newColumns = this._columns.slice(0, depth + 1);
41773
43229
  newColumns.push(node.children);
@@ -41792,10 +43248,11 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41792
43248
  * own depth: its column for expansions, -1 for a view root (it sits above
41793
43249
  * column 0, so loaders computing child depth as `depth + 1` stay correct).
41794
43250
  */
41795
- #loadUnloadedChildren(node, parentDepth, fireExpandEvent) {
43251
+ #loadUnloadedChildren(node, parentDepth, fireExpandEvent, immediate = false) {
41796
43252
  const nodeDepth = parentDepth + 1;
41797
43253
  this._columns = this._columns.slice(0, nodeDepth);
41798
43254
  this._loadingDepth = nodeDepth;
43255
+ this.#pendingController?.abort();
41799
43256
  this.#pendingNode = node;
41800
43257
  const controller = new AbortController();
41801
43258
  this.#pendingController = controller;
@@ -41826,15 +43283,20 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41826
43283
  this.requestUpdate();
41827
43284
  };
41828
43285
  if (this.model || this.loadChildren) {
41829
- if (this.#expandTimer !== void 0) clearTimeout(this.#expandTimer);
41830
- this.#expandTimer = setTimeout(() => {
43286
+ if (this.#expandTimer !== void 0) {
43287
+ clearTimeout(this.#expandTimer);
43288
+ this.#expandTimer = void 0;
43289
+ }
43290
+ const load = () => {
41831
43291
  this.#expandTimer = void 0;
41832
43292
  if (this.#pendingNode !== node) return;
41833
43293
  (this.model ? this.model.getChildren(node, nodeDepth - 1, controller.signal) : collectDataResult(this.loadChildren({
41834
43294
  node,
41835
43295
  depth: nodeDepth - 1
41836
43296
  }, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
41837
- }, EXPAND_DEBOUNCE_MS);
43297
+ };
43298
+ if (immediate) load();
43299
+ else this.#expandTimer = setTimeout(load, EXPAND_DEBOUNCE_MS);
41838
43300
  } else {
41839
43301
  this._loadingDepth = void 0;
41840
43302
  this.#pendingNode = void 0;
@@ -41845,10 +43307,12 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41845
43307
  }
41846
43308
  }
41847
43309
  #onCheckboxClick(node) {
43310
+ const previous = new Set(this._selectedNodes);
41848
43311
  const currentlySelected = this._selectedKeys.has(node.item.key);
41849
43312
  if (!!node.children?.length ? this.#allDescendantsSelected(node) : currentlySelected) this.#applyCheckedToggle(node, false);
41850
43313
  else this.#applyCheckedToggle(node, true);
41851
43314
  this.requestUpdate();
43315
+ this.#dispatchSelectEvent(previous);
41852
43316
  }
41853
43317
  #allDescendantsSelected(node) {
41854
43318
  if (!this._selectedKeys.has(node.item.key)) return false;
@@ -41885,7 +43349,7 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41885
43349
  index
41886
43350
  };
41887
43351
  }
41888
- #scrollNavigatedItemIntoView() {
43352
+ #scrollNavigatedItemIntoView(behavior = `smooth`) {
41889
43353
  requestAnimationFrame(() => {
41890
43354
  const tickled = this.#getTickledNodeAndDepth();
41891
43355
  if (!tickled) return;
@@ -41893,7 +43357,7 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41893
43357
  if (!container) return;
41894
43358
  container.querySelectorAll(`.item`)[tickled.index]?.scrollIntoView({
41895
43359
  block: `nearest`,
41896
- behavior: `smooth`
43360
+ behavior
41897
43361
  });
41898
43362
  });
41899
43363
  }
@@ -41908,9 +43372,67 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41908
43372
  composed: true
41909
43373
  }));
41910
43374
  }
43375
+ /**
43376
+ * Number of items a PageUp/PageDown moves: how many rows fit in the column's
43377
+ * visible area, less one so the previous page's last row stays in view.
43378
+ */
43379
+ #pageSize(depth) {
43380
+ const content = this.shadowRoot?.querySelectorAll(`.column`)[depth]?.querySelector(`.column-content`);
43381
+ const item = content?.querySelector(`.item`);
43382
+ if (!content || !item || item.offsetHeight === 0) return 10;
43383
+ return Math.max(1, Math.floor(content.clientHeight / item.offsetHeight) - 1);
43384
+ }
43385
+ /**
43386
+ * Move the cursor within column `col` to the index returned by `target`
43387
+ * (given the current cursor index, or -1 when the cursor isn't in this
43388
+ * column, which behaves as "before the first item"). Handles Shift range
43389
+ * extension and anchor seating for `standard` / `vscode` modes.
43390
+ * `scroll` brings the new item into view (for jumps larger than one row).
43391
+ */
43392
+ #moveCursor(col, currentDepth, event, target, scroll = false) {
43393
+ const current = this.#getTickledNodeAndDepth();
43394
+ const targetIndex = target(current !== void 0 && current.depth === currentDepth ? current.index : -1);
43395
+ const targetNode = col[targetIndex];
43396
+ if (!targetNode) return;
43397
+ this.#setTickledNode(targetNode, currentDepth);
43398
+ if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
43399
+ const prevIndex = current?.index ?? 0;
43400
+ if (event.shiftKey) {
43401
+ if (!this.#anchor || this.#anchor.depth !== currentDepth) {
43402
+ const anchorNode = col[prevIndex];
43403
+ if (anchorNode) this.#anchor = {
43404
+ key: anchorNode.item.key,
43405
+ depth: currentDepth,
43406
+ index: prevIndex,
43407
+ selectionSnapshot: this.interactionMode === `vscode` ? new Set(this._selectedKeys) : void 0
43408
+ };
43409
+ }
43410
+ if (this.#anchor) {
43411
+ const from = Math.min(this.#anchor.index, targetIndex);
43412
+ const to = Math.max(this.#anchor.index, targetIndex);
43413
+ const rangeKeys = col.slice(from, to + 1).map((n) => n.item.key);
43414
+ if (this.interactionMode === `vscode`) {
43415
+ const snap = this.#anchor.selectionSnapshot;
43416
+ for (const k of rangeKeys) if (snap.has(k)) this._selectedKeys.delete(k);
43417
+ else this._selectedKeys.add(k);
43418
+ } else this._selectedKeys = new Set(rangeKeys);
43419
+ }
43420
+ } else if (this.interactionMode === `standard`) this.#anchor = {
43421
+ key: targetNode.item.key,
43422
+ depth: currentDepth,
43423
+ index: targetIndex
43424
+ };
43425
+ else this.#anchor = void 0;
43426
+ }
43427
+ this.requestUpdate();
43428
+ if (scroll) this.#scrollNavigatedItemIntoView(`auto`);
43429
+ }
41911
43430
  #onKeyDown;
41912
43431
  #onResizeStart;
43432
+ #resizeRaf;
43433
+ #resizePendingWidth;
41913
43434
  #onResizeMove;
43435
+ #applyResizeWidth;
41914
43436
  #onResizeEnd;
41915
43437
  #getColumnHeader(depth) {
41916
43438
  if (depth === 0) return this.#displayRoot?.item.label ?? ``;
@@ -41924,9 +43446,12 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
41924
43446
  }
41925
43447
  return ``;
41926
43448
  }
43449
+ #explicitWidthStyle(width) {
43450
+ return `width:${width}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
43451
+ }
41927
43452
  #columnStyle(depth, isLast) {
41928
43453
  const w = this.#columnWidths.get(depth);
41929
- if (w !== void 0) return `width:${w}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
43454
+ if (w !== void 0) return this.#explicitWidthStyle(w);
41930
43455
  if (this.stretchLastColumn && isLast) return `width:max-content;flex-grow:1;flex-shrink:0;flex-basis:auto;min-width:0`;
41931
43456
  return `width:max-content;flex-grow:0;flex-shrink:0;flex-basis:auto`;
41932
43457
  }
@@ -42176,8 +43701,24 @@ __decorate([state()], MillerBaseElement.prototype, "_selectedNodes", void 0);
42176
43701
  MillerBaseElement = __decorate([safeCustomElement(`ixfx-miller-base`)], MillerBaseElement);
42177
43702
  //#endregion
42178
43703
  //#region src/miller/miller-list.ts
43704
+ const HAS_CHECKED = 1;
43705
+ const HAS_UNCHECKED = 2;
43706
+ /**
43707
+ * Walk the loaded subtree under `node`, returning which kinds of leaf were
43708
+ * seen (`HAS_CHECKED` | `HAS_UNCHECKED`). Stops as soon as both have been
43709
+ * seen, since the result can no longer change.
43710
+ */
43711
+ function scanCheckState(node, selectedKeys) {
43712
+ if (node.item.isLeaf || !node.children?.length) return selectedKeys.has(node.item.key) ? HAS_CHECKED : HAS_UNCHECKED;
43713
+ let seen = 0;
43714
+ for (const child of node.children) {
43715
+ seen |= scanCheckState(child, selectedKeys);
43716
+ if (seen === 3) break;
43717
+ }
43718
+ return seen;
43719
+ }
42179
43720
  /**
42180
- * Recursively compute the checkbox state for a node based on `selectedKeys`.
43721
+ * Compute the checkbox state for a node based on `selectedKeys`.
42181
43722
  *
42182
43723
  * - Unloaded branch (`children === undefined`): treated as a leaf — we can only
42183
43724
  * report on what is known.
@@ -42186,12 +43727,9 @@ MillerBaseElement = __decorate([safeCustomElement(`ixfx-miller-base`)], MillerBa
42186
43727
  * indeterminate otherwise.
42187
43728
  */
42188
43729
  function computeCheckboxState(node, selectedKeys) {
42189
- if (node.item.isLeaf || !node.children?.length) return selectedKeys.has(node.item.key) ? `checked` : `unchecked`;
42190
- const childStates = node.children.map((c) => computeCheckboxState(c, selectedKeys));
42191
- const allChecked = childStates.every((s) => s === `checked`);
42192
- const allUnchecked = childStates.every((s) => s === `unchecked`);
42193
- if (allChecked) return `checked`;
42194
- if (allUnchecked) return `unchecked`;
43730
+ const seen = scanCheckState(node, selectedKeys);
43731
+ if (seen === HAS_CHECKED) return `checked`;
43732
+ if (seen === HAS_UNCHECKED) return `unchecked`;
42195
43733
  return `indeterminate`;
42196
43734
  }
42197
43735
  let MillerListElement = class MillerListElement extends MillerBaseElement {
@@ -50406,9 +51944,12 @@ var TabController = class {
50406
51944
  }
50407
51945
  /** The tab's visible label text, excluding any `slot="toolbar"` content. */
50408
51946
  #labelOf(item) {
50409
- const clone = item.cloneNode(true);
50410
- clone.querySelectorAll(`[slot="toolbar"]`).forEach((el) => el.remove());
50411
- return (clone.textContent ?? ``).trim();
51947
+ let text = ``;
51948
+ for (const node of item.childNodes) {
51949
+ if (node instanceof Element && node.getAttribute(`slot`) === `toolbar`) continue;
51950
+ text += node.textContent ?? ``;
51951
+ }
51952
+ return text.trim();
50412
51953
  }
50413
51954
  #registerCommand(tab) {
50414
51955
  if (!this.#commands) return;
@@ -50525,9 +52066,12 @@ let TabPanelsElement = class TabPanelsElement extends LitElement {
50525
52066
  }
50526
52067
  /** The tab's visible label text, excluding any `slot="toolbar"` content. */
50527
52068
  #labelOf(item) {
50528
- const clone = item.cloneNode(true);
50529
- clone.querySelectorAll(`[slot="toolbar"]`).forEach((el) => el.remove());
50530
- return (clone.textContent ?? ``).trim();
52069
+ let text = ``;
52070
+ for (const node of item.childNodes) {
52071
+ if (node instanceof Element && node.getAttribute(`slot`) === `toolbar`) continue;
52072
+ text += node.textContent ?? ``;
52073
+ }
52074
+ return text.trim();
50531
52075
  }
50532
52076
  static {
50533
52077
  this.styles = css`
@@ -53396,7 +54940,7 @@ var TreeController = class {
53396
54940
  const treeNode = this.#nodeToTree.get(node);
53397
54941
  if (!treeNode) return;
53398
54942
  node.children = this.#childrenOf(treeNode);
53399
- this.#element.requestUpdate();
54943
+ this.#element.invalidateStructure();
53400
54944
  }, { signal });
53401
54945
  this.#element.addEventListener(`collapse`, (_e) => {}, { signal });
53402
54946
  }
@@ -53418,7 +54962,10 @@ var TreeController = class {
53418
54962
  if (!node) return false;
53419
54963
  if (node.children === void 0) {
53420
54964
  const mapped = this.#nodeToTree.get(node);
53421
- if (mapped) node.children = this.#childrenOf(mapped);
54965
+ if (mapped) {
54966
+ node.children = this.#childrenOf(mapped);
54967
+ this.#element.invalidateStructure();
54968
+ }
53422
54969
  }
53423
54970
  currentColumn = node.children ?? [];
53424
54971
  }