@ixfx/components 0.13.1 → 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 (78) hide show
  1. package/bundle/index.d.ts +100 -14
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2211 -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-DEDG6epz.d.ts → flat-list-Y98IqH00.d.ts} +5 -2
  18. package/dist/flat-list-Y98IqH00.d.ts.map +1 -0
  19. package/dist/flat-list.d.ts +1 -1
  20. package/dist/flat-list.js +33 -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/flat-list.md +14 -2
  66. package/docs-user/grid-list.md +81 -4
  67. package/docs-user/index.json +2 -2
  68. package/docs-user/miller.md +59 -3
  69. package/docs-user/tree.md +63 -2
  70. package/docs-user/vertical-list.md +9 -1
  71. package/package.json +6 -1
  72. package/dist/detail-list.js.map +0 -1
  73. package/dist/flat-list-DEDG6epz.d.ts.map +0 -1
  74. package/dist/grid-list-DjIl910A.js.map +0 -1
  75. package/dist/incr-search-D91M4Ozq.js.map +0 -1
  76. package/dist/tab-list-v8mKX7Fu.d.ts.map +0 -1
  77. package/dist/tree-DGveys4K.js.map +0 -1
  78. package/dist/tree.d.ts.map +0 -1
@@ -2,7 +2,7 @@ import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
2
2
  import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrAHEj1q.js";
3
3
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
- import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-D91M4Ozq.js";
5
+ import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-JIhwB49y.js";
6
6
  import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
7
7
  import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
8
8
  import "./interaction-ZTh4htzr.js";
@@ -643,15 +643,10 @@ var TreeDropController = class {
643
643
  if (clientY > rows[rows.length - 1].rect.bottom) return rows[rows.length - 1];
644
644
  return rows.find((r) => clientY >= r.rect.top && clientY <= r.rect.bottom) ?? rows[rows.length - 1];
645
645
  }
646
- #parentRow(rows, index) {
647
- const target = rows[index];
648
- for (let i = index - 1; i >= 0; i--) if (rows[i].depth === target.depth - 1) return rows[i];
649
- }
650
646
  #resolveIntent(event, rows, source) {
651
647
  if (rows.length === 0) return void 0;
652
648
  const row = this.#rowAtPointer(rows, event.clientY);
653
649
  if (!row) return void 0;
654
- const rowIndex = rows.indexOf(row);
655
650
  const f = clamp((event.clientY - row.rect.top) / row.rect.height, 0, 1);
656
651
  let position = f < .25 ? `before` : f > .75 ? `after` : `inside`;
657
652
  const isLeaf = row.node.item.isLeaf === true;
@@ -674,18 +669,21 @@ var TreeDropController = class {
674
669
  const indent = this.#config.getIndentPx() || 1;
675
670
  const treeLeft = rows[0].rect.left;
676
671
  const effDepth = clamp(Math.round((event.clientX - treeLeft) / indent), 0, row.depth);
677
- let anchorIndex = rowIndex;
678
- for (let up = row.depth - effDepth; up > 0; up--) {
679
- const parent = this.#parentRow(rows, anchorIndex);
680
- if (!parent) break;
681
- anchorIndex = rows.indexOf(parent);
672
+ let anchor = {
673
+ node: row.node,
674
+ depth: row.depth,
675
+ parentKey: this.#config.getRowInfo(row.key)?.parentKey
676
+ };
677
+ referenceKey = row.key;
678
+ for (let up = row.depth - effDepth; up > 0 && anchor.parentKey !== void 0; up--) {
679
+ const parentInfo = this.#config.getRowInfo(anchor.parentKey);
680
+ if (!parentInfo) break;
681
+ referenceKey = anchor.parentKey;
682
+ anchor = parentInfo;
682
683
  }
683
- const anchor = rows[anchorIndex];
684
- referenceKey = anchor.key;
685
684
  depth = anchor.depth;
686
- const parent = this.#parentRow(rows, anchorIndex);
687
- parentKey = parent?.key;
688
- const refIdx = (parent ? parent.node.children ?? [] : rows.filter((r) => r.depth === 0).map((r) => r.node)).findIndex((n) => n.item.key === anchor.key);
685
+ parentKey = anchor.parentKey;
686
+ const refIdx = this.#config.getChildren(parentKey).findIndex((n) => n.item.key === referenceKey);
689
687
  index = (refIdx < 0 ? 0 : refIdx) + (position === `after` ? 1 : 0);
690
688
  }
691
689
  if (source === `internal`) {
@@ -760,9 +758,333 @@ var TreeDropController = class {
760
758
  }
761
759
  };
762
760
  //#endregion
761
+ //#region src/tree/tree-flat.ts
762
+ /**
763
+ * Flatten the visible rows under `root` (root itself excluded).
764
+ *
765
+ * Iterative implementation using an explicit stack to avoid recursion depth
766
+ * limits with deep trees. `posInSet` / `setSize` count only shown siblings.
767
+ *
768
+ * @param root - The root node (never included in output; its children are depth 0)
769
+ * @param options - Callbacks determining expansion and visibility
770
+ * @returns FlatTree with rows in display order and a key→index map
771
+ */
772
+ function flattenTree(root, options) {
773
+ const rows = [];
774
+ const indexByKey = /* @__PURE__ */ new Map();
775
+ const stack = [];
776
+ const pushChildren = (children, depth, parentKey) => {
777
+ if (!children || children.length === 0) return;
778
+ const shown = children.filter((c) => options.isShown(c, depth));
779
+ for (let i = shown.length - 1; i >= 0; i--) stack.push({
780
+ node: shown[i],
781
+ depth,
782
+ parentKey,
783
+ posInSet: i + 1,
784
+ setSize: shown.length
785
+ });
786
+ };
787
+ pushChildren(root.children, 0, void 0);
788
+ while (stack.length > 0) {
789
+ const { node, depth, parentKey, posInSet, setSize } = stack.pop();
790
+ indexByKey.set(node.item.key, rows.length);
791
+ rows.push({
792
+ node,
793
+ depth,
794
+ parentKey,
795
+ posInSet,
796
+ setSize
797
+ });
798
+ if (node.children && node.children.length > 0 && options.isExpanded(node, depth)) pushChildren(node.children, depth + 1, node.item.key);
799
+ }
800
+ return {
801
+ rows,
802
+ indexByKey
803
+ };
804
+ }
805
+ /**
806
+ * Build a whole-tree index over the currently loaded nodes.
807
+ * This walks the entire loaded tree once and is cached on structure version.
808
+ *
809
+ * @param root - The root node
810
+ * @returns TreeIndex with parent map and icon depth set
811
+ */
812
+ function indexTree(root) {
813
+ const parentByKey = /* @__PURE__ */ new Map();
814
+ const depthsWithIcons = /* @__PURE__ */ new Set();
815
+ const stack = [];
816
+ if (root.children) for (const child of root.children) {
817
+ parentByKey.set(child.item.key, root.item.key);
818
+ stack.push({
819
+ node: child,
820
+ depth: 0,
821
+ parentKey: root.item.key
822
+ });
823
+ }
824
+ while (stack.length > 0) {
825
+ const { node, depth, parentKey } = stack.pop();
826
+ parentByKey.set(node.item.key, parentKey);
827
+ if (node.item.icon) depthsWithIcons.add(depth);
828
+ if (node.children) for (let i = node.children.length - 1; i >= 0; i--) {
829
+ const child = node.children[i];
830
+ stack.push({
831
+ node: child,
832
+ depth: depth + 1,
833
+ parentKey: node.item.key
834
+ });
835
+ }
836
+ }
837
+ return {
838
+ parentByKey,
839
+ depthsWithIcons
840
+ };
841
+ }
842
+ /**
843
+ * Compute which nodes match a filter predicate.
844
+ * Returns the set of keys for nodes that match OR have a matching descendant.
845
+ *
846
+ * One iterative post-order pass; every node is visited (no short-circuiting).
847
+ */
848
+ function computeFilterMatches(root, predicate) {
849
+ const matches = /* @__PURE__ */ new Set();
850
+ if (!root.children) return matches;
851
+ const stack = [];
852
+ const enter = (node, depth) => {
853
+ stack.push({
854
+ node,
855
+ depth,
856
+ next: 0,
857
+ matched: predicate(node, depth)
858
+ });
859
+ };
860
+ for (const child of root.children) {
861
+ enter(child, 0);
862
+ while (stack.length > 0) {
863
+ const frame = stack[stack.length - 1];
864
+ const children = frame.node.children;
865
+ if (children && frame.next < children.length) {
866
+ enter(children[frame.next++], frame.depth + 1);
867
+ continue;
868
+ }
869
+ stack.pop();
870
+ if (frame.matched) {
871
+ matches.add(frame.node.item.key);
872
+ if (stack.length > 0) stack[stack.length - 1].matched = true;
873
+ }
874
+ }
875
+ }
876
+ return matches;
877
+ }
878
+ /**
879
+ * Merge ghost rows back near their previous position.
880
+ * Each ghost goes immediately before the first row that followed it last render
881
+ * and still exists; otherwise at the end. Ghosts sharing an anchor keep their
882
+ * previous relative order. O(rows + prevKeys).
883
+ *
884
+ * @param rows - Current flat rows (with node and depth)
885
+ * @param prevKeys - Previous render's row keys in order
886
+ * @param ghosts - Map of ghost keys to their row data
887
+ * @returns Rows with ghosts woven in at their original relative positions
888
+ */
889
+ function weaveGhosts(rows, prevKeys, ghosts) {
890
+ if (ghosts.size === 0) return [...rows];
891
+ const present = /* @__PURE__ */ new Set();
892
+ for (const r of rows) present.add(r.node.item.key);
893
+ const END = Symbol(`end`);
894
+ const before = /* @__PURE__ */ new Map();
895
+ let nextSurvivor = END;
896
+ for (let i = prevKeys.length - 1; i >= 0; i--) {
897
+ const key = prevKeys[i];
898
+ if (present.has(key)) {
899
+ nextSurvivor = key;
900
+ continue;
901
+ }
902
+ const ghost = ghosts.get(key);
903
+ if (!ghost) continue;
904
+ const list = before.get(nextSurvivor);
905
+ if (list) list.push(ghost);
906
+ else before.set(nextSurvivor, [ghost]);
907
+ }
908
+ const result = [];
909
+ for (const row of rows) {
910
+ const list = before.get(row.node.item.key);
911
+ if (list) for (let i = list.length - 1; i >= 0; i--) result.push(list[i]);
912
+ result.push(row);
913
+ }
914
+ const tail = before.get(END);
915
+ if (tail) for (let i = tail.length - 1; i >= 0; i--) result.push(tail[i]);
916
+ return result;
917
+ }
918
+ /**
919
+ * Compute checkbox states for all nodes in a single iterative post-order pass.
920
+ * A leaf (or a branch with no children) uses `selected.has(node)`; otherwise a
921
+ * branch is checked if all children are checked, unchecked if all are
922
+ * unchecked, else indeterminate.
923
+ *
924
+ * @param root - The root node
925
+ * @param selected - Set of selected nodes
926
+ * @returns Map from node key to checkbox state
927
+ */
928
+ function computeCheckboxStates(root, selected) {
929
+ const states = /* @__PURE__ */ new Map();
930
+ if (!root.children) return states;
931
+ const stack = [];
932
+ for (const top of root.children) {
933
+ stack.push({
934
+ node: top,
935
+ next: 0,
936
+ checked: 0,
937
+ unchecked: 0
938
+ });
939
+ while (stack.length > 0) {
940
+ const frame = stack[stack.length - 1];
941
+ const { node } = frame;
942
+ const children = node.children;
943
+ const isLeafLike = node.item.isLeaf || !children?.length;
944
+ if (!isLeafLike && frame.next < children.length) {
945
+ stack.push({
946
+ node: children[frame.next++],
947
+ next: 0,
948
+ checked: 0,
949
+ unchecked: 0
950
+ });
951
+ continue;
952
+ }
953
+ stack.pop();
954
+ let state;
955
+ if (isLeafLike) state = selected.has(node) ? `checked` : `unchecked`;
956
+ else if (frame.checked === children.length) state = `checked`;
957
+ else if (frame.unchecked === children.length) state = `unchecked`;
958
+ else state = `indeterminate`;
959
+ states.set(node.item.key, state);
960
+ if (stack.length > 0) {
961
+ const parent = stack[stack.length - 1];
962
+ if (state === `checked`) parent.checked++;
963
+ else if (state === `unchecked`) parent.unchecked++;
964
+ }
965
+ }
966
+ }
967
+ return states;
968
+ }
969
+ function withKey(set, key) {
970
+ if (set.has(key)) return set;
971
+ const next = new Set(set);
972
+ next.add(key);
973
+ return next;
974
+ }
975
+ function withoutKey(set, key) {
976
+ if (!set.has(key)) return set;
977
+ const next = new Set(set);
978
+ next.delete(key);
979
+ return next;
980
+ }
981
+ /** Remove every key for which `pred` returns true. */
982
+ function withoutKeys(set, pred) {
983
+ let next;
984
+ for (const key of set) if (pred(key)) {
985
+ next ??= new Set(set);
986
+ next.delete(key);
987
+ }
988
+ return next ?? set;
989
+ }
990
+ function treeStride(params) {
991
+ return Math.max(1, params.rowHeight + params.gap);
992
+ }
993
+ /** Content-space top of the row box at `index`. */
994
+ function treeRowTop(params, index) {
995
+ return params.listOffset + index * treeStride(params) + params.gap;
996
+ }
997
+ function treeRowBottom(params, index) {
998
+ return treeRowTop(params, index) + params.rowHeight;
999
+ }
1000
+ /** The window of rows to render for a given scroll offset. Always `0 <= start <= end <= count`. */
1001
+ function computeTreeWindow(params, scrollTop, overscan = 5) {
1002
+ const { count } = params;
1003
+ if (count <= 0) return {
1004
+ start: 0,
1005
+ end: 0
1006
+ };
1007
+ const stride = treeStride(params);
1008
+ const top = Math.max(0, scrollTop - params.listOffset);
1009
+ const firstVisible = Math.floor(top / stride);
1010
+ const lastVisible = Math.ceil((top + params.viewportHeight) / stride);
1011
+ const start = Math.min(count, Math.max(0, firstVisible - overscan));
1012
+ return {
1013
+ start,
1014
+ end: Math.min(count, Math.max(start, lastVisible + overscan))
1015
+ };
1016
+ }
1017
+ /**
1018
+ * The scroll offset that brings row `index` fully into view with the least
1019
+ * movement, leaving `topInset` px at the top clear (the sticky-header overlay).
1020
+ * Returns `scrollTop` unchanged when the row is already visible.
1021
+ */
1022
+ function treeScrollTopToReveal(params, index, scrollTop, topInset = 0) {
1023
+ if (index < 0 || index >= params.count) return scrollTop;
1024
+ const top = treeRowTop(params, index);
1025
+ const bottom = top + params.rowHeight;
1026
+ if (top < scrollTop + topInset) return index === 0 ? 0 : Math.max(0, top - topInset);
1027
+ if (bottom > scrollTop + params.viewportHeight) return Math.max(0, bottom - params.viewportHeight);
1028
+ return scrollTop;
1029
+ }
1030
+ /** Rows that fit one viewport (PageUp / PageDown step), at least 1. */
1031
+ function treePageSize(params) {
1032
+ return Math.max(1, Math.floor(params.viewportHeight / treeStride(params)) - 1);
1033
+ }
1034
+ /** Arithmetic `RowGeometryReader` over flat-row indices (the windowed counterpart of reading `offsetTop`). */
1035
+ function treeRowReader(params) {
1036
+ return {
1037
+ count: params.count,
1038
+ topOf: (i) => treeRowTop(params, i),
1039
+ bottomOf: (i) => treeRowBottom(params, i)
1040
+ };
1041
+ }
1042
+ //#endregion
1043
+ //#region src/tree/tree-row-geometry.ts
1044
+ /**
1045
+ * First index whose bottom is greater than `y` (binary search; rows are
1046
+ * monotonic). Returns `reader.count` when no row qualifies.
1047
+ */
1048
+ function firstRowBelow(reader, y) {
1049
+ let lo = 0;
1050
+ let hi = reader.count;
1051
+ while (lo < hi) {
1052
+ const mid = lo + hi >>> 1;
1053
+ if (reader.bottomOf(mid) > y) hi = mid;
1054
+ else lo = mid + 1;
1055
+ }
1056
+ return lo;
1057
+ }
1058
+ /**
1059
+ * Inclusive index range of rows that overlap `[top, bottom]`, widened by
1060
+ * `margin` rows on each side. Returns `undefined` when there are no rows.
1061
+ */
1062
+ function rowRangeOverlapping(reader, top, bottom, margin = 0) {
1063
+ if (reader.count === 0) return void 0;
1064
+ const firstHit = Math.min(firstRowBelow(reader, top), reader.count - 1);
1065
+ let lo = firstHit;
1066
+ let hi = reader.count;
1067
+ while (lo < hi) {
1068
+ const mid = lo + hi >>> 1;
1069
+ if (reader.topOf(mid) >= bottom) hi = mid;
1070
+ else lo = mid + 1;
1071
+ }
1072
+ const lastHit = Math.max(firstHit, lo - 1);
1073
+ return {
1074
+ first: Math.max(0, firstHit - margin),
1075
+ last: Math.min(reader.count - 1, lastHit + margin)
1076
+ };
1077
+ }
1078
+ //#endregion
763
1079
  //#region src/tree/tree-base.ts
764
1080
  /** Above this many rows entering / leaving at once, skip the animation entirely. */
765
1081
  const MAX_ANIMATED_ROWS = 50;
1082
+ const EMPTY_FLAT = {
1083
+ rows: [],
1084
+ indexByKey: /* @__PURE__ */ new Map()
1085
+ };
1086
+ /** Viewport height assumed until the container has been measured (or when it reads 0). */
1087
+ const FALLBACK_VIEWPORT_HEIGHT = 600;
766
1088
  let TreeBaseElement = class TreeBaseElement extends LitElement {
767
1089
  constructor(..._args) {
768
1090
  super(..._args);
@@ -776,11 +1098,33 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
776
1098
  this.dndExpandHoverMs = 500;
777
1099
  this.reorderable = true;
778
1100
  this.stickyHeaders = true;
1101
+ this.virtualize = `auto`;
1102
+ this.virtualizeThreshold = 200;
779
1103
  this.applyInternalDrop = true;
780
1104
  this._expandedKeys = /* @__PURE__ */ new Set();
1105
+ this.#structureVersion = 0;
1106
+ this._selectionVersion = 0;
1107
+ this._scrollTop = 0;
1108
+ this.#metrics = {
1109
+ rowHeight: 30,
1110
+ gap: 1,
1111
+ listOffset: 0
1112
+ };
1113
+ this.#viewportH = 0;
1114
+ this.#virtualScrollRaf = 0;
1115
+ this.#winStart = 0;
1116
+ this.#winEnd = 0;
1117
+ this.#scrollOnlyUpdate = false;
1118
+ this.#onVirtualScroll = () => {
1119
+ if (this.#virtualScrollRaf !== 0) return;
1120
+ this.#virtualScrollRaf = requestAnimationFrame(() => {
1121
+ this.#virtualScrollRaf = 0;
1122
+ this.#syncWindowToScroll();
1123
+ });
1124
+ };
781
1125
  this.tickled = new TickledController(this, {
782
1126
  getItems: () => {
783
- return this.#getAllNodes().map(({ node }) => ({
1127
+ return this.#getFlat().rows.map(({ node }) => ({
784
1128
  id: node.item.key,
785
1129
  enabled: true
786
1130
  }));
@@ -797,6 +1141,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
797
1141
  }) });
798
1142
  this.#config = { indent: 20 };
799
1143
  this.#renderedKeys = [];
1144
+ this.#renderedRowKeys = [];
1145
+ this.#renderedRowIndex = /* @__PURE__ */ new Map();
800
1146
  this.#lastNodeByKey = /* @__PURE__ */ new Map();
801
1147
  this.#enteringKeys = /* @__PURE__ */ new Set();
802
1148
  this.#leaving = /* @__PURE__ */ new Map();
@@ -820,9 +1166,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
820
1166
  this.#stickyDirty = true;
821
1167
  this.#stickyRaf = 0;
822
1168
  this.#stickyScrollBound = false;
1169
+ this.#revealToken = 0;
823
1170
  this.#onKeyDown = (event) => {
824
1171
  if (this.search.isOpen) return;
825
- const nodes = this.#getAllNodes();
1172
+ const nodes = this.#getFlat().rows;
826
1173
  if (nodes.length === 0) return;
827
1174
  if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
828
1175
  event.preventDefault();
@@ -834,29 +1181,41 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
834
1181
  `ArrowUp`,
835
1182
  `ArrowLeft`,
836
1183
  `ArrowRight`,
1184
+ `Home`,
1185
+ `End`,
1186
+ `PageUp`,
1187
+ `PageDown`,
837
1188
  `Enter`,
838
1189
  ` `
839
1190
  ].includes(event.key)) this.tickled.setInputMode(`keyboard`);
840
1191
  const currentIdx = this.#getFocusedIndex();
841
1192
  switch (event.key) {
842
- case `ArrowDown`: {
1193
+ case `ArrowDown`:
843
1194
  event.preventDefault();
844
- const nextIdx = Math.min(currentIdx + 1, nodes.length - 1);
845
- if (this.interactionMode === `implicit`) this.select(nodes[nextIdx].node);
846
- this.#setFocusedIndex(nextIdx);
847
- if (event.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, nextIdx);
848
- else if (this.interactionMode === `vscode`) this.#anchor = void 0;
849
- this.requestUpdate();
1195
+ this.#navigateToIndex(nodes, currentIdx, Math.min(currentIdx + 1, nodes.length - 1), event.shiftKey);
1196
+ break;
1197
+ case `ArrowUp`:
1198
+ event.preventDefault();
1199
+ this.#navigateToIndex(nodes, currentIdx, Math.max(currentIdx - 1, 0), event.shiftKey);
1200
+ break;
1201
+ case `Home`:
1202
+ event.preventDefault();
1203
+ this.#navigateToIndex(nodes, currentIdx, 0, event.shiftKey);
1204
+ break;
1205
+ case `End`:
1206
+ event.preventDefault();
1207
+ this.#navigateToIndex(nodes, currentIdx, nodes.length - 1, event.shiftKey);
1208
+ break;
1209
+ case `PageDown`: {
1210
+ event.preventDefault();
1211
+ const page = this.#pageSize();
1212
+ this.#navigateToIndex(nodes, currentIdx, Math.min(currentIdx + page, nodes.length - 1), event.shiftKey);
850
1213
  break;
851
1214
  }
852
- case `ArrowUp`: {
1215
+ case `PageUp`: {
853
1216
  event.preventDefault();
854
- const prevIdx = Math.max(currentIdx - 1, 0);
855
- if (this.interactionMode === `implicit`) this.select(nodes[prevIdx].node);
856
- this.#setFocusedIndex(prevIdx);
857
- if (event.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, prevIdx);
858
- else if (this.interactionMode === `vscode`) this.#anchor = void 0;
859
- this.requestUpdate();
1217
+ const page = this.#pageSize();
1218
+ this.#navigateToIndex(nodes, currentIdx, Math.max(currentIdx - page, 0), event.shiftKey);
860
1219
  break;
861
1220
  }
862
1221
  case `ArrowRight`: {
@@ -873,7 +1232,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
873
1232
  event.preventDefault();
874
1233
  const current = nodes[currentIdx];
875
1234
  if (current && this._expandedKeys.has(current.node.item.key)) {
876
- this._expandedKeys.delete(current.node.item.key);
1235
+ this.#setExpanded(withoutKey(this._expandedKeys, current.node.item.key));
877
1236
  this.dispatchEvent(new CustomEvent(`collapse`, {
878
1237
  detail: {
879
1238
  node: current.node,
@@ -927,7 +1286,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
927
1286
  if (!li) return;
928
1287
  const key = li.dataset.key;
929
1288
  if (!key) return;
930
- const node = this.#getAllNodes().find((n) => n.node.item.key === key);
1289
+ const flat = this.#getFlat();
1290
+ const idx = flat.indexByKey.get(key);
1291
+ const node = idx === void 0 ? void 0 : flat.rows[idx];
931
1292
  if (!node) return;
932
1293
  const detail = {
933
1294
  node: node.node,
@@ -948,6 +1309,206 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
948
1309
  get selectedNodes() {
949
1310
  return this._selectedNodes;
950
1311
  }
1312
+ #setExpanded(next) {
1313
+ if (next === this._expandedKeys) return;
1314
+ this._expandedKeys = next;
1315
+ this.#flat = void 0;
1316
+ this.#stickyDirty = true;
1317
+ }
1318
+ #flat;
1319
+ #flatRoot;
1320
+ #flatFilter;
1321
+ #structureVersion;
1322
+ #treeIndexCache;
1323
+ #filterCache;
1324
+ /**
1325
+ * Call after editing `node.children` / `node.item` in place (outside
1326
+ * `TreeDataModel`). Drops the cached whole-tree indexes and re-renders.
1327
+ */
1328
+ invalidateStructure() {
1329
+ this.#structureVersion++;
1330
+ this.#flat = void 0;
1331
+ this.requestUpdate();
1332
+ }
1333
+ /** Visible rows in display order (cached for the current update). */
1334
+ #getFlat() {
1335
+ if (!this.root) return EMPTY_FLAT;
1336
+ if (this.#flat && this.#flatRoot === this.root && this.#flatFilter === this.filterPredicate) return this.#flat;
1337
+ const filter = this.#getFilter();
1338
+ this.#flat = flattenTree(this.root, {
1339
+ isExpanded: (node, depth) => this._isExpanded(node, depth),
1340
+ isShown: filter ? (node) => filter.matches.has(node.item.key) : () => true
1341
+ });
1342
+ this.#flatRoot = this.root;
1343
+ this.#flatFilter = this.filterPredicate;
1344
+ return this.#flat;
1345
+ }
1346
+ /** Whole-loaded-tree index (parents, icon depths); cached on `(root, structureVersion)`. */
1347
+ #getTreeIndex() {
1348
+ const root = this.root;
1349
+ if (!root) return {
1350
+ parentByKey: /* @__PURE__ */ new Map(),
1351
+ depthsWithIcons: /* @__PURE__ */ new Set()
1352
+ };
1353
+ const cached = this.#treeIndexCache;
1354
+ if (cached && cached.root === root && cached.version === this.#structureVersion) return cached.index;
1355
+ const index = indexTree(root);
1356
+ this.#treeIndexCache = {
1357
+ root,
1358
+ version: this.#structureVersion,
1359
+ index
1360
+ };
1361
+ return index;
1362
+ }
1363
+ /** Row height / gap / list offset as last measured from CSS + layout. */
1364
+ #metrics;
1365
+ /** Last non-zero container height (a hidden dock tab reads 0). */
1366
+ #viewportH;
1367
+ #virtualScrollRaf;
1368
+ #virtualScrollContainer;
1369
+ #resizeObserver;
1370
+ #winStart;
1371
+ #winEnd;
1372
+ /** Set in `willUpdate` when only the scroll offset changed; consumed in `updated`. */
1373
+ #scrollOnlyUpdate;
1374
+ /** True when the current visible row count should be windowed. */
1375
+ #virtualActive() {
1376
+ if (this.virtualize === `off`) return false;
1377
+ if (this.virtualize === `on`) return true;
1378
+ return this.#getFlat().rows.length > this.virtualizeThreshold;
1379
+ }
1380
+ #layoutParams(count) {
1381
+ return {
1382
+ count,
1383
+ rowHeight: this.#metrics.rowHeight,
1384
+ gap: this.#metrics.gap,
1385
+ listOffset: this.#metrics.listOffset,
1386
+ viewportHeight: this.#viewportH > 0 ? this.#viewportH : FALLBACK_VIEWPORT_HEIGHT
1387
+ };
1388
+ }
1389
+ /**
1390
+ * Re-read the CSS metrics and viewport height. Returns true when anything
1391
+ * that changes row positions moved (the caller re-renders).
1392
+ */
1393
+ #measureMetrics() {
1394
+ const list = this.renderRoot.querySelector(`ul.tree-list`);
1395
+ const sample = list?.querySelector(`li.item:not(.leaving)`);
1396
+ const measuredHeight = sample?.getBoundingClientRect().height ?? 0;
1397
+ const rowHeight = measuredHeight > 0 ? measuredHeight : this.#getItemHeight();
1398
+ const measuredGap = sample ? Number.parseFloat(getComputedStyle(sample).marginTop) : NaN;
1399
+ const parsedGap = Number.isNaN(measuredGap) ? Number.parseFloat(getComputedStyle(this).getPropertyValue(`--tree-item-gap`)) : measuredGap;
1400
+ const gap = Number.isNaN(parsedGap) ? 1 : parsedGap;
1401
+ let listOffset = this.#metrics.listOffset;
1402
+ if (list) {
1403
+ const padTop = Number.parseFloat(getComputedStyle(list).paddingTop);
1404
+ listOffset = list.offsetTop + (Number.isNaN(padTop) ? 0 : padTop);
1405
+ }
1406
+ const height = this.#container()?.clientHeight ?? 0;
1407
+ const heightChanged = height > 0 && height !== this.#viewportH;
1408
+ if (height > 0) this.#viewportH = height;
1409
+ const prev = this.#metrics;
1410
+ const changed = prev.rowHeight !== rowHeight || prev.gap !== gap || prev.listOffset !== listOffset;
1411
+ this.#metrics = {
1412
+ rowHeight,
1413
+ gap,
1414
+ listOffset
1415
+ };
1416
+ return changed || heightChanged;
1417
+ }
1418
+ /** The window for the current scroll offset. */
1419
+ #currentWindow(count) {
1420
+ return computeTreeWindow(this.#layoutParams(count), this._scrollTop);
1421
+ }
1422
+ /** Pull the container's real scroll offset into state (programmatic and clamped scrolls included). */
1423
+ #syncScrollTopFromContainer() {
1424
+ const top = this.#container()?.scrollTop;
1425
+ if (top !== void 0 && top !== this._scrollTop) this._scrollTop = top;
1426
+ }
1427
+ #onVirtualScroll;
1428
+ /** Re-render only when the scroll has moved the window; scrolling inside it is free. */
1429
+ #syncWindowToScroll() {
1430
+ const container = this.#container();
1431
+ if (!container || !this.root || !this.#virtualActive()) return;
1432
+ const top = container.scrollTop;
1433
+ if (top === this._scrollTop) return;
1434
+ const next = computeTreeWindow(this.#layoutParams(this.#getFlat().rows.length), top);
1435
+ if (next.start !== this.#winStart || next.end !== this.#winEnd) this._scrollTop = top;
1436
+ }
1437
+ /** Bind the window's scroll listener and resize observer to the container once it exists. */
1438
+ #syncVirtualObservers() {
1439
+ const container = this.#container();
1440
+ if (container === this.#virtualScrollContainer) return;
1441
+ this.#virtualScrollContainer?.removeEventListener(`scroll`, this.#onVirtualScroll);
1442
+ this.#resizeObserver?.disconnect();
1443
+ this.#resizeObserver = void 0;
1444
+ this.#virtualScrollContainer = container;
1445
+ if (!container) return;
1446
+ container.addEventListener(`scroll`, this.#onVirtualScroll, { passive: true });
1447
+ if (typeof ResizeObserver !== `undefined`) {
1448
+ this.#resizeObserver = new ResizeObserver((entries) => {
1449
+ const height = entries.at(-1)?.contentRect.height ?? 0;
1450
+ if (height > 0 && height !== this.#viewportH && this.#virtualActive()) this.requestUpdate();
1451
+ });
1452
+ this.#resizeObserver.observe(container);
1453
+ }
1454
+ }
1455
+ #teardownVirtualObservers() {
1456
+ this.#virtualScrollContainer?.removeEventListener(`scroll`, this.#onVirtualScroll);
1457
+ this.#virtualScrollContainer = void 0;
1458
+ this.#resizeObserver?.disconnect();
1459
+ this.#resizeObserver = void 0;
1460
+ if (this.#virtualScrollRaf !== 0) {
1461
+ cancelAnimationFrame(this.#virtualScrollRaf);
1462
+ this.#virtualScrollRaf = 0;
1463
+ }
1464
+ }
1465
+ /**
1466
+ * Scroll flat row `index` into view (below the sticky overlay). Writes the
1467
+ * container's `scrollTop` and `_scrollTop` together, so the very next render
1468
+ * already contains the target row — no frame where it is missing.
1469
+ */
1470
+ #revealIndex(index) {
1471
+ const container = this.#container();
1472
+ if (!container) return;
1473
+ const params = this.#layoutParams(this.#getFlat().rows.length);
1474
+ const inset = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
1475
+ const next = treeScrollTopToReveal(params, index, container.scrollTop, inset);
1476
+ if (next === container.scrollTop && next === this._scrollTop) return;
1477
+ container.scrollTop = next;
1478
+ this._scrollTop = next;
1479
+ }
1480
+ /** Filter match sets, or `undefined` when no filter is active. */
1481
+ #getFilter() {
1482
+ const predicate = this.filterPredicate;
1483
+ const root = this.root;
1484
+ if (!predicate || !root) return void 0;
1485
+ const cached = this.#filterCache;
1486
+ if (cached && cached.predicate === predicate && cached.root === root && cached.version === this.#structureVersion) return cached;
1487
+ const matches = computeFilterMatches(root, predicate);
1488
+ const { parentByKey } = this.#getTreeIndex();
1489
+ const autoExpanded = /* @__PURE__ */ new Set();
1490
+ for (const key of matches) {
1491
+ const parent = parentByKey.get(key);
1492
+ if (parent !== void 0) autoExpanded.add(parent);
1493
+ }
1494
+ const next = {
1495
+ predicate,
1496
+ root,
1497
+ version: this.#structureVersion,
1498
+ matches,
1499
+ autoExpanded
1500
+ };
1501
+ this.#filterCache = next;
1502
+ return next;
1503
+ }
1504
+ /** Structure version for cache keys in subclasses (bumped on root / model change and lazy loads). */
1505
+ get _structureVersion() {
1506
+ return this.#structureVersion;
1507
+ }
1508
+ /** Depths at which any loaded node carries an icon (stable across expansion). */
1509
+ get _depthsWithIcons() {
1510
+ return this.#getTreeIndex().depthsWithIcons;
1511
+ }
951
1512
  #unregisterSearchCommands;
952
1513
  #effectiveSearchModes() {
953
1514
  const raw = this.searchModes?.trim();
@@ -968,6 +1529,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
968
1529
  #ownedRegistry;
969
1530
  #config;
970
1531
  #renderedKeys;
1532
+ /** Keys in the exact DOM order of the last render (ghost rows woven in). */
1533
+ #renderedRowKeys;
1534
+ #renderedRowIndex;
971
1535
  #lastNodeByKey;
972
1536
  #enteringKeys;
973
1537
  #leaving;
@@ -1002,73 +1566,65 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1002
1566
  }
1003
1567
  }
1004
1568
  #onStickyScroll;
1005
- /** Real (non-sticky) row element for `key`, or `null`. */
1006
- #stickyRowEl(key) {
1007
- return this.renderRoot.querySelector(`ul.tree-list li[data-key="${CSS.escape(key)}"]`);
1569
+ /**
1570
+ * Row geometry over the rendered `<li>` elements (ghost rows included), in
1571
+ * container content coordinates. `#container` is `position: relative`, so
1572
+ * `offsetTop` is scroll-independent. Reads only — no layout writes.
1573
+ */
1574
+ #rowReader(rows) {
1575
+ return {
1576
+ count: rows.length,
1577
+ topOf: (i) => rows[i].offsetTop,
1578
+ bottomOf: (i) => rows[i].offsetTop + rows[i].offsetHeight
1579
+ };
1008
1580
  }
1009
1581
  /**
1010
1582
  * Recompute which ancestors stick to the top: the ancestors of the first
1011
1583
  * row whose bottom clears the container top, minus any ancestor that is
1012
1584
  * itself still visible below the overlay. Synchronous — reads current
1013
- * geometry, renders nothing, expands nothing.
1585
+ * geometry, renders nothing, expands nothing. O(log N) offset reads + O(depth).
1014
1586
  */
1015
1587
  #updateStickyHeaders() {
1016
1588
  this.#stickyDirty = false;
1017
1589
  const empty = /* @__PURE__ */ new Map();
1018
- if (!this.stickyHeaders || !this.root) {
1019
- this.#setStickyKeys([], empty);
1020
- return;
1021
- }
1590
+ const clear = () => this.#setStickyKeys([], empty);
1591
+ if (!this.stickyHeaders || !this.root) return clear();
1022
1592
  const container = this.#container();
1023
- if (!container) {
1024
- this.#setStickyKeys([], empty);
1025
- return;
1026
- }
1027
- const nodes = this.#getAllNodes();
1028
- if (nodes.length === 0) {
1029
- this.#setStickyKeys([], empty);
1030
- return;
1031
- }
1032
- const parent = /* @__PURE__ */ new Map();
1033
- const walk = (n) => {
1034
- for (const child of n.children ?? []) {
1035
- parent.set(child.item.key, n.item.key);
1036
- walk(child);
1037
- }
1038
- };
1039
- walk(this.root);
1040
- const containerTop = container.getBoundingClientRect().top;
1041
- const visibleThreshold = containerTop + (this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0);
1042
- let topKey;
1043
- for (const { node } of nodes) {
1044
- const li = this.#stickyRowEl(node.item.key);
1045
- if (!li || li.classList.contains(`leaving`)) continue;
1046
- if (li.getBoundingClientRect().bottom > containerTop) {
1047
- topKey = node.item.key;
1048
- break;
1049
- }
1050
- }
1051
- if (topKey === void 0) {
1052
- this.#setStickyKeys([], empty);
1053
- return;
1054
- }
1055
- const rootKey = this.root.item.key;
1593
+ const list = this.renderRoot.querySelector(`ul.tree-list`);
1594
+ if (!container || !list) return clear();
1595
+ const flat = this.#getFlat();
1596
+ const virtual = this.#virtualActive();
1597
+ const rows = list.children;
1598
+ const keys = this.#renderedRowKeys;
1599
+ if (flat.rows.length === 0) return clear();
1600
+ if (!virtual && (rows.length === 0 || rows.length !== keys.length)) return clear();
1601
+ const reader = virtual ? treeRowReader(this.#layoutParams(flat.rows.length)) : this.#rowReader(rows);
1602
+ const scrollTop = container.scrollTop;
1603
+ const visibleThreshold = scrollTop + (this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0);
1604
+ let topIdx = firstRowBelow(reader, scrollTop);
1605
+ if (!virtual) while (topIdx < rows.length && rows[topIdx].classList.contains(`leaving`)) topIdx++;
1606
+ if (topIdx >= reader.count) return clear();
1607
+ const topFlat = virtual ? topIdx : flat.indexByKey.get(keys[topIdx]);
1608
+ if (topFlat === void 0) return clear();
1056
1609
  const chain = [];
1057
- let cursor = parent.get(topKey);
1058
- while (cursor !== void 0 && cursor !== rootKey) {
1059
- chain.unshift(cursor);
1060
- cursor = parent.get(cursor);
1610
+ let cursor = flat.rows[topFlat].parentKey;
1611
+ while (cursor !== void 0) {
1612
+ const idx = flat.indexByKey.get(cursor);
1613
+ if (idx === void 0) break;
1614
+ chain.unshift(flat.rows[idx]);
1615
+ cursor = flat.rows[idx].parentKey;
1061
1616
  }
1062
- const byKey = new Map(nodes.map((entry) => [entry.node.item.key, entry]));
1063
1617
  const entries = /* @__PURE__ */ new Map();
1064
1618
  const stuck = [];
1065
- for (const key of chain) {
1066
- const li = this.#stickyRowEl(key);
1067
- if (!li || li.getBoundingClientRect().bottom > visibleThreshold) continue;
1068
- const entry = byKey.get(key);
1069
- if (!entry) continue;
1619
+ for (const row of chain) {
1620
+ const key = row.node.item.key;
1621
+ const domIdx = virtual ? flat.indexByKey.get(key) : this.#renderedRowIndex.get(key);
1622
+ if (domIdx === void 0 || reader.bottomOf(domIdx) > visibleThreshold) continue;
1070
1623
  stuck.push(key);
1071
- entries.set(key, entry);
1624
+ entries.set(key, {
1625
+ node: row.node,
1626
+ depth: row.depth
1627
+ });
1072
1628
  }
1073
1629
  this.#setStickyKeys(stuck, entries);
1074
1630
  }
@@ -1081,12 +1637,14 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1081
1637
  #renderStickyHeaders() {
1082
1638
  if (!this.stickyHeaders || this._stickyKeys.length === 0) return nothing;
1083
1639
  return html`
1084
- <div class="sticky-headers" part="sticky-headers">
1085
- ${this._stickyKeys.map((key) => {
1640
+ <div class="sticky-anchor">
1641
+ <div class="sticky-headers" part="sticky-headers">
1642
+ ${this._stickyKeys.map((key) => {
1086
1643
  const entry = this.#stickyEntries.get(key);
1087
1644
  if (!entry) return nothing;
1088
1645
  return this.#renderStickyHeader(entry.node, entry.depth);
1089
1646
  })}
1647
+ </div>
1090
1648
  </div>
1091
1649
  `;
1092
1650
  }
@@ -1158,15 +1716,44 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1158
1716
  }
1159
1717
  willUpdate(changedProps) {
1160
1718
  super.willUpdate(changedProps);
1161
- this.#diffVisibleNodes();
1719
+ const scrollOnly = this.#flat !== void 0 && changedProps.size === 1 && changedProps.has(`_scrollTop`) && this.#virtualActive();
1720
+ this.#scrollOnlyUpdate = scrollOnly;
1721
+ if (scrollOnly) {
1722
+ this.#syncScrollTopFromContainer();
1723
+ return;
1724
+ }
1725
+ this.#flat = void 0;
1726
+ if (changedProps.has(`root`) || changedProps.has(`model`)) this.#structureVersion++;
1727
+ this.#diffVisibleNodes(changedProps.has(`filterPredicate`));
1728
+ const virtual = this.#virtualActive();
1729
+ this.toggleAttribute(`data-virtual`, virtual);
1730
+ if (virtual) {
1731
+ this.#measureMetrics();
1732
+ this.#syncScrollTopFromContainer();
1733
+ }
1162
1734
  if (changedProps.has(`root`) || changedProps.has(`model`) || changedProps.has(`filterPredicate`) || changedProps.has(`stickyHeaders`)) this.#stickyDirty = true;
1163
1735
  if (this.#stickyDirty) this.#updateStickyHeaders();
1164
1736
  }
1165
- #diffVisibleNodes() {
1166
- const nodes = this.root ? this.#getAllNodes() : [];
1737
+ /**
1738
+ * `filterChanged`: typing in a filter adds / removes whole rows from the DOM.
1739
+ * That is not an expand / collapse, so skip the enter / leave animations
1740
+ * (and any ghosts) for this update; `#renderedKeys` is still refreshed.
1741
+ */
1742
+ #diffVisibleNodes(filterChanged) {
1743
+ const nodes = this.#getFlat().rows;
1744
+ if (this.#virtualActive()) {
1745
+ this.#enteringKeys.clear();
1746
+ this.#cancelAllLeavingAnimations();
1747
+ this.#leaving.clear();
1748
+ this.#renderedKeys = [];
1749
+ return;
1750
+ }
1167
1751
  const nowKeys = new Set(nodes.map((n) => n.node.item.key));
1168
1752
  this.#enteringKeys.clear();
1169
- if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
1753
+ if (filterChanged) {
1754
+ this.#cancelAllLeavingAnimations();
1755
+ this.#leaving.clear();
1756
+ } else if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
1170
1757
  const prevSet = new Set(this.#renderedKeys);
1171
1758
  for (const { node } of nodes) if (!prevSet.has(node.item.key)) this.#enteringKeys.add(node.item.key);
1172
1759
  if (this.#enteringKeys.size > MAX_ANIMATED_ROWS) this.#enteringKeys.clear();
@@ -1189,30 +1776,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1189
1776
  return this.#leaving.has(node.item.key);
1190
1777
  }
1191
1778
  /**
1192
- * True when `filterPredicate` is set and neither `node` nor any descendant
1193
- * matches — the row should carry the `filtered` class (`display: none`).
1779
+ * Kept for subclass compatibility. Filtered-out rows are no longer rendered
1780
+ * at all (they are omitted from the flat list), so this is `false` for every
1781
+ * rendered row.
1194
1782
  */
1195
- _isFilteredOut(node, depth) {
1196
- return this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
1197
- }
1198
- /** Splice the fading ghost rows back into the visible list near where they were. */
1199
- #weaveLeaving(nodes) {
1200
- if (this.#leaving.size === 0) return nodes;
1201
- const result = [...nodes];
1202
- const indexOfKey = (key) => result.findIndex((e) => e.node.item.key === key);
1203
- for (const [key, entry] of this.#leaving) {
1204
- const prevIdx = this.#renderedKeys.indexOf(key);
1205
- let insertAt = result.length;
1206
- for (let j = prevIdx + 1; j < this.#renderedKeys.length; j++) {
1207
- const at = indexOfKey(this.#renderedKeys[j]);
1208
- if (at !== -1) {
1209
- insertAt = at;
1210
- break;
1211
- }
1212
- }
1213
- result.splice(insertAt, 0, entry);
1214
- }
1215
- return result;
1783
+ _isFilteredOut(_node, _depth) {
1784
+ return false;
1216
1785
  }
1217
1786
  #runItemAnimations() {
1218
1787
  if (this.#enteringKeys.size > 0) {
@@ -1252,7 +1821,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1252
1821
  });
1253
1822
  });
1254
1823
  }
1255
- const visible = this.root ? this.#getAllNodes() : [];
1824
+ const visible = this.#getFlat().rows;
1256
1825
  this.#renderedKeys = visible.map((n) => n.node.item.key);
1257
1826
  this.#lastNodeByKey.clear();
1258
1827
  for (const entry of visible) this.#lastNodeByKey.set(entry.node.item.key, entry);
@@ -1260,10 +1829,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1260
1829
  }
1261
1830
  updated(changedProps) {
1262
1831
  this.#rowsCache = void 0;
1832
+ this.#syncVirtualObservers();
1833
+ if (this.#scrollOnlyUpdate) {
1834
+ this.#scrollOnlyUpdate = false;
1835
+ this.#repaintSearchHighlights();
1836
+ return;
1837
+ }
1263
1838
  this.#syncStickyScroll();
1264
1839
  if (changedProps.has(`root`) && this.root) {
1265
1840
  this.#abortPending();
1266
- this._expandedKeys.clear();
1841
+ this.#setExpanded(/* @__PURE__ */ new Set());
1267
1842
  this.#clearSelectionInternal();
1268
1843
  this.tickled.clearTickled();
1269
1844
  this._focusedDepth = 0;
@@ -1283,8 +1858,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1283
1858
  this.requestUpdate();
1284
1859
  } else if (event.type === `invalidate`) {
1285
1860
  if (this._expandedKeys.has(event.key)) {
1286
- this._expandedKeys.delete(event.key);
1287
- this.#stickyDirty = true;
1861
+ this.#setExpanded(withoutKey(this._expandedKeys, event.key));
1288
1862
  this.requestUpdate();
1289
1863
  }
1290
1864
  }
@@ -1298,7 +1872,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1298
1872
  if (changedProps.has(`selectedNode`) && this.selectedNode) {
1299
1873
  if (changedProps.get(`selectedNode`) !== this.selectedNode) this.tickled.setTickled(this.selectedNode.item.key);
1300
1874
  }
1301
- this.#runItemAnimations();
1875
+ if (this.#virtualActive()) {
1876
+ this.#renderedKeys = [];
1877
+ this.#lastNodeByKey.clear();
1878
+ if (this.#measureMetrics()) queueMicrotask(() => this.requestUpdate());
1879
+ this.#repaintSearchHighlights();
1880
+ } else this.#runItemAnimations();
1881
+ }
1882
+ /** Highlights are painted on rendered labels, so they move with the window. */
1883
+ #repaintSearchHighlights() {
1884
+ if (this.search.isOpen) this.search.repaintHighlights();
1302
1885
  }
1303
1886
  connectedCallback() {
1304
1887
  super.connectedCallback();
@@ -1330,8 +1913,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1330
1913
  return target.closest(`ul.tree-list li[data-key]`)?.getAttribute(`data-key`) ?? void 0;
1331
1914
  },
1332
1915
  prepareSelection: (originKey) => {
1333
- const allNodes = this.#getAllNodes();
1334
- const originEntry = allNodes.find((n) => n.node.item.key === originKey);
1916
+ const flat = this.#getFlat();
1917
+ const allNodes = flat.rows;
1918
+ const originIdx = flat.indexByKey.get(originKey);
1919
+ const originEntry = originIdx === void 0 ? void 0 : allNodes[originIdx];
1335
1920
  if (!originEntry) return void 0;
1336
1921
  const selectedNodes = new Set(this._selectedNodes);
1337
1922
  if (!selectedNodes.has(originEntry.node)) selectedNodes.clear();
@@ -1420,6 +2005,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1420
2005
  getDndMode: () => this.itemDnd,
1421
2006
  getExpandHoverMs: () => this.dndExpandHoverMs,
1422
2007
  getVisibleRows: () => this.#getVisibleRows(),
2008
+ getRowInfo: (key) => {
2009
+ const flat = this.#getFlat();
2010
+ const idx = flat.indexByKey.get(key);
2011
+ if (idx === void 0) return void 0;
2012
+ const { node, depth, parentKey } = flat.rows[idx];
2013
+ return {
2014
+ node,
2015
+ depth,
2016
+ parentKey
2017
+ };
2018
+ },
2019
+ getChildren: (parentKey) => this.#childrenOfKey(parentKey),
1423
2020
  getIndentPx: () => this.#getIndentPx(),
1424
2021
  getScrollContainer: () => this.#container(),
1425
2022
  isSelfOrDescendant: (ancestorKey, key) => this.#isSelfOrDescendant(ancestorKey, key),
@@ -1492,44 +2089,121 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1492
2089
  const parsed = Number.parseInt(getComputedStyle(this).getPropertyValue(`--tree-item-height`), 10);
1493
2090
  return Number.isNaN(parsed) ? 30 : parsed;
1494
2091
  }
1495
- /** Measured rows, cached for the duration of a drag and invalidated on scroll / update. */
1496
- #getVisibleRows() {
1497
- if (this.#rowsCache) return this.#rowsCache;
1498
- const rows = [];
1499
- for (const { node, depth } of this.#getAllNodes()) {
1500
- const li = this.shadowRoot?.querySelector(`ul.tree-list li[data-key="${CSS.escape(node.item.key)}"]`);
1501
- if (li) rows.push({
2092
+ /** Children of `parentKey` (the root's children when undefined), straight from the tree. */
2093
+ #childrenOfKey(parentKey) {
2094
+ if (parentKey === void 0) return this.root?.children ?? [];
2095
+ const flat = this.#getFlat();
2096
+ const idx = flat.indexByKey.get(parentKey);
2097
+ return idx === void 0 ? [] : flat.rows[idx].node.children ?? [];
2098
+ }
2099
+ /**
2100
+ * Windowed counterpart of the measured rows: rects come from the layout
2101
+ * arithmetic and one rendered row's horizontal extent, so nothing is looked
2102
+ * up per row and rows outside the window still resolve.
2103
+ */
2104
+ #collectVirtualDropRows(container, list, out) {
2105
+ const flat = this.#getFlat();
2106
+ const params = this.#layoutParams(flat.rows.length);
2107
+ const range = rowRangeOverlapping(treeRowReader(params), container.scrollTop, container.scrollTop + container.clientHeight, 1);
2108
+ if (!range) return;
2109
+ const containerRect = container.getBoundingClientRect();
2110
+ const sample = list.querySelector(`li.item`)?.getBoundingClientRect() ?? containerRect;
2111
+ for (let i = range.first; i <= range.last; i++) {
2112
+ const { node, depth } = flat.rows[i];
2113
+ const top = containerRect.top - container.scrollTop + treeRowTop(params, i);
2114
+ const rect = {
2115
+ x: sample.left,
2116
+ y: top,
2117
+ left: sample.left,
2118
+ right: sample.left + sample.width,
2119
+ top,
2120
+ bottom: top + params.rowHeight,
2121
+ width: sample.width,
2122
+ height: params.rowHeight,
2123
+ toJSON: () => ({})
2124
+ };
2125
+ out.push({
1502
2126
  key: node.item.key,
1503
2127
  node,
1504
2128
  depth,
1505
- rect: li.getBoundingClientRect()
2129
+ index: i,
2130
+ rect
1506
2131
  });
1507
2132
  }
2133
+ }
2134
+ /**
2135
+ * Measured rows for the part of the list that intersects the scroll
2136
+ * container (plus one row of margin each side), cached for the duration of a
2137
+ * drag and invalidated on scroll / update. Each row carries its flat index;
2138
+ * structural lookups (parents, siblings) come from `getRowInfo` / `getChildren`.
2139
+ */
2140
+ #getVisibleRows() {
2141
+ if (this.#rowsCache) return this.#rowsCache;
2142
+ const rows = [];
2143
+ const container = this.#container();
2144
+ const list = this.renderRoot.querySelector(`ul.tree-list`);
2145
+ const keys = this.#renderedRowKeys;
2146
+ if (container && list && this.#virtualActive()) this.#collectVirtualDropRows(container, list, rows);
2147
+ else if (container && list && list.children.length === keys.length) {
2148
+ const flat = this.#getFlat();
2149
+ const items = list.children;
2150
+ const range = rowRangeOverlapping(this.#rowReader(items), container.scrollTop, container.scrollTop + container.clientHeight, 1);
2151
+ if (range) for (let i = range.first; i <= range.last; i++) {
2152
+ const li = items[i];
2153
+ if (li.classList.contains(`leaving`)) continue;
2154
+ const flatIdx = flat.indexByKey.get(keys[i]);
2155
+ if (flatIdx === void 0) continue;
2156
+ const { node, depth } = flat.rows[flatIdx];
2157
+ rows.push({
2158
+ key: node.item.key,
2159
+ node,
2160
+ depth,
2161
+ index: flatIdx,
2162
+ rect: li.getBoundingClientRect()
2163
+ });
2164
+ }
2165
+ }
1508
2166
  this.#rowsCache = rows;
1509
2167
  return rows;
1510
2168
  }
1511
2169
  /** Route spring-load through the real expand path so lazy children load and `expand` fires with the right depth. */
1512
2170
  #springLoadExpand(key) {
1513
2171
  if (this._expandedKeys.has(key)) return;
1514
- const entry = this.#getAllNodes().find((n) => n.node.item.key === key);
2172
+ const flat = this.#getFlat();
2173
+ const idx = flat.indexByKey.get(key);
2174
+ const entry = idx === void 0 ? void 0 : flat.rows[idx];
1515
2175
  if (!entry || entry.node.item.isLeaf === true) return;
1516
2176
  this.#toggleExpand(entry.node, entry.depth);
1517
2177
  this.#rowsCache = void 0;
1518
2178
  this.requestUpdate();
1519
2179
  }
2180
+ /** O(depth): walk up the whole-tree parent map. */
1520
2181
  #isSelfOrDescendant(ancestorKey, key) {
1521
2182
  if (ancestorKey === key) return true;
1522
- let node = this.#getAllNodes().find((n) => n.node.item.key === key)?.node;
1523
- let parent = node ? this.#findParentByKey(node.item.key) : void 0;
1524
- while (parent) {
1525
- if (parent.item.key === ancestorKey) return true;
1526
- node = parent;
1527
- parent = this.#findParentByKey(node.item.key);
2183
+ const { parentByKey } = this.#getTreeIndex();
2184
+ let cursor = parentByKey.get(key);
2185
+ while (cursor !== void 0) {
2186
+ if (cursor === ancestorKey) return true;
2187
+ cursor = parentByKey.get(cursor);
1528
2188
  }
1529
2189
  return false;
1530
2190
  }
1531
2191
  #findParentByKey(key) {
1532
- for (const { node } of this.#getAllNodes()) if (node.children?.some((c) => c.item.key === key)) return node;
2192
+ const parentKey = this.#getTreeIndex().parentByKey.get(key);
2193
+ if (parentKey === void 0 || parentKey === this.root?.item.key) return void 0;
2194
+ return this.#nodeByKey(parentKey);
2195
+ }
2196
+ /** Node for a key anywhere in the loaded tree (via the flat list when visible). */
2197
+ #nodeByKey(key) {
2198
+ const flat = this.#getFlat();
2199
+ const idx = flat.indexByKey.get(key);
2200
+ if (idx !== void 0) return flat.rows[idx].node;
2201
+ const { parentByKey } = this.#getTreeIndex();
2202
+ const path = [];
2203
+ for (let cursor = key; cursor !== void 0 && cursor !== this.root?.item.key; cursor = parentByKey.get(cursor)) path.unshift(cursor);
2204
+ let node = this.root;
2205
+ for (const k of path) node = node?.children?.find((c) => c.item.key === k);
2206
+ return node;
1533
2207
  }
1534
2208
  /**
1535
2209
  * Decide whether a DataTransfer is acceptable and resolve the dragged set.
@@ -1553,7 +2227,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1553
2227
  return {
1554
2228
  source: `internal`,
1555
2229
  keys,
1556
- nodes: keys.map((k) => this.#getAllNodes().find((n) => n.node.item.key === k)?.node).filter((n) => n !== void 0)
2230
+ nodes: keys.map((k) => this.#nodeByKey(k)).filter((n) => n !== void 0)
1557
2231
  };
1558
2232
  }
1559
2233
  if (phase === `drop`) try {
@@ -1597,7 +2271,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1597
2271
  }
1598
2272
  #anyRowDraggable() {
1599
2273
  if (this.itemDnd === `none`) return false;
1600
- const allNodes = this.#getAllNodes();
2274
+ const allNodes = this.#getFlat().rows;
1601
2275
  if (allNodes.length === 0) return false;
1602
2276
  if (!this.canDrag) return true;
1603
2277
  return allNodes.some(({ node }) => this.canDrag({
@@ -1619,6 +2293,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1619
2293
  this.#postDragSuppressTimer = void 0;
1620
2294
  }
1621
2295
  this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
2296
+ this.#teardownVirtualObservers();
1622
2297
  if (this.#stickyScrollBound) {
1623
2298
  this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
1624
2299
  this.#stickyScrollContainer = void 0;
@@ -1672,12 +2347,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1672
2347
  }
1673
2348
  selectAll() {
1674
2349
  const prev = new Set(this._selectedNodes);
1675
- if (this.interactionMode === `checked`) for (const { node } of this.#getAllNodes()) {
2350
+ if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
1676
2351
  if (!this.#canSelect(node)) continue;
1677
2352
  this.#applyCheckedToggle(node, true);
1678
2353
  }
1679
2354
  else {
1680
- for (const { node } of this.#getAllNodes()) {
2355
+ for (const { node } of this.#getFlat().rows) {
1681
2356
  if (!this.#canSelect(node)) continue;
1682
2357
  this._selectedNodes.add(node);
1683
2358
  }
@@ -1688,7 +2363,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1688
2363
  }
1689
2364
  deselectAll() {
1690
2365
  const prev = new Set(this._selectedNodes);
1691
- if (this.interactionMode === `checked`) for (const { node } of this.#getAllNodes()) {
2366
+ if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
1692
2367
  if (!this.#canSelect(node)) continue;
1693
2368
  this.#applyCheckedToggle(node, false);
1694
2369
  }
@@ -1709,7 +2384,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1709
2384
  const found = children.find((n) => n.item.key === key);
1710
2385
  if (!found) return false;
1711
2386
  this.#applyExclusivity(found.item.key);
1712
- this._expandedKeys.add(found.item.key);
2387
+ this.#setExpanded(withKey(this._expandedKeys, found.item.key));
1713
2388
  if (key === keys.at(-1)) {
1714
2389
  this.select(found);
1715
2390
  this.tickled.setTickled(found.item.key);
@@ -1734,7 +2409,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1734
2409
  if (!path) return false;
1735
2410
  for (let i = 0; i < path.length - 1; i++) {
1736
2411
  this.#applyExclusivity(path[i]);
1737
- this._expandedKeys.add(path[i]);
2412
+ this.#setExpanded(withKey(this._expandedKeys, path[i]));
1738
2413
  }
1739
2414
  this.tickled.setTickled(key);
1740
2415
  this.#stickyDirty = true;
@@ -1770,10 +2445,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1770
2445
  return true;
1771
2446
  }
1772
2447
  #clearSelectionInternal() {
2448
+ this._selectionVersion++;
1773
2449
  this._selectedNodes.clear();
1774
2450
  this.selectedNode = void 0;
1775
2451
  }
1776
2452
  #dispatchSelect(previous) {
2453
+ this._selectionVersion++;
1777
2454
  const detail = {
1778
2455
  selected: new Set(this._selectedNodes),
1779
2456
  previous
@@ -1879,9 +2556,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1879
2556
  return;
1880
2557
  }
1881
2558
  if (isShift && this.#anchor) {
1882
- const visible = this.#getAllNodes();
1883
- const anchorIdx = visible.findIndex((n) => n.node === this.#anchor.node);
1884
- const targetIdx = visible.findIndex((n) => n.node === node);
2559
+ const flat = this.#getFlat();
2560
+ const visible = flat.rows;
2561
+ const anchorIdx = flat.indexByKey.get(this.#anchor.node.item.key) ?? -1;
2562
+ const targetIdx = flat.indexByKey.get(node.item.key) ?? -1;
1885
2563
  if (anchorIdx >= 0 && targetIdx >= 0) {
1886
2564
  const prev = new Set(this._selectedNodes);
1887
2565
  const from = Math.min(anchorIdx, targetIdx);
@@ -1931,9 +2609,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1931
2609
  return;
1932
2610
  }
1933
2611
  if (isShift && this.#anchor) {
1934
- const visible = this.#getAllNodes();
1935
- const anchorIdx = visible.findIndex((n) => n.node === this.#anchor.node);
1936
- const targetIdx = visible.findIndex((n) => n.node === node);
2612
+ const flat = this.#getFlat();
2613
+ const visible = flat.rows;
2614
+ const anchorIdx = flat.indexByKey.get(this.#anchor.node.item.key) ?? -1;
2615
+ const targetIdx = flat.indexByKey.get(node.item.key) ?? -1;
1937
2616
  if (anchorIdx >= 0 && targetIdx >= 0) {
1938
2617
  const prev = new Set(this._selectedNodes);
1939
2618
  const from = Math.min(anchorIdx, targetIdx);
@@ -1976,7 +2655,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1976
2655
  const isExpanded = this._expandedKeys.has(node.item.key);
1977
2656
  this.#stickyDirty = true;
1978
2657
  if (isExpanded) {
1979
- this._expandedKeys.delete(node.item.key);
2658
+ this.#setExpanded(withoutKey(this._expandedKeys, node.item.key));
1980
2659
  this.dispatchEvent(new CustomEvent(`collapse`, {
1981
2660
  detail: {
1982
2661
  node,
@@ -1995,7 +2674,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1995
2674
  }
1996
2675
  } else {
1997
2676
  this.#applyExclusivity(node.item.key);
1998
- this._expandedKeys.add(node.item.key);
2677
+ this.#setExpanded(withKey(this._expandedKeys, node.item.key));
1999
2678
  this.dispatchEvent(new CustomEvent(`expand`, {
2000
2679
  detail: {
2001
2680
  node,
@@ -2019,7 +2698,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2019
2698
  if (node.children) for (const child of node.children) this.#applyCheckedToggle(child, selected);
2020
2699
  }
2021
2700
  #getFlatIndexOf(node) {
2022
- return this.#getAllNodes().findIndex((n) => n.node === node);
2701
+ return this.#getFlat().indexByKey.get(node.item.key) ?? -1;
2023
2702
  }
2024
2703
  #extendRangeSelection(nodes, fromIdx, toIdx) {
2025
2704
  if (!this.#anchor) {
@@ -2030,7 +2709,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2030
2709
  };
2031
2710
  }
2032
2711
  if (!this.#anchor) return;
2033
- const anchorIdx = nodes.findIndex((n) => n.node === this.#anchor.node);
2712
+ const anchorIdx = this.#getFlat().indexByKey.get(this.#anchor.node.item.key) ?? -1;
2034
2713
  if (anchorIdx < 0) return;
2035
2714
  const from = Math.min(anchorIdx, toIdx);
2036
2715
  const to = Math.max(anchorIdx, toIdx);
@@ -2066,8 +2745,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2066
2745
  }, controller.signal), controller.signal)];
2067
2746
  if (controller.signal.aborted) return;
2068
2747
  node.children = children;
2748
+ this.#structureVersion++;
2069
2749
  this.#applyExclusivity(node.item.key);
2070
- this._expandedKeys.add(node.item.key);
2750
+ this.#setExpanded(withKey(this._expandedKeys, node.item.key));
2751
+ this.#flat = void 0;
2071
2752
  this.#stickyDirty = true;
2072
2753
  this.#pendingNode = void 0;
2073
2754
  this.#pendingController = void 0;
@@ -2083,6 +2764,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2083
2764
  } catch {
2084
2765
  if (controller.signal.aborted) return;
2085
2766
  node.children = [];
2767
+ this.#structureVersion++;
2768
+ this.#flat = void 0;
2086
2769
  this.#pendingNode = void 0;
2087
2770
  this.#pendingController = void 0;
2088
2771
  this.requestUpdate();
@@ -2094,33 +2777,44 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2094
2777
  _isFocused(node) {
2095
2778
  return this.tickled.isTickled(node.item.key);
2096
2779
  }
2097
- _isExpanded(node, depth = 0) {
2780
+ _isExpanded(node, _depth = 0) {
2098
2781
  if (this._expandedKeys.has(node.item.key)) return true;
2099
- if (this.filterPredicate && node.children) return node.children.some((child) => this.#matchesFilter(child, depth + 1));
2100
- return false;
2101
- }
2102
- #matchesFilter(node, depth) {
2103
- if (!this.filterPredicate) return true;
2104
- if (this.filterPredicate(node, depth)) return true;
2105
- return node.children?.some((child) => this.#matchesFilter(child, depth + 1)) ?? false;
2782
+ return this.#getFilter()?.autoExpanded.has(node.item.key) ?? false;
2106
2783
  }
2107
2784
  get _pendingNode() {
2108
2785
  return this.#pendingNode;
2109
2786
  }
2110
2787
  #scrollKeyIntoView(key) {
2788
+ if (this.#virtualActive()) {
2789
+ this.updateComplete.then(() => {
2790
+ const idx = this.#getFlat().indexByKey.get(key);
2791
+ if (idx !== void 0) this.#revealIndex(idx);
2792
+ });
2793
+ return;
2794
+ }
2795
+ const token = ++this.#revealToken;
2111
2796
  requestAnimationFrame(() => {
2112
- const container = this.#container();
2113
- const el = this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(key)}"]`);
2114
- if (!el || !container) return;
2115
- const elRect = el.getBoundingClientRect();
2116
- const containerRect = container.getBoundingClientRect();
2117
- const relativeTop = elRect.top - containerRect.top;
2118
- const relativeBottom = relativeTop + elRect.height;
2119
- const overlayHeight = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
2120
- if (relativeTop < overlayHeight) container.scrollTop += relativeTop - overlayHeight;
2121
- else if (relativeBottom > container.clientHeight) container.scrollTop += relativeBottom - container.clientHeight;
2797
+ this.#revealRowElement(key);
2798
+ const running = this.#container()?.getAnimations?.({ subtree: true }) ?? [];
2799
+ if (running.length > 0) Promise.allSettled(running.map((a) => a.finished)).then(() => {
2800
+ if (token === this.#revealToken) this.#revealRowElement(key);
2801
+ });
2122
2802
  });
2123
2803
  }
2804
+ #revealToken;
2805
+ /** Scroll the rendered row for `key` into the container, below the sticky overlay. No-op if it isn't rendered or is already visible. */
2806
+ #revealRowElement(key) {
2807
+ const container = this.#container();
2808
+ const el = this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(key)}"]`);
2809
+ if (!el || !container) return;
2810
+ const elRect = el.getBoundingClientRect();
2811
+ const containerRect = container.getBoundingClientRect();
2812
+ const relativeTop = elRect.top - containerRect.top;
2813
+ const relativeBottom = relativeTop + elRect.height;
2814
+ const overlayHeight = this.renderRoot.querySelector(`.sticky-headers`)?.getBoundingClientRect().height ?? 0;
2815
+ if (relativeTop < overlayHeight) container.scrollTop += relativeTop - overlayHeight;
2816
+ else if (relativeBottom > container.clientHeight) container.scrollTop += relativeBottom - container.clientHeight;
2817
+ }
2124
2818
  #abortPending() {
2125
2819
  if (this.#pendingController) {
2126
2820
  this.#pendingController.abort();
@@ -2134,14 +2828,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2134
2828
  if (this.exclusivity === `depth`) this.#collapseSiblingsAtDepth(expandKey);
2135
2829
  else if (this.exclusivity === `global`) {
2136
2830
  const ancestorKeys = this.#findAncestorKeys(expandKey);
2137
- for (const key of [...this._expandedKeys]) if (!ancestorKeys.has(key) && key !== expandKey) this._expandedKeys.delete(key);
2831
+ this.#setExpanded(withoutKeys(this._expandedKeys, (key) => !ancestorKeys.has(key) && key !== expandKey));
2138
2832
  }
2139
2833
  }
2140
2834
  #collapseSiblingsAtDepth(expandKey) {
2141
2835
  if (!this.root) return;
2142
2836
  const parent = this.#findParent(this.root, expandKey);
2143
2837
  if (!parent?.children) return;
2144
- for (const child of parent.children) if (child.item.key !== expandKey && this._expandedKeys.has(child.item.key)) this._expandedKeys.delete(child.item.key);
2838
+ const siblingKeys = new Set(parent.children.map((c) => c.item.key));
2839
+ this.#setExpanded(withoutKeys(this._expandedKeys, (key) => key !== expandKey && siblingKeys.has(key)));
2145
2840
  }
2146
2841
  #findAncestorKeys(targetKey) {
2147
2842
  const keys = /* @__PURE__ */ new Set();
@@ -2165,30 +2860,13 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2165
2860
  if (found) return found;
2166
2861
  }
2167
2862
  }
2168
- #getVisibleNodes(node, depth) {
2169
- const result = [];
2170
- const visit = (n, d) => {
2171
- result.push({
2172
- node: n,
2173
- depth: d
2174
- });
2175
- if (n.children && this._isExpanded(n, d)) for (const child of n.children) visit(child, d + 1);
2176
- };
2177
- if (node.children) for (const child of node.children) visit(child, depth + 1);
2178
- return result;
2179
- }
2180
- #getAllNodes() {
2181
- if (!this.root) return [];
2182
- return this.#getVisibleNodes(this.root, -1);
2183
- }
2184
2863
  #getFocusedIndex() {
2185
- const nodes = this.#getAllNodes();
2186
2864
  const tickledKey = this.tickled.tickledId;
2187
2865
  if (!tickledKey) return 0;
2188
- return nodes.findIndex((n) => n.node.item.key === tickledKey);
2866
+ return this.#getFlat().indexByKey.get(tickledKey) ?? 0;
2189
2867
  }
2190
2868
  #setFocusedIndex(index) {
2191
- const nodes = this.#getAllNodes();
2869
+ const nodes = this.#getFlat().rows;
2192
2870
  if (index < 0 || index >= nodes.length) return;
2193
2871
  const { node, depth } = nodes[index];
2194
2872
  this.tickled.setTickled(node.item.key);
@@ -2202,6 +2880,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2202
2880
  composed: true
2203
2881
  }));
2204
2882
  this.requestUpdate();
2883
+ if (this.#virtualActive()) {
2884
+ this.#revealIndex(index);
2885
+ return;
2886
+ }
2205
2887
  this.updateComplete.then(() => this.#scrollTickledIntoView());
2206
2888
  }
2207
2889
  #scrollTickledIntoView() {
@@ -2212,12 +2894,25 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2212
2894
  behavior: `smooth`
2213
2895
  });
2214
2896
  }
2897
+ /** Rows per PageUp / PageDown: one viewport, from the current CSS metrics. */
2898
+ #pageSize() {
2899
+ this.#measureMetrics();
2900
+ return treePageSize(this.#layoutParams(this.#getFlat().rows.length));
2901
+ }
2902
+ /** Move the cursor to `nextIdx`, applying the interaction mode's selection side effects. */
2903
+ #navigateToIndex(nodes, currentIdx, nextIdx, shiftKey) {
2904
+ if (this.interactionMode === `implicit`) this.select(nodes[nextIdx].node);
2905
+ this.#setFocusedIndex(nextIdx);
2906
+ if (shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`)) this.#extendRangeSelection(nodes, currentIdx, nextIdx);
2907
+ else if (this.interactionMode === `vscode`) this.#anchor = void 0;
2908
+ this.requestUpdate();
2909
+ }
2215
2910
  #onKeyDown;
2216
2911
  renderLoadingIndicator() {
2217
2912
  return html`<span class="loading-spinner"></span>`;
2218
2913
  }
2219
2914
  render() {
2220
- const nodes = this.#getAllNodes();
2915
+ const nodes = this.#getFlat().rows;
2221
2916
  return html`
2222
2917
  <div
2223
2918
  id="container"
@@ -2240,12 +2935,51 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2240
2935
  ${this.renderLoadingIndicator()}
2241
2936
  </div>
2242
2937
  `;
2938
+ if (this.#virtualActive()) return this.#renderWindow(nodes);
2939
+ const woven = weaveGhosts(nodes, this.#renderedRowKeys, this.#leaving);
2940
+ this.#renderedRowKeys = woven.map((e) => e.node.item.key);
2941
+ this.#renderedRowIndex = new Map(this.#renderedRowKeys.map((key, i) => [key, i]));
2243
2942
  return html`
2244
2943
  <ul class="tree-list" role="tree">
2245
- ${repeat(this.#weaveLeaving(nodes), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
2944
+ ${repeat(woven, ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
2945
+ </ul>
2946
+ `;
2947
+ }
2948
+ /**
2949
+ * Windowed render: the rows near the viewport between two spacers that hold
2950
+ * the rest of the scroll height. Keyed, so a row that stays in the window
2951
+ * keeps its element while others enter and leave at the edges.
2952
+ */
2953
+ #renderWindow(nodes) {
2954
+ const params = this.#layoutParams(nodes.length);
2955
+ const { start, end } = this.#currentWindow(nodes.length);
2956
+ this.#winStart = start;
2957
+ this.#winEnd = end;
2958
+ this.#renderedRowKeys = [];
2959
+ this.#renderedRowIndex = /* @__PURE__ */ new Map();
2960
+ const stride = treeStride(params);
2961
+ return html`
2962
+ <ul class="tree-list virtual" role="tree" aria-rowcount=${nodes.length}>
2963
+ <li class="virtual-spacer" role="presentation" aria-hidden="true" style=${`height: ${start * stride}px`}></li>
2964
+ ${repeat(nodes.slice(start, end), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
2965
+ <li class="virtual-spacer" role="presentation" aria-hidden="true" style=${`height: ${(nodes.length - end) * stride}px`}></li>
2246
2966
  </ul>
2247
2967
  `;
2248
2968
  }
2969
+ /**
2970
+ * `{ posinset, setsize }` for `aria-posinset` / `aria-setsize`. A windowed
2971
+ * list has most siblings out of the DOM, so assistive tech cannot count them.
2972
+ */
2973
+ _ariaSetInfo(node) {
2974
+ const flat = this.#getFlat();
2975
+ const idx = flat.indexByKey.get(node.item.key);
2976
+ if (idx === void 0) return void 0;
2977
+ const { posInSet, setSize } = flat.rows[idx];
2978
+ return {
2979
+ posInSet,
2980
+ setSize
2981
+ };
2982
+ }
2249
2983
  /** Shared drag-and-drop render state for a row, used by every `_renderNode` variant. */
2250
2984
  _dndRowState(node) {
2251
2985
  const draggable = this.itemDnd !== `none` && (this.root?.children?.length ?? 0) > 0 && (this.itemDnd === `internal` || this.itemDnd === `both` || this.itemDnd === `external`) && (this.canDrag?.({
@@ -2269,17 +3003,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2269
3003
  const isBranch = node.item.isLeaf !== true;
2270
3004
  const isExpanded = this._isExpanded(node, depth);
2271
3005
  const isPending = this.#pendingNode === node;
2272
- const isFiltered = this._isFilteredOut(node, depth);
2273
3006
  const isLeaving = this._isLeaving(node);
2274
3007
  const dnd = this._dndRowState(node);
2275
3008
  return html`
2276
3009
  <li
2277
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
3010
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isLeaving ? `leaving` : ``} ${dnd.dragging ? `dragging` : ``} ${dnd.dropTarget && dnd.dropPosition === `inside` ? `drop-into` : ``}"
2278
3011
  role="treeitem"
2279
3012
  aria-selected=${selected}
2280
3013
  aria-expanded=${isBranch ? isExpanded : nothing}
2281
3014
  aria-hidden=${isLeaving ? `true` : nothing}
2282
3015
  aria-level=${depth + 1}
3016
+ aria-setsize=${this._ariaSetInfo(node)?.setSize ?? nothing}
3017
+ aria-posinset=${this._ariaSetInfo(node)?.posInSet ?? nothing}
2283
3018
  data-key=${node.item.key}
2284
3019
  style=${`--depth: ${depth}`}
2285
3020
  draggable=${dnd.draggable ? `true` : nothing}
@@ -2320,6 +3055,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2320
3055
  --tree-muted-text: var(--surface-muted-text);
2321
3056
  --tree-indent: 1ch;
2322
3057
  --tree-item-height: 30px;
3058
+ /* Margin above each row. A windowed list is positioned by (height + gap), so keep it in px. */
3059
+ --tree-item-gap: 1px;
2323
3060
  --tree-caret-size: 30px;
2324
3061
  --tree-radius: var(--radius-s);
2325
3062
  --tree-drop-line: var(--accent);
@@ -2343,6 +3080,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2343
3080
  The scroll lives here, not on :host, so the search popover (a sibling of
2344
3081
  #container, positioned against :host) stays put while the list scrolls. */
2345
3082
  #container {
3083
+ /* offsetParent for rows: row offsets are scroll-independent (sticky / DnD geometry). */
3084
+ position: relative;
2346
3085
  outline: none;
2347
3086
  min-height: 100%;
2348
3087
  max-height: 100%;
@@ -2389,7 +3128,40 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2389
3128
  font-size: var(--text-m);
2390
3129
  border-radius: var(--tree-radius);
2391
3130
  transition: background var(--transition, 0.15s ease-out);
2392
- margin: 1px 0;
3131
+ margin: var(--tree-item-gap) 0;
3132
+ }
3133
+
3134
+ /* Windowed list: rows are placed purely by stride arithmetic — a margin above
3135
+ each row only (no collapsing), spacers hold the offscreen height. */
3136
+ .tree-list.virtual .item {
3137
+ margin: var(--tree-item-gap) 0 0;
3138
+ }
3139
+
3140
+ .virtual-spacer {
3141
+ display: block;
3142
+ margin: 0;
3143
+ padding: 0;
3144
+ list-style: none;
3145
+ pointer-events: none;
3146
+ }
3147
+
3148
+ .sticky-anchor {
3149
+ display: contents;
3150
+ }
3151
+
3152
+ :host([data-virtual]) .sticky-anchor {
3153
+ position: sticky;
3154
+ top: 0;
3155
+ z-index: 5;
3156
+ display: block;
3157
+ height: 0;
3158
+ }
3159
+
3160
+ :host([data-virtual]) .sticky-headers {
3161
+ position: absolute;
3162
+ top: 0;
3163
+ left: 0;
3164
+ right: 0;
2393
3165
  }
2394
3166
 
2395
3167
  .tree-caret {
@@ -2444,10 +3216,6 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2444
3216
  to { transform: rotate(360deg); }
2445
3217
  }
2446
3218
 
2447
- .item.filtered {
2448
- display: none;
2449
- }
2450
-
2451
3219
  /* Fading-out ghost row (see #runItemAnimations) — inert while it collapses. */
2452
3220
  .item.leaving {
2453
3221
  pointer-events: none;
@@ -2558,6 +3326,14 @@ __decorate([property({
2558
3326
  toAttribute: (value) => value ? null : `false`
2559
3327
  }
2560
3328
  })], TreeBaseElement.prototype, "stickyHeaders", void 0);
3329
+ __decorate([property({
3330
+ reflect: true,
3331
+ attribute: `virtualize`
3332
+ })], TreeBaseElement.prototype, "virtualize", void 0);
3333
+ __decorate([property({
3334
+ type: Number,
3335
+ attribute: `virtualize-threshold`
3336
+ })], TreeBaseElement.prototype, "virtualizeThreshold", void 0);
2561
3337
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewRenderer", void 0);
2562
3338
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewSource", void 0);
2563
3339
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrag", void 0);
@@ -2565,33 +3341,33 @@ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrop
2565
3341
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "parseExternalDrop", void 0);
2566
3342
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "applyInternalDrop", void 0);
2567
3343
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
3344
+ __decorate([state()], TreeBaseElement.prototype, "_scrollTop", void 0);
2568
3345
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
2569
3346
  __decorate([state()], TreeBaseElement.prototype, "_stickyKeys", void 0);
2570
3347
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
2571
3348
  //#endregion
2572
3349
  //#region src/tree/tree-list.ts
2573
- function computeCheckboxState(node, selectedNodes) {
2574
- if (node.item.isLeaf || !node.children?.length) return selectedNodes.has(node) ? `checked` : `unchecked`;
2575
- const childStates = node.children.map((c) => computeCheckboxState(c, selectedNodes));
2576
- const allChecked = childStates.every((s) => s === `checked`);
2577
- const allUnchecked = childStates.every((s) => s === `unchecked`);
2578
- if (allChecked) return `checked`;
2579
- if (allUnchecked) return `unchecked`;
2580
- return `indeterminate`;
2581
- }
2582
3350
  let TreeListElement = class TreeListElement extends TreeBaseElement {
2583
- #depthsWithIcons = /* @__PURE__ */ new Set();
2584
- render() {
2585
- this.#detectIcons();
2586
- return super.render();
3351
+ /** Checkbox tri-states for the whole loaded tree, rebuilt only when the structure or selection changes. */
3352
+ #checkboxCache;
3353
+ willUpdate(changedProps) {
3354
+ super.willUpdate(changedProps);
3355
+ this.#syncCheckboxStates();
2587
3356
  }
2588
- #detectIcons() {
2589
- this.#depthsWithIcons.clear();
2590
- const visit = (node, depth) => {
2591
- if (node.item.icon) this.#depthsWithIcons.add(depth);
2592
- if (node.children) for (const child of node.children) visit(child, depth + 1);
3357
+ #syncCheckboxStates() {
3358
+ const root = this.root;
3359
+ if (this.interactionMode !== `checked` || !root) {
3360
+ this.#checkboxCache = void 0;
3361
+ return;
3362
+ }
3363
+ const c = this.#checkboxCache;
3364
+ if (c && c.root === root && c.structureVersion === this._structureVersion && c.selectionVersion === this._selectionVersion) return;
3365
+ this.#checkboxCache = {
3366
+ root,
3367
+ structureVersion: this._structureVersion,
3368
+ selectionVersion: this._selectionVersion,
3369
+ states: computeCheckboxStates(root, this._selectedNodes)
2593
3370
  };
2594
- if (this.root) visit(this.root, -1);
2595
3371
  }
2596
3372
  renderNodeContent(node, depth) {
2597
3373
  const isBranch = node.item.isLeaf !== true;
@@ -2609,7 +3385,7 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
2609
3385
  </span>
2610
3386
  `;
2611
3387
  if (this.interactionMode === `checked`) {
2612
- const state = computeCheckboxState(node, this._selectedNodes);
3388
+ const state = this.#checkboxCache?.states.get(node.item.key) ?? `unchecked`;
2613
3389
  return html`${this.#renderCheckbox(node, depth, state)}${caretAndLabel}`;
2614
3390
  }
2615
3391
  return caretAndLabel;
@@ -2643,19 +3419,20 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
2643
3419
  const isBranch = node.item.isLeaf !== true;
2644
3420
  const isExpanded = this._isExpanded(node, depth);
2645
3421
  const isPending = this._pendingNode === node;
2646
- const hasIconAtDepth = this.#depthsWithIcons.has(depth);
3422
+ const hasIconAtDepth = this._depthsWithIcons.has(depth);
2647
3423
  const isLeaving = this._isLeaving(node);
2648
- const isFiltered = this._isFilteredOut(node, depth);
2649
3424
  const dnd = this._dndRowState(node);
2650
3425
  return html`
2651
3426
  <li
2652
- 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` : ``}"
3427
+ 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` : ``}"
2653
3428
  data-key=${node.item.key}
2654
3429
  role="treeitem"
2655
3430
  aria-selected=${selected}
2656
3431
  aria-expanded=${isBranch ? isExpanded : nothing}
2657
3432
  aria-hidden=${isLeaving ? `true` : nothing}
2658
3433
  aria-level=${depth + 1}
3434
+ aria-setsize=${this._ariaSetInfo(node)?.setSize ?? nothing}
3435
+ aria-posinset=${this._ariaSetInfo(node)?.posInSet ?? nothing}
2659
3436
  style=${`--depth: ${depth}`}
2660
3437
  draggable=${dnd.draggable ? `true` : nothing}
2661
3438
  part=${dnd.dropTarget ? `drop-target` : nothing}
@@ -2794,7 +3571,7 @@ var TreeController = class {
2794
3571
  const treeNode = this.#nodeToTree.get(node);
2795
3572
  if (!treeNode) return;
2796
3573
  node.children = this.#childrenOf(treeNode);
2797
- this.#element.requestUpdate();
3574
+ this.#element.invalidateStructure();
2798
3575
  }, { signal });
2799
3576
  this.#element.addEventListener(`collapse`, (_e) => {}, { signal });
2800
3577
  }
@@ -2816,7 +3593,10 @@ var TreeController = class {
2816
3593
  if (!node) return false;
2817
3594
  if (node.children === void 0) {
2818
3595
  const mapped = this.#nodeToTree.get(node);
2819
- if (mapped) node.children = this.#childrenOf(mapped);
3596
+ if (mapped) {
3597
+ node.children = this.#childrenOf(mapped);
3598
+ this.#element.invalidateStructure();
3599
+ }
2820
3600
  }
2821
3601
  currentColumn = node.children ?? [];
2822
3602
  }
@@ -2851,4 +3631,4 @@ var TreeController = class {
2851
3631
  //#endregion
2852
3632
  export { TreeDataModel as a, pruneDraggedDescendants as c, TreeDropController as i, TreeListElement as n, applyTreeDrop as o, TreeBaseElement as r, dropTargetFor as s, TreeController as t };
2853
3633
 
2854
- //# sourceMappingURL=tree-DGveys4K.js.map
3634
+ //# sourceMappingURL=tree-DD8OaqsK.js.map