@ixfx/components 0.15.5 → 0.16.1

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 (80) hide show
  1. package/bundle/index.d.ts +986 -49
  2. package/bundle/index.js +765 -729
  3. package/dist/ac-token.js +1 -1
  4. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  5. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  6. package/dist/crumbs.d.ts +2 -115
  7. package/dist/crumbs.js +1 -1179
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-Cqxuwgbb.js} +615 -210
  9. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-Csh5EJW9.d.ts} +55 -4
  10. package/dist/data-grid.d.ts +3 -3
  11. package/dist/data-grid.js +1 -1
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-Bo5ze3SN.js} +234 -9
  13. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-DSZ1DDor.d.ts} +48 -4
  14. package/dist/detail-list.d.ts +4 -4
  15. package/dist/detail-list.js +1 -1
  16. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-CT9kX3TQ.d.ts} +27 -4
  17. package/dist/flat-list.d.ts +2 -2
  18. package/dist/flat-list.js +123 -4
  19. package/dist/{grid-list-y6dWJYu3.js → grid-list-D0dJNVEF.js} +278 -76
  20. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-DDLrWRxm.d.ts} +128 -22
  21. package/dist/grid-list.d.ts +3 -3
  22. package/dist/grid-list.js +1 -1
  23. package/dist/{incr-search-JIhwB49y.js → incr-search-CgbL-OlS.js} +140 -10
  24. package/dist/incr-search.d.ts +1 -1
  25. package/dist/incr-search.js +1 -1
  26. package/dist/index-9ttcdwlL.d.ts +71 -0
  27. package/dist/index-Bnm-kKGC.d.ts +475 -0
  28. package/dist/{index-KN2MW_xD.d.ts → index-CUErGhCg.d.ts} +31 -4
  29. package/dist/{index-DEVyVIn8.d.ts → index-D-lZNlHa.d.ts} +52 -6
  30. package/dist/index.d.ts +341 -21
  31. package/dist/index.js +2286 -884
  32. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  33. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  34. package/dist/miller-DUqiUAWQ.js +2230 -0
  35. package/dist/miller.d.ts +4 -437
  36. package/dist/miller.js +1 -1815
  37. package/dist/narrowed-text.js +1 -1
  38. package/dist/pending-BGidHppL.js +91 -0
  39. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  40. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  41. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  42. package/dist/split-layout.d.ts +1 -1
  43. package/dist/split-layout.js +1 -1
  44. package/dist/sunburst-tree.d.ts +2 -2
  45. package/dist/sunburst-tree.js +1 -1
  46. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_M0YTEA.js} +299 -80
  47. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-DePuFYlU.d.ts} +40 -3
  48. package/dist/tree-CftHwtEb.js +146 -0
  49. package/dist/{tree-D8VxvR9u.js → tree-DAlgvaHB.js} +276 -48
  50. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  51. package/dist/tree.d.ts +3 -3
  52. package/dist/tree.js +1 -1
  53. package/dist/types-LU1u0CB8.d.ts +87 -0
  54. package/dist/vertical-list-DyKPKGxK.js +1993 -0
  55. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-SAx56U-l.d.ts} +31 -3
  56. package/dist/vertical-list.d.ts +3 -3
  57. package/dist/vertical-list.js +1 -1789
  58. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  59. package/docs-user/README.md +2 -0
  60. package/docs-user/crumbs.md +27 -0
  61. package/docs-user/data-grid.md +79 -5
  62. package/docs-user/detail-list.md +69 -0
  63. package/docs-user/dock.md +63 -12
  64. package/docs-user/flat-list.md +27 -0
  65. package/docs-user/grid-list.md +69 -13
  66. package/docs-user/hyperbolic-tree.md +31 -4
  67. package/docs-user/icicle-tree.md +36 -0
  68. package/docs-user/index.json +12 -2
  69. package/docs-user/llms.txt +2 -0
  70. package/docs-user/miller.md +52 -21
  71. package/docs-user/multicol-list.md +50 -8
  72. package/docs-user/split-layout.md +45 -17
  73. package/docs-user/sunburst-tree.md +32 -2
  74. package/docs-user/tree.md +45 -6
  75. package/docs-user/treemap.md +32 -0
  76. package/docs-user/twosplit-layout.md +22 -0
  77. package/docs-user/user-catalog.md +1 -1
  78. package/docs-user/vertical-list.md +55 -7
  79. package/llms.txt +2 -0
  80. package/package.json +2 -2
@@ -0,0 +1,146 @@
1
+ import { a as capKeys, i as capFlags, n as EMPTY_KEYS } from "./pending-BGidHppL.js";
2
+ //#region src/common/view-state/tree.ts
3
+ /**
4
+ * - live expanded keys that are no longer loaded → pending `true`
5
+ * - pending keys that are now loaded → applied (`true` opens, `false` closes)
6
+ * - pending-open keys whose node can't open yet (`canOpen` false: children
7
+ * unloaded) stay pending and are listed in `needsLoad`
8
+ */
9
+ function reconcileExpansion(live, pending, has, canOpen) {
10
+ let nextLive;
11
+ let nextPending;
12
+ const needsLoad = [];
13
+ for (const key of live) {
14
+ if (has(key)) continue;
15
+ nextLive ??= new Set(live);
16
+ nextLive.delete(key);
17
+ nextPending ??= new Map(pending);
18
+ nextPending.set(key, true);
19
+ }
20
+ for (const [key, open] of pending) {
21
+ if (!has(key)) continue;
22
+ if (open && !canOpen(key)) {
23
+ needsLoad.push(key);
24
+ continue;
25
+ }
26
+ nextPending ??= new Map(pending);
27
+ nextPending.delete(key);
28
+ if (open !== (nextLive ?? live).has(key)) {
29
+ nextLive ??= new Set(live);
30
+ if (open) nextLive.add(key);
31
+ else nextLive.delete(key);
32
+ }
33
+ }
34
+ return {
35
+ live: nextLive ?? live,
36
+ pending: nextPending ? capFlags(nextPending) : pending,
37
+ needsLoad,
38
+ changed: nextLive !== void 0 || nextPending !== void 0
39
+ };
40
+ }
41
+ /**
42
+ * Re-key the node selection onto the current tree: selected nodes are swapped
43
+ * for the loaded node with the same key, or go pending when the key is gone;
44
+ * pending keys that resolve (and pass `canSelect`) are selected. With
45
+ * `single`, at most one node stays selected (a restored key wins).
46
+ */
47
+ function reconcileSelection(selected, pending, nodeByKey, canSelect, single) {
48
+ if (selected.size === 0 && pending.size === 0) return {
49
+ selected,
50
+ pending,
51
+ restored: false,
52
+ changed: false
53
+ };
54
+ const next = /* @__PURE__ */ new Set();
55
+ const nextPending = /* @__PURE__ */ new Set();
56
+ let changed = false;
57
+ for (const node of selected) {
58
+ const current = nodeByKey(node.item.key);
59
+ if (current) {
60
+ next.add(current);
61
+ if (current !== node) changed = true;
62
+ } else {
63
+ nextPending.add(node.item.key);
64
+ changed = true;
65
+ }
66
+ }
67
+ let restored;
68
+ for (const key of pending) {
69
+ const node = nodeByKey(key);
70
+ if (node && canSelect(node)) {
71
+ next.add(node);
72
+ restored = node;
73
+ changed = true;
74
+ } else nextPending.add(key);
75
+ }
76
+ if (!changed) return {
77
+ selected,
78
+ pending,
79
+ restored: false,
80
+ changed: false
81
+ };
82
+ let finalSelected = next;
83
+ let finalPending = nextPending;
84
+ if (single && next.size > 0) {
85
+ finalSelected = /* @__PURE__ */ new Set([restored ?? [...next].at(-1)]);
86
+ finalPending = EMPTY_KEYS;
87
+ }
88
+ return {
89
+ selected: finalSelected,
90
+ pending: capKeys(finalPending),
91
+ restored: restored !== void 0,
92
+ changed: true
93
+ };
94
+ }
95
+ /**
96
+ * Keyboard cursor: a cursor key that left the data goes pending (`clear`);
97
+ * a pending key that is now visible is applied (`apply`).
98
+ */
99
+ function reconcileCursor(cursor, pending, has, isVisible) {
100
+ let nextPending = pending;
101
+ let clear = false;
102
+ if (cursor !== void 0 && !has(cursor)) {
103
+ nextPending ??= cursor;
104
+ clear = true;
105
+ }
106
+ if (nextPending !== void 0 && isVisible(nextPending)) return {
107
+ clear,
108
+ apply: nextPending,
109
+ pending: void 0
110
+ };
111
+ return {
112
+ clear,
113
+ apply: void 0,
114
+ pending: nextPending
115
+ };
116
+ }
117
+ /** Sanitise a JSON zoom / centre path: strings only, `undefined` when not an array. */
118
+ function pathFromJson(value) {
119
+ return Array.isArray(value) ? value.filter((k) => typeof k === `string`) : void 0;
120
+ }
121
+ /**
122
+ * Follow `keys` from the children of `root`. When the walk reaches a node
123
+ * whose children aren't loaded, reports it as `load` if `canLoad` allows.
124
+ */
125
+ function resolveKeyPath(root, keys, canLoad) {
126
+ if (!root) return { status: `missing` };
127
+ const nodes = [];
128
+ let current = root;
129
+ for (const key of keys) {
130
+ if (current.children === void 0) return canLoad(current) ? {
131
+ status: `load`,
132
+ node: current,
133
+ depth: nodes.length - 1
134
+ } : { status: `missing` };
135
+ const next = current.children.find((c) => c.item.key === key);
136
+ if (!next) return { status: `missing` };
137
+ nodes.push(next);
138
+ current = next;
139
+ }
140
+ return {
141
+ status: `found`,
142
+ nodes
143
+ };
144
+ }
145
+ //#endregion
146
+ export { resolveKeyPath as a, reconcileSelection as i, reconcileCursor as n, reconcileExpansion as r, pathFromJson as t };
@@ -2,10 +2,14 @@ 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-JIhwB49y.js";
5
+ import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-CgbL-OlS.js";
6
6
  import { i as animateListItemOut, o as prefersReducedMotion, r as animateListItemIn, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
7
+ import { a as capKeys, c as keysAsFlags, f as withoutFlag, i as capFlags, l as keysToArray, n as EMPTY_KEYS, o as flagsFromRecord, s as flagsToRecord, t as EMPTY_FLAGS } from "./pending-BGidHppL.js";
8
+ import { n as createScrollHoverGuard } from "./scroll-hover-guard-Cjw6179Q.js";
7
9
  import "./interaction-E-uMGHkJ.js";
8
10
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
11
+ import { n as anchorFromRow, o as scrollToOffset, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
12
+ import { i as reconcileSelection, r as reconcileExpansion } from "./tree-CftHwtEb.js";
9
13
  import { LitElement, css, html, nothing } from "lit";
10
14
  import { property, state } from "lit/decorators.js";
11
15
  import { createCommandRegistry } from "@clinth/ui-commands";
@@ -1112,6 +1116,12 @@ function computeTreeWindow(params, scrollTop, overscan = 5) {
1112
1116
  end: Math.min(count, Math.max(start, lastVisible + overscan))
1113
1117
  };
1114
1118
  }
1119
+ /** Index of the row whose slot contains content-space `y`, clamped to `[0, count - 1]`. */
1120
+ function treeIndexAt(params, y) {
1121
+ if (params.count <= 0) return -1;
1122
+ const index = Math.floor((y - params.listOffset) / treeStride(params));
1123
+ return Math.min(params.count - 1, Math.max(0, index));
1124
+ }
1115
1125
  /**
1116
1126
  * The scroll offset that brings row `index` fully into view with the least
1117
1127
  * movement, leaving `topInset` px at the top clear (the sticky-header overlay).
@@ -1204,6 +1214,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1204
1214
  this.interactionMode = `standard`;
1205
1215
  this._selectedNodes = /* @__PURE__ */ new Set();
1206
1216
  this.disableKeyboardNavigation = false;
1217
+ this.scrollHoverTickle = false;
1207
1218
  this.registry = void 0;
1208
1219
  this.itemDnd = `none`;
1209
1220
  this.dndExpandHoverMs = 500;
@@ -1214,6 +1225,14 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1214
1225
  this.animation = `auto`;
1215
1226
  this.applyInternalDrop = true;
1216
1227
  this._expandedKeys = /* @__PURE__ */ new Set();
1228
+ this.restoreLoads = true;
1229
+ this.#viewState = new ViewStateController(this, {
1230
+ kind: `tree-list`,
1231
+ capture: () => this.getViewState()
1232
+ });
1233
+ this.#pendingExpanded = EMPTY_FLAGS;
1234
+ this.#pendingSelected = EMPTY_KEYS;
1235
+ this.#reconciledVersion = -1;
1217
1236
  this.#structureVersion = 0;
1218
1237
  this._selectionVersion = 0;
1219
1238
  this._scrollTop = 0;
@@ -1246,6 +1265,17 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1246
1265
  this.setAttribute(`data-input-mode`, this.tickled.inputMode);
1247
1266
  }
1248
1267
  });
1268
+ this.#scrollHover = createScrollHoverGuard({
1269
+ host: this,
1270
+ enabled: () => !this.scrollHoverTickle,
1271
+ onResume: (x, y) => {
1272
+ const key = (this.shadowRoot?.elementFromPoint?.(x, y)?.closest(`li.item`))?.dataset.key;
1273
+ const flat = this.#getFlat();
1274
+ const idx = key === void 0 ? void 0 : flat.indexByKey.get(key);
1275
+ const row = idx === void 0 ? void 0 : flat.rows[idx];
1276
+ if (row) this._handleRowPointerEnter(row.node, row.depth);
1277
+ }
1278
+ });
1249
1279
  this._focusedDepth = 0;
1250
1280
  this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
1251
1281
  labelSelector: `.tree-label`,
@@ -1278,6 +1308,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1278
1308
  this.#stickyDirty = true;
1279
1309
  this.#stickyRaf = 0;
1280
1310
  this.#stickyScrollBound = false;
1311
+ this.#onViewStateScroll = () => {
1312
+ this.#viewState.notifyScroll();
1313
+ };
1281
1314
  this.#selectOrigin = `user`;
1282
1315
  this.#selectSilent = false;
1283
1316
  this.#revealToken = 0;
@@ -1287,7 +1320,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1287
1320
  if (nodes.length === 0) return;
1288
1321
  if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
1289
1322
  event.preventDefault();
1290
- this.selectAll();
1323
+ this.selectAll({ visibleOnly: true });
1291
1324
  return;
1292
1325
  }
1293
1326
  if ([
@@ -1428,7 +1461,23 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1428
1461
  this._expandedKeys = next;
1429
1462
  this.#flat = void 0;
1430
1463
  this.#stickyDirty = true;
1431
- }
1464
+ this.#viewState.notify();
1465
+ }
1466
+ #viewState;
1467
+ /** Saved expansion for keys not (yet) loaded, or loaded but awaiting children. */
1468
+ #pendingExpanded;
1469
+ /** Saved selection for keys not (yet) loaded. */
1470
+ #pendingSelected;
1471
+ #pendingFocus;
1472
+ /**
1473
+ * Cursor key applied from saved state. Recorded as `focus` while the cursor
1474
+ * stays there, even though the input mode isn't `keyboard` (switching the
1475
+ * mode would reset the shared hover guard).
1476
+ */
1477
+ #restoredFocus;
1478
+ /** Structure version the view state was last reconciled against. */
1479
+ #reconciledVersion;
1480
+ #viewStateScrollTarget;
1432
1481
  #flat;
1433
1482
  #flatRoot;
1434
1483
  #flatFilter;
@@ -1623,6 +1672,25 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1623
1672
  get _depthsWithIcons() {
1624
1673
  return this.#getTreeIndex().depthsWithIcons;
1625
1674
  }
1675
+ #scrollHover;
1676
+ /**
1677
+ * Row `pointerenter` funnel: moves the hover cursor unless a scroll gesture
1678
+ * is sliding rows beneath a stationary pointer (see `scrollHoverTickle`).
1679
+ * Subclasses rendering their own rows should call this rather than the
1680
+ * tickled controller directly.
1681
+ */
1682
+ _handleRowPointerEnter(node, depth) {
1683
+ if (this.#scrollHover.suppressed()) return;
1684
+ this.tickled.handlePointerEnter(node.item.key);
1685
+ this.dispatchEvent(new CustomEvent(`tickle`, {
1686
+ detail: {
1687
+ node,
1688
+ depth
1689
+ },
1690
+ bubbles: true,
1691
+ composed: true
1692
+ }));
1693
+ }
1626
1694
  #unregisterSearchCommands;
1627
1695
  #effectiveSearchModes() {
1628
1696
  const raw = this.searchModes?.trim();
@@ -1844,10 +1912,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1844
1912
  }
1845
1913
  this.#flat = void 0;
1846
1914
  if (changedProps.has(`root`) || changedProps.has(`model`)) this.#structureVersion++;
1847
- if (changedProps.has(`root`) && this.root && this._expandedKeys.size > 0) {
1848
- const { parentByKey } = this.#getTreeIndex();
1849
- this.#setExpanded(withoutKeys(this._expandedKeys, (key) => !parentByKey.has(key)));
1850
- }
1915
+ if (this.#reconciledVersion !== this.#structureVersion) this.#reconcileViewState();
1851
1916
  this.#diffVisibleNodes(changedProps.has(`filterPredicate`));
1852
1917
  const virtual = this.#virtualActive();
1853
1918
  this.toggleAttribute(`data-virtual`, virtual);
@@ -1970,6 +2035,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1970
2035
  updated(changedProps) {
1971
2036
  this.#rowsCache = void 0;
1972
2037
  this.#syncVirtualObservers();
2038
+ if (changedProps.has(`scrollHoverTickle`) && this.scrollHoverTickle) this.#scrollHover.end();
1973
2039
  if (this.#scrollOnlyUpdate) {
1974
2040
  this.#scrollOnlyUpdate = false;
1975
2041
  this.#repaintSearchHighlights();
@@ -1978,8 +2044,6 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1978
2044
  this.#syncStickyScroll();
1979
2045
  if (changedProps.has(`root`) && this.root) {
1980
2046
  this.#abortPending();
1981
- this.#clearSelectionInternal();
1982
- this.tickled.clearTickled();
1983
2047
  this._focusedDepth = 0;
1984
2048
  this.#cancelAllLeavingAnimations();
1985
2049
  this.#leaving.clear();
@@ -2004,7 +2068,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2004
2068
  });
2005
2069
  }
2006
2070
  }
2007
- if (changedProps.has(`interactionMode`)) {
2071
+ if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
2008
2072
  this.#anchor = void 0;
2009
2073
  this.clearSelection();
2010
2074
  }
@@ -2018,6 +2082,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2018
2082
  this.#repaintSearchHighlights();
2019
2083
  } else this.#runItemAnimations();
2020
2084
  this.#animationOverride = void 0;
2085
+ this.#syncViewStateAfterRender();
2021
2086
  }
2022
2087
  /** Highlights are painted on rendered labels, so they move with the window. */
2023
2088
  #repaintSearchHighlights() {
@@ -2031,7 +2096,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2031
2096
  id: `select-all`,
2032
2097
  label: `Select All`,
2033
2098
  description: `Select all visible items`,
2034
- execute: () => this.selectAll()
2099
+ execute: () => this.selectAll({ visibleOnly: true })
2035
2100
  });
2036
2101
  registry.register({
2037
2102
  id: `deselect-all`,
@@ -2433,6 +2498,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2433
2498
  this.#postDragSuppressTimer = void 0;
2434
2499
  }
2435
2500
  this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
2501
+ this.#scrollHover.end();
2436
2502
  this.#teardownVirtualObservers();
2437
2503
  if (this.#stickyScrollBound) {
2438
2504
  this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
@@ -2461,6 +2527,145 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2461
2527
  };
2462
2528
  this.requestUpdate();
2463
2529
  }
2530
+ /**
2531
+ * Snapshot selection, expansion, cursor and scroll as plain JSON, keyed by
2532
+ * `item.key`. Includes pending state for keys not currently loaded.
2533
+ */
2534
+ getViewState(options) {
2535
+ const withPending = options?.includePending ?? true;
2536
+ const tickled = this.tickled.tickledId;
2537
+ const focus = this.tickled.inputMode === `keyboard` || tickled !== void 0 && tickled === this.#restoredFocus ? tickled : void 0;
2538
+ return this.#viewState.shape({
2539
+ selected: keysToArray([...this._selectedNodes].map((n) => n.item.key), withPending ? this.#pendingSelected : []),
2540
+ expanded: flagsToRecord(withPending ? this.#pendingExpanded : [], keysAsFlags(this._expandedKeys)),
2541
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0),
2542
+ scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? this.#captureScroll()
2543
+ }, options);
2544
+ }
2545
+ /**
2546
+ * Apply saved view state. Fields present replace the current ones; keys
2547
+ * that aren't loaded yet are applied as they appear — including via
2548
+ * `loadChildren` / `model` for saved-open branches when `restoreLoads`.
2549
+ */
2550
+ setViewState(state) {
2551
+ if (!this.#viewState.accepts(state)) return;
2552
+ this.#viewState.restoring(() => {
2553
+ if (state.expanded) {
2554
+ this.#pendingExpanded = capFlags(flagsFromRecord(state.expanded));
2555
+ this.#setExpanded(/* @__PURE__ */ new Set());
2556
+ }
2557
+ if (state.selected) {
2558
+ this.#pendingSelected = capKeys(new Set(state.selected));
2559
+ if (this._selectedNodes.size > 0) {
2560
+ const prev = new Set(this._selectedNodes);
2561
+ this.#clearSelectionInternal();
2562
+ this.#dispatchSelect(prev);
2563
+ }
2564
+ }
2565
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
2566
+ if (state.scroll) this.#viewState.pendingScroll = state.scroll;
2567
+ this.#reconcileViewState();
2568
+ });
2569
+ this.requestUpdate();
2570
+ }
2571
+ clearPendingViewState() {
2572
+ this.#pendingExpanded = EMPTY_FLAGS;
2573
+ this.#pendingSelected = EMPTY_KEYS;
2574
+ this.#pendingFocus = void 0;
2575
+ this.#viewState.pendingScroll = void 0;
2576
+ }
2577
+ /** Move state for missing keys to pending; apply pending state for keys now loaded. */
2578
+ #reconcileViewState() {
2579
+ this.#reconciledVersion = this.#structureVersion;
2580
+ if (!this.root) return;
2581
+ const { parentByKey } = this.#getTreeIndex();
2582
+ const has = (key) => parentByKey.has(key);
2583
+ this.#viewState.restoring(() => {
2584
+ const exp = reconcileExpansion(this._expandedKeys, this.#pendingExpanded, has, (key) => this.#nodeByKey(key)?.children !== void 0);
2585
+ if (exp.changed) {
2586
+ let pending = exp.pending;
2587
+ for (const key of exp.needsLoad) if (this.#nodeByKey(key)?.item.isLeaf !== false) pending = withoutFlag(pending, key);
2588
+ this.#pendingExpanded = pending;
2589
+ if (exp.live !== this._expandedKeys) {
2590
+ this.#animationOverride ??= false;
2591
+ this.#setExpanded(exp.live);
2592
+ }
2593
+ }
2594
+ const sel = reconcileSelection(this._selectedNodes, this.#pendingSelected, (key) => has(key) ? this.#nodeByKey(key) : void 0, (node) => this.#canSelect(node), this.interactionMode === `implicit`);
2595
+ if (sel.changed) {
2596
+ const prev = this._selectedNodes;
2597
+ this._selectedNodes = new Set(sel.selected);
2598
+ this.#pendingSelected = sel.pending;
2599
+ const selectedKey = this.selectedNode?.item.key;
2600
+ this.selectedNode = (selectedKey === void 0 ? void 0 : [...sel.selected].find((n) => n.item.key === selectedKey)) ?? [...sel.selected].at(-1);
2601
+ if (sel.restored) this.#dispatchSelect(prev);
2602
+ else this._selectionVersion++;
2603
+ }
2604
+ const tickled = this.tickled.tickledId;
2605
+ if (tickled !== void 0 && !has(tickled)) {
2606
+ this.#pendingFocus ??= tickled;
2607
+ this.tickled.clearTickled();
2608
+ }
2609
+ if (this.#pendingFocus !== void 0 && this.#getFlat().indexByKey.has(this.#pendingFocus)) {
2610
+ this.tickled.setTickled(this.#pendingFocus);
2611
+ this.#restoredFocus = this.#pendingFocus;
2612
+ this.#pendingFocus = void 0;
2613
+ }
2614
+ });
2615
+ }
2616
+ /** Post-render: bind scroll tracking, apply a pending scroll, continue restore loads. */
2617
+ #syncViewStateAfterRender() {
2618
+ const container = this.#container();
2619
+ if (container && container !== this.#viewStateScrollTarget) {
2620
+ this.#viewStateScrollTarget?.removeEventListener(`scroll`, this.#onViewStateScroll);
2621
+ this.#viewStateScrollTarget = container;
2622
+ container.addEventListener(`scroll`, this.#onViewStateScroll, { passive: true });
2623
+ this.#viewState.observeScrollTarget(container);
2624
+ }
2625
+ this.#viewState.applyPendingScroll((scroll) => this.#applyScroll(scroll));
2626
+ this.#continueRestoreLoads();
2627
+ }
2628
+ #onViewStateScroll;
2629
+ #captureScroll() {
2630
+ const container = this.#container();
2631
+ if (!container) return void 0;
2632
+ const rows = this.#getFlat().rows;
2633
+ if (!this.#virtualActive()) this.#measureMetrics();
2634
+ const params = this.#layoutParams(rows.length);
2635
+ const top = container.scrollTop;
2636
+ const index = treeIndexAt(params, top);
2637
+ const key = index >= 0 ? rows[index]?.node.item.key : void 0;
2638
+ return anchorFromRow(top, key, index >= 0 ? treeRowTop(params, index) : 0);
2639
+ }
2640
+ /** Returns `true` once the saved position is fully reached. */
2641
+ #applyScroll(scroll) {
2642
+ const container = this.#container();
2643
+ if (!container) return false;
2644
+ let target = scroll.top;
2645
+ if (scroll.anchor) {
2646
+ const flat = this.#getFlat();
2647
+ const index = flat.indexByKey.get(scroll.anchor.key);
2648
+ if (index === void 0) return false;
2649
+ if (!this.#virtualActive()) this.#measureMetrics();
2650
+ target = treeRowTop(this.#layoutParams(flat.rows.length), index) + scroll.anchor.offset;
2651
+ }
2652
+ return scrollToOffset(container, target);
2653
+ }
2654
+ /** Open the next saved-open branch whose children aren't loaded (one load at a time). */
2655
+ #continueRestoreLoads() {
2656
+ if (!this.restoreLoads || this.#pendingNode || this.#pendingExpanded.size === 0 || !this.root || !this.#hasLoader()) return;
2657
+ const { parentByKey } = this.#getTreeIndex();
2658
+ for (const [key, open] of this.#pendingExpanded) {
2659
+ if (!open || !parentByKey.has(key)) continue;
2660
+ const node = this.#nodeByKey(key);
2661
+ if (node && node.children === void 0 && node.item.isLeaf === false) {
2662
+ let depth = 0;
2663
+ for (let k = parentByKey.get(key); k !== void 0 && k !== this.root.item.key; k = parentByKey.get(k)) depth++;
2664
+ this.#startLoad(node, depth);
2665
+ return;
2666
+ }
2667
+ }
2668
+ }
2464
2669
  select(node) {
2465
2670
  if (!this.#canSelect(node)) return;
2466
2671
  const prev = new Set(this._selectedNodes);
@@ -2479,39 +2684,53 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2479
2684
  this.requestUpdate();
2480
2685
  }
2481
2686
  clearSelection() {
2687
+ this.#pendingSelected = EMPTY_KEYS;
2482
2688
  if (this._selectedNodes.size === 0) return;
2483
2689
  const prev = new Set(this._selectedNodes);
2484
2690
  this.#clearSelectionInternal();
2485
2691
  this.#dispatchSelect(prev);
2486
2692
  this.requestUpdate();
2487
2693
  }
2488
- selectAll() {
2694
+ /**
2695
+ * Select every selectable node in the tree, or only the matches while a
2696
+ * `filterPredicate` is active. Nodes inside collapsed branches are included;
2697
+ * unloaded (lazy) children are not. With `visibleOnly`, only rows that are
2698
+ * showing are selected (Ctrl+A). In `checked` mode checking a row checks its
2699
+ * descendants, so `visibleOnly` makes no difference there.
2700
+ */
2701
+ selectAll(options = {}) {
2489
2702
  const prev = new Set(this._selectedNodes);
2490
- if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
2491
- if (!this.#canSelect(node)) continue;
2492
- this.#applyCheckedToggle(node, true);
2493
- }
2494
- else {
2495
- for (const { node } of this.#getFlat().rows) {
2496
- if (!this.#canSelect(node)) continue;
2497
- this._selectedNodes.add(node);
2498
- }
2499
- this.selectedNode = [...this._selectedNodes].at(-1);
2500
- }
2703
+ for (const node of this.#bulkSelectionScope(options)) if (this.#canSelect(node)) this._selectedNodes.add(node);
2704
+ if (this.selectedNode === void 0 || !this._selectedNodes.has(this.selectedNode)) this.selectedNode = [...this._selectedNodes].at(-1);
2501
2705
  this.#dispatchSelect(prev);
2502
2706
  this.requestUpdate();
2503
2707
  }
2504
- deselectAll() {
2708
+ /** Mirror of {@link selectAll}: deselect the same set of nodes it would select. */
2709
+ deselectAll(options = {}) {
2505
2710
  const prev = new Set(this._selectedNodes);
2506
- if (this.interactionMode === `checked`) for (const { node } of this.#getFlat().rows) {
2507
- if (!this.#canSelect(node)) continue;
2508
- this.#applyCheckedToggle(node, false);
2509
- }
2510
- else this.#clearSelectionInternal();
2711
+ for (const node of this.#bulkSelectionScope(options)) this._selectedNodes.delete(node);
2712
+ if (this.selectedNode !== void 0 && !this._selectedNodes.has(this.selectedNode)) this.selectedNode = [...this._selectedNodes].at(-1);
2511
2713
  this.#dispatchSelect(prev);
2512
2714
  this.requestUpdate();
2513
2715
  }
2514
2716
  /**
2717
+ * Nodes `selectAll` / `deselectAll` act on (before `canSelect`): the showing
2718
+ * rows for `visibleOnly` outside `checked` mode, otherwise every loaded node
2719
+ * that the active filter (if any) matches.
2720
+ */
2721
+ #bulkSelectionScope(options) {
2722
+ if (options.visibleOnly === true && this.interactionMode !== `checked`) return this.#getFlat().rows.map((r) => r.node);
2723
+ const matches = this.#getFilter()?.matches;
2724
+ const result = [];
2725
+ const visit = (node) => {
2726
+ if (matches && !matches.has(node.item.key)) return;
2727
+ result.push(node);
2728
+ node.children?.forEach(visit);
2729
+ };
2730
+ this.root?.children?.forEach(visit);
2731
+ return result;
2732
+ }
2733
+ /**
2515
2734
  * Expand every already-loaded branch in a single update. With `maxDepth`,
2516
2735
  * only branches at depth < `maxDepth` are expanded (the root's children are
2517
2736
  * depth 0), so rows at depth `maxDepth` become visible but stay collapsed.
@@ -2680,6 +2899,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2680
2899
  #selectSilent;
2681
2900
  #dispatchSelect(previous) {
2682
2901
  this._selectionVersion++;
2902
+ if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
2903
+ this.#viewState.notify();
2683
2904
  if (this.#selectSilent) return;
2684
2905
  const detail = {
2685
2906
  selected: new Set(this._selectedNodes),
@@ -2897,12 +3118,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2897
3118
  }));
2898
3119
  } else if (node.children === void 0) {
2899
3120
  if (node.item.isLeaf !== false) return;
2900
- if (this.model ?? (this.loadChildren ? this : void 0)) {
2901
- this.#pendingNode = node;
2902
- const controller = new AbortController();
2903
- this.#pendingController = controller;
2904
- this.#invokeLoadChildren(node, depth, controller).catch(() => {});
2905
- }
3121
+ if (this.#hasLoader()) this.#startLoad(node, depth);
2906
3122
  } else {
2907
3123
  this.#applyExclusivity(node.item.key);
2908
3124
  this.#setExpanded(withKey(this._expandedKeys, node.item.key));
@@ -2970,6 +3186,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2970
3186
  this.#dispatchSelect(prev);
2971
3187
  }
2972
3188
  }
3189
+ #hasLoader() {
3190
+ return this.model !== void 0 || this.loadChildren !== void 0;
3191
+ }
3192
+ #startLoad(node, depth) {
3193
+ this.#pendingNode = node;
3194
+ const controller = new AbortController();
3195
+ this.#pendingController = controller;
3196
+ this.#invokeLoadChildren(node, depth, controller).catch(() => {});
3197
+ }
2973
3198
  async #invokeLoadChildren(node, depth, controller) {
2974
3199
  try {
2975
3200
  let children;
@@ -3114,6 +3339,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
3114
3339
  const { node, depth } = nodes[index];
3115
3340
  this.tickled.setTickled(node.item.key);
3116
3341
  this._focusedDepth = depth;
3342
+ this.#pendingFocus = void 0;
3343
+ this.#viewState.notify();
3117
3344
  this.dispatchEvent(new CustomEvent(`tickle`, {
3118
3345
  detail: {
3119
3346
  node,
@@ -3162,7 +3389,12 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
3162
3389
  data-input-mode=${this.tickled.inputMode}
3163
3390
  @keydown=${this.#onKeyDown}
3164
3391
  @dblclick=${this.#onDblClick}
3165
- @pointerleave=${(e) => this.tickled.handlePointerLeave(e)}
3392
+ @scroll=${this.#scrollHover.onScroll}
3393
+ @pointermove=${this.#scrollHover.onPointerMove}
3394
+ @pointerleave=${(e) => {
3395
+ this.#scrollHover.onPointerLeave();
3396
+ this.tickled.handlePointerLeave(e);
3397
+ }}
3166
3398
  tabindex="0"
3167
3399
  >
3168
3400
  ${this.#renderStickyHeaders()}
@@ -3263,17 +3495,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
3263
3495
  draggable=${dnd.draggable ? `true` : nothing}
3264
3496
  part=${dnd.dropTarget ? `drop-target` : nothing}
3265
3497
  @click=${(e) => this._handleItemClick(node, depth, e)}
3266
- @pointerenter=${() => {
3267
- this.tickled.handlePointerEnter(node.item.key);
3268
- this.dispatchEvent(new CustomEvent(`tickle`, {
3269
- detail: {
3270
- node,
3271
- depth
3272
- },
3273
- bubbles: true,
3274
- composed: true
3275
- }));
3276
- }}
3498
+ @pointerenter=${() => this._handleRowPointerEnter(node, depth)}
3277
3499
  >
3278
3500
  ${dnd.dropTarget && (dnd.dropPosition === `before` || dnd.dropPosition === `after`) ? html`<div
3279
3501
  class="drop-line drop-line-${dnd.dropPosition}"
@@ -3535,6 +3757,11 @@ __decorate([property({
3535
3757
  type: Boolean,
3536
3758
  attribute: `disable-keyboard-navigation`
3537
3759
  })], TreeBaseElement.prototype, "disableKeyboardNavigation", void 0);
3760
+ __decorate([property({
3761
+ type: Boolean,
3762
+ attribute: `scroll-hover-tickle`,
3763
+ reflect: true
3764
+ })], TreeBaseElement.prototype, "scrollHoverTickle", void 0);
3538
3765
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "registry", void 0);
3539
3766
  __decorate([property({
3540
3767
  type: String,
@@ -3588,6 +3815,7 @@ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrop
3588
3815
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "parseExternalDrop", void 0);
3589
3816
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "applyInternalDrop", void 0);
3590
3817
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
3818
+ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "restoreLoads", void 0);
3591
3819
  __decorate([state()], TreeBaseElement.prototype, "_scrollTop", void 0);
3592
3820
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
3593
3821
  __decorate([state()], TreeBaseElement.prototype, "_stickyKeys", void 0);
@@ -3689,7 +3917,7 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
3689
3917
  draggable=${dnd.draggable ? `true` : nothing}
3690
3918
  part=${dnd.dropTarget ? `drop-target` : nothing}
3691
3919
  @click=${(e) => this._handleItemClick(node, depth, e)}
3692
- @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
3920
+ @pointerenter=${() => this._handleRowPointerEnter(node, depth)}
3693
3921
  >
3694
3922
  ${dnd.dropTarget && (dnd.dropPosition === `before` || dnd.dropPosition === `after`) ? html`<div
3695
3923
  class="drop-line drop-line-${dnd.dropPosition}"