@ixfx/components 0.10.0 → 0.10.2

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 (110) hide show
  1. package/bundle/index.d.ts +233 -21
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2101 -150
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +38 -85
  6. package/dist/ac-text.js +1 -1
  7. package/dist/ac-token.js +2 -2
  8. package/dist/{colour-picker-R73i0QTP.js → colour-picker-WDrPuqoO.js} +2 -2
  9. package/dist/{colour-picker-R73i0QTP.js.map → colour-picker-WDrPuqoO.js.map} +1 -1
  10. package/dist/colour-picker.js +1 -1
  11. package/dist/crumbs.d.ts +1 -1
  12. package/dist/crumbs.js +2 -2
  13. package/dist/{data-grid-C5hk_pzK.d.ts → data-grid-pv6eaJCZ.d.ts} +4 -3
  14. package/dist/data-grid-pv6eaJCZ.d.ts.map +1 -0
  15. package/dist/data-grid.d.ts +2 -2
  16. package/dist/data-grid.js +28 -10
  17. package/dist/data-grid.js.map +1 -1
  18. package/dist/defaults-kVl1t2tT.js +59 -0
  19. package/dist/defaults-kVl1t2tT.js.map +1 -0
  20. package/dist/{detail-list-CxPpv89T.d.ts → detail-list-BA3RVVCB.d.ts} +4 -4
  21. package/dist/{detail-list-CxPpv89T.d.ts.map → detail-list-BA3RVVCB.d.ts.map} +1 -1
  22. package/dist/detail-list.d.ts +3 -3
  23. package/dist/detail-list.js +4 -4
  24. package/dist/{grid-list-DSnYOGh7.js → grid-list-CIc4jo-x.js} +34 -7
  25. package/dist/grid-list-CIc4jo-x.js.map +1 -0
  26. package/dist/{grid-list-BGvoWbZz.d.ts → grid-list-DWXJoRnK.d.ts} +4 -4
  27. package/dist/{grid-list-BGvoWbZz.d.ts.map → grid-list-DWXJoRnK.d.ts.map} +1 -1
  28. package/dist/grid-list.d.ts +2 -2
  29. package/dist/grid-list.js +1 -1
  30. package/dist/handle-DAXs7kbJ.js +656 -0
  31. package/dist/handle-DAXs7kbJ.js.map +1 -0
  32. package/dist/{icon-BCJJYEpm.js → icon-1gpwtPx8.js} +10 -1
  33. package/dist/icon-1gpwtPx8.js.map +1 -0
  34. package/dist/icon-CDGprSdE.d.ts.map +1 -1
  35. package/dist/icons.d.ts +1 -1
  36. package/dist/icons.js +2 -2
  37. package/dist/{incr-search-C05WjbZh.js → incr-search-D91M4Ozq.js} +5 -4
  38. package/dist/incr-search-D91M4Ozq.js.map +1 -0
  39. package/dist/incr-search.d.ts +1 -1
  40. package/dist/incr-search.js +1 -1
  41. package/dist/index-9AhaHIT2.d.ts.map +1 -1
  42. package/dist/index-CYndvTWH.d.ts +38 -0
  43. package/dist/index-CYndvTWH.d.ts.map +1 -0
  44. package/dist/{index--5BSdbnL.d.ts → index-CsaQwNkz.d.ts} +3 -2
  45. package/dist/index-CsaQwNkz.d.ts.map +1 -0
  46. package/dist/index.d.ts +207 -11
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1295 -40
  49. package/dist/index.js.map +1 -1
  50. package/dist/{list-native-drag-controller-D97TYjn1.js → list-native-drag-controller-B9xO0XWn.js} +6 -4
  51. package/dist/list-native-drag-controller-B9xO0XWn.js.map +1 -0
  52. package/dist/{list-native-drag-controller-DWiXQZou.d.ts → list-native-drag-controller-BAuWAtyQ.d.ts} +2 -2
  53. package/dist/{list-native-drag-controller-DWiXQZou.d.ts.map → list-native-drag-controller-BAuWAtyQ.d.ts.map} +1 -1
  54. package/dist/{menu-C8a-84S5.js → menu-B2Qv3Oiz.js} +6 -5
  55. package/dist/menu-B2Qv3Oiz.js.map +1 -0
  56. package/dist/{menu-item-DzizkMH-.js → menu-item-Woc8jJc1.js} +4 -4
  57. package/dist/menu-item-Woc8jJc1.js.map +1 -0
  58. package/dist/menu.js +2 -2
  59. package/dist/miller.d.ts +2 -2
  60. package/dist/miller.js +1 -1
  61. package/dist/narrowed-text.js +1 -1
  62. package/dist/{scroll-fade-CAs1EabA.js → scroll-fade-BrNUIYnm.js} +16 -3
  63. package/dist/scroll-fade-BrNUIYnm.js.map +1 -0
  64. package/dist/select-horiz.js +1 -1
  65. package/dist/{split-layout-3yBOEVZW.js → split-layout-BJEU1W0d.js} +2 -2
  66. package/dist/{split-layout-3yBOEVZW.js.map → split-layout-BJEU1W0d.js.map} +1 -1
  67. package/dist/split-layout.js +1 -1
  68. package/dist/style.css +38 -85
  69. package/dist/tabs.js +1 -1
  70. package/dist/{timeline-CFlBFFGZ.js → timeline-BP1Ux2EV.js} +2 -2
  71. package/dist/{timeline-CFlBFFGZ.js.map → timeline-BP1Ux2EV.js.map} +1 -1
  72. package/dist/timeline.js +1 -1
  73. package/dist/titlebar.js +1 -1
  74. package/dist/{tree-C25xYUa6.js → tree-Bm2TzTfe.js} +234 -15
  75. package/dist/tree-Bm2TzTfe.js.map +1 -0
  76. package/dist/{tree-component-mw8x-n2Y.d.ts → tree-component-DGmKBbpy.d.ts} +14 -1
  77. package/dist/tree-component-DGmKBbpy.d.ts.map +1 -0
  78. package/dist/tree.d.ts +15 -3
  79. package/dist/tree.d.ts.map +1 -1
  80. package/dist/tree.js +1 -1
  81. package/dist/{vertical-list--tFanQwM.d.ts → vertical-list-BWLqy3PJ.d.ts} +3 -3
  82. package/dist/{vertical-list--tFanQwM.d.ts.map → vertical-list-BWLqy3PJ.d.ts.map} +1 -1
  83. package/dist/vertical-list.d.ts +2 -2
  84. package/dist/vertical-list.js +2 -2
  85. package/docs-user/README.md +1 -0
  86. package/docs-user/icons.md +24 -10
  87. package/docs-user/index.json +8 -3
  88. package/docs-user/llms.txt +2 -1
  89. package/docs-user/multicol-list.md +128 -0
  90. package/docs-user/tree.md +12 -0
  91. package/docs-user/vertical-list.md +65 -9
  92. package/llms.txt +2 -1
  93. package/package.json +1 -1
  94. package/dist/data-grid-C5hk_pzK.d.ts.map +0 -1
  95. package/dist/defaults-BJwDsMH4.js +0 -86
  96. package/dist/defaults-BJwDsMH4.js.map +0 -1
  97. package/dist/grid-list-DSnYOGh7.js.map +0 -1
  98. package/dist/handle-BPOxBOKf.js +0 -219
  99. package/dist/handle-BPOxBOKf.js.map +0 -1
  100. package/dist/icon-BCJJYEpm.js.map +0 -1
  101. package/dist/incr-search-C05WjbZh.js.map +0 -1
  102. package/dist/index--5BSdbnL.d.ts.map +0 -1
  103. package/dist/index-CS9yM8L-.d.ts +0 -47
  104. package/dist/index-CS9yM8L-.d.ts.map +0 -1
  105. package/dist/list-native-drag-controller-D97TYjn1.js.map +0 -1
  106. package/dist/menu-C8a-84S5.js.map +0 -1
  107. package/dist/menu-item-DzizkMH-.js.map +0 -1
  108. package/dist/scroll-fade-CAs1EabA.js.map +0 -1
  109. package/dist/tree-C25xYUa6.js.map +0 -1
  110. package/dist/tree-component-mw8x-n2Y.d.ts.map +0 -1
@@ -2,8 +2,8 @@ import { t as TickledController } from "./tickled-controller-Dh1Uh_vG.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-C05WjbZh.js";
6
- import { t as ListNativeDragController } from "./list-native-drag-controller-D97TYjn1.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";
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-BskmlA9t.js";
9
9
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
@@ -775,6 +775,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
775
775
  this.itemDnd = `none`;
776
776
  this.dndExpandHoverMs = 500;
777
777
  this.reorderable = true;
778
+ this.stickyHeaders = true;
778
779
  this.applyInternalDrop = true;
779
780
  this._expandedKeys = /* @__PURE__ */ new Set();
780
781
  this.tickled = new TickledController(this, {
@@ -807,6 +808,18 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
807
808
  this.#onContainerScroll = () => {
808
809
  this.#rowsCache = void 0;
809
810
  };
811
+ this.#onStickyScroll = () => {
812
+ if (this.#stickyRaf !== 0) return;
813
+ this.#stickyRaf = requestAnimationFrame(() => {
814
+ this.#stickyRaf = 0;
815
+ this.#updateStickyHeaders();
816
+ });
817
+ };
818
+ this._stickyKeys = [];
819
+ this.#stickyEntries = /* @__PURE__ */ new Map();
820
+ this.#stickyDirty = true;
821
+ this.#stickyRaf = 0;
822
+ this.#stickyScrollBound = false;
810
823
  this.#onKeyDown = (event) => {
811
824
  if (this.search.isOpen) return;
812
825
  const nodes = this.#getAllNodes();
@@ -910,7 +923,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
910
923
  }
911
924
  };
912
925
  this.#onDblClick = (e) => {
913
- const li = e.target.closest(`li`);
926
+ const li = e.target.closest(`ul.tree-list li`);
914
927
  if (!li) return;
915
928
  const key = li.dataset.key;
916
929
  if (!key) return;
@@ -972,6 +985,157 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
972
985
  /** Per-drag cache of measured rows; invalidated on scroll / update. */
973
986
  #rowsCache;
974
987
  #onContainerScroll;
988
+ /** Bind the sticky scroll listener once the container exists; unbind on opt-out. */
989
+ #syncStickyScroll() {
990
+ const container = this.#container();
991
+ if (this.stickyHeaders && container) {
992
+ if (!this.#stickyScrollBound || this.#stickyScrollContainer !== container) {
993
+ this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
994
+ container.addEventListener(`scroll`, this.#onStickyScroll, { passive: true });
995
+ this.#stickyScrollContainer = container;
996
+ this.#stickyScrollBound = true;
997
+ }
998
+ } else if (this.#stickyScrollBound) {
999
+ this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
1000
+ this.#stickyScrollContainer = void 0;
1001
+ this.#stickyScrollBound = false;
1002
+ }
1003
+ }
1004
+ #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)}"]`);
1008
+ }
1009
+ /**
1010
+ * Recompute which ancestors stick to the top: the ancestors of the first
1011
+ * row whose bottom clears the container top, minus any ancestor that is
1012
+ * itself still visible below the overlay. Synchronous — reads current
1013
+ * geometry, renders nothing, expands nothing.
1014
+ */
1015
+ #updateStickyHeaders() {
1016
+ this.#stickyDirty = false;
1017
+ const empty = /* @__PURE__ */ new Map();
1018
+ if (!this.stickyHeaders || !this.root) {
1019
+ this.#setStickyKeys([], empty);
1020
+ return;
1021
+ }
1022
+ 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;
1056
+ const chain = [];
1057
+ let cursor = parent.get(topKey);
1058
+ while (cursor !== void 0 && cursor !== rootKey) {
1059
+ chain.unshift(cursor);
1060
+ cursor = parent.get(cursor);
1061
+ }
1062
+ const byKey = new Map(nodes.map((entry) => [entry.node.item.key, entry]));
1063
+ const entries = /* @__PURE__ */ new Map();
1064
+ 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;
1070
+ stuck.push(key);
1071
+ entries.set(key, entry);
1072
+ }
1073
+ this.#setStickyKeys(stuck, entries);
1074
+ }
1075
+ #setStickyKeys(keys, entries) {
1076
+ const prev = this._stickyKeys;
1077
+ if (prev.length === keys.length && prev.every((key, i) => key === keys[i])) return;
1078
+ this.#stickyEntries = entries;
1079
+ this._stickyKeys = keys;
1080
+ }
1081
+ #renderStickyHeaders() {
1082
+ if (!this.stickyHeaders || this._stickyKeys.length === 0) return nothing;
1083
+ return html`
1084
+ <div class="sticky-headers" part="sticky-headers">
1085
+ ${this._stickyKeys.map((key) => {
1086
+ const entry = this.#stickyEntries.get(key);
1087
+ if (!entry) return nothing;
1088
+ return this.#renderStickyHeader(entry.node, entry.depth);
1089
+ })}
1090
+ </div>
1091
+ `;
1092
+ }
1093
+ #renderStickyHeader(node, depth) {
1094
+ const selected = this._isSelected(node);
1095
+ const isBranch = node.item.isLeaf !== true;
1096
+ return html`
1097
+ <div
1098
+ class="item ${isBranch ? `branch` : `leaf`} sticky-header${selected ? ` selected` : ``}"
1099
+ part="sticky-header"
1100
+ role="button"
1101
+ tabindex="0"
1102
+ aria-label="Go to ${node.item.label}"
1103
+ data-key=${node.item.key}
1104
+ style=${`--depth: ${depth}`}
1105
+ @click=${(e) => this.#onStickyClick(e, node)}
1106
+ @keydown=${(e) => {
1107
+ if (e.key === `Enter` || e.key === ` `) {
1108
+ e.preventDefault();
1109
+ this.#activateSticky(node);
1110
+ }
1111
+ }}
1112
+ >${this.renderNodeContent(node, depth)}</div>
1113
+ `;
1114
+ }
1115
+ #onStickyClick(event, node) {
1116
+ if (this.#drop?.isDragging || this.#nativeDrag?.isDragging) return;
1117
+ if (node.item.isLeaf !== true && event.target instanceof Element && event.target.closest(`.tree-caret`)) {
1118
+ const depth = this.#stickyEntries.get(node.item.key)?.depth ?? 0;
1119
+ this.#toggleExpand(node, depth);
1120
+ this.#stickyDirty = true;
1121
+ this.requestUpdate();
1122
+ return;
1123
+ }
1124
+ this.#activateSticky(node);
1125
+ }
1126
+ #activateSticky(node) {
1127
+ if (this.#canSelect(node)) this.select(node);
1128
+ this.#scrollKeyIntoView(node.item.key);
1129
+ this.#stickyDirty = true;
1130
+ this.requestUpdate();
1131
+ }
1132
+ /** Visible `{ node, depth }` entries for `_stickyKeys` — depths drive the indent. */
1133
+ #stickyEntries;
1134
+ /** Set when expansion / data / filter changes may have altered the sticky set. Consumed in `updated()`. */
1135
+ #stickyDirty;
1136
+ #stickyRaf;
1137
+ #stickyScrollContainer;
1138
+ #stickyScrollBound;
975
1139
  get nativeDrag() {
976
1140
  return this.#nativeDrag;
977
1141
  }
@@ -995,6 +1159,8 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
995
1159
  willUpdate(changedProps) {
996
1160
  super.willUpdate(changedProps);
997
1161
  this.#diffVisibleNodes();
1162
+ if (changedProps.has(`root`) || changedProps.has(`model`) || changedProps.has(`filterPredicate`) || changedProps.has(`stickyHeaders`)) this.#stickyDirty = true;
1163
+ if (this.#stickyDirty) this.#updateStickyHeaders();
998
1164
  }
999
1165
  #diffVisibleNodes() {
1000
1166
  const nodes = this.root ? this.#getAllNodes() : [];
@@ -1094,6 +1260,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1094
1260
  }
1095
1261
  updated(changedProps) {
1096
1262
  this.#rowsCache = void 0;
1263
+ this.#syncStickyScroll();
1097
1264
  if (changedProps.has(`root`) && this.root) {
1098
1265
  this.#abortPending();
1099
1266
  this._expandedKeys.clear();
@@ -1117,6 +1284,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1117
1284
  } else if (event.type === `invalidate`) {
1118
1285
  if (this._expandedKeys.has(event.key)) {
1119
1286
  this._expandedKeys.delete(event.key);
1287
+ this.#stickyDirty = true;
1120
1288
  this.requestUpdate();
1121
1289
  }
1122
1290
  }
@@ -1159,7 +1327,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1159
1327
  isEnabled: () => this.itemDnd !== `none` && this.#anyRowDraggable(),
1160
1328
  resolveEventToKey: (target) => {
1161
1329
  if (!(target instanceof Element)) return void 0;
1162
- return target.closest(`li[data-key]`)?.getAttribute(`data-key`) ?? void 0;
1330
+ return target.closest(`ul.tree-list li[data-key]`)?.getAttribute(`data-key`) ?? void 0;
1163
1331
  },
1164
1332
  prepareSelection: (originKey) => {
1165
1333
  const allNodes = this.#getAllNodes();
@@ -1174,7 +1342,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1174
1342
  key: n.node.item.key,
1175
1343
  index: i,
1176
1344
  item: n.node,
1177
- element: this.shadowRoot?.querySelector(`li[data-key="${CSS.escape(n.node.item.key)}"]`) ?? void 0
1345
+ element: this.shadowRoot?.querySelector(`ul.tree-list li[data-key="${CSS.escape(n.node.item.key)}"]`) ?? void 0
1178
1346
  }));
1179
1347
  const origin = selected.find((e) => e.key === originKey) ?? {
1180
1348
  key: originKey,
@@ -1329,7 +1497,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1329
1497
  if (this.#rowsCache) return this.#rowsCache;
1330
1498
  const rows = [];
1331
1499
  for (const { node, depth } of this.#getAllNodes()) {
1332
- const li = this.shadowRoot?.querySelector(`li[data-key="${CSS.escape(node.item.key)}"]`);
1500
+ const li = this.shadowRoot?.querySelector(`ul.tree-list li[data-key="${CSS.escape(node.item.key)}"]`);
1333
1501
  if (li) rows.push({
1334
1502
  key: node.item.key,
1335
1503
  node,
@@ -1451,6 +1619,15 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1451
1619
  this.#postDragSuppressTimer = void 0;
1452
1620
  }
1453
1621
  this.#container()?.removeEventListener(`scroll`, this.#onContainerScroll);
1622
+ if (this.#stickyScrollBound) {
1623
+ this.#stickyScrollContainer?.removeEventListener(`scroll`, this.#onStickyScroll);
1624
+ this.#stickyScrollContainer = void 0;
1625
+ this.#stickyScrollBound = false;
1626
+ }
1627
+ if (this.#stickyRaf !== 0) {
1628
+ cancelAnimationFrame(this.#stickyRaf);
1629
+ this.#stickyRaf = 0;
1630
+ }
1454
1631
  treeDragUnregister(this.#dragSourceId);
1455
1632
  if (this.#ownedRegistry) {
1456
1633
  this.#ownedRegistry.unregister(`select-all`);
@@ -1539,6 +1716,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1539
1716
  }
1540
1717
  currentNode = found;
1541
1718
  }
1719
+ this.#stickyDirty = true;
1542
1720
  this.requestUpdate();
1543
1721
  this.#scrollKeyIntoView(keys.at(-1));
1544
1722
  return true;
@@ -1559,6 +1737,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1559
1737
  this._expandedKeys.add(path[i]);
1560
1738
  }
1561
1739
  this.tickled.setTickled(key);
1740
+ this.#stickyDirty = true;
1562
1741
  this.requestUpdate();
1563
1742
  this.updateComplete.then(() => this.#scrollKeyIntoView(key));
1564
1743
  return true;
@@ -1794,7 +1973,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1794
1973
  this.requestUpdate();
1795
1974
  }
1796
1975
  #toggleExpand(node, depth) {
1797
- if (this._expandedKeys.has(node.item.key)) {
1976
+ const isExpanded = this._expandedKeys.has(node.item.key);
1977
+ this.#stickyDirty = true;
1978
+ if (isExpanded) {
1798
1979
  this._expandedKeys.delete(node.item.key);
1799
1980
  this.dispatchEvent(new CustomEvent(`collapse`, {
1800
1981
  detail: {
@@ -1887,6 +2068,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1887
2068
  node.children = children;
1888
2069
  this.#applyExclusivity(node.item.key);
1889
2070
  this._expandedKeys.add(node.item.key);
2071
+ this.#stickyDirty = true;
1890
2072
  this.#pendingNode = void 0;
1891
2073
  this.#pendingController = void 0;
1892
2074
  this.dispatchEvent(new CustomEvent(`expand`, {
@@ -1927,14 +2109,16 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
1927
2109
  }
1928
2110
  #scrollKeyIntoView(key) {
1929
2111
  requestAnimationFrame(() => {
1930
- const el = this.shadowRoot?.querySelector(`[data-key="${CSS.escape(key)}"]`);
1931
- if (!el) return;
2112
+ const container = this.#container();
2113
+ const el = this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(key)}"]`);
2114
+ if (!el || !container) return;
1932
2115
  const elRect = el.getBoundingClientRect();
1933
- const hostRect = this.getBoundingClientRect();
1934
- const relativeTop = elRect.top - hostRect.top;
2116
+ const containerRect = container.getBoundingClientRect();
2117
+ const relativeTop = elRect.top - containerRect.top;
1935
2118
  const relativeBottom = relativeTop + elRect.height;
1936
- if (relativeTop < 0) this.scrollTop += relativeTop;
1937
- else if (relativeBottom > this.clientHeight) this.scrollTop += relativeBottom - this.clientHeight;
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;
1938
2122
  });
1939
2123
  }
1940
2124
  #abortPending() {
@@ -2023,7 +2207,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2023
2207
  #scrollTickledIntoView() {
2024
2208
  const tickledKey = this.tickled.tickledId;
2025
2209
  if (tickledKey === void 0) return;
2026
- (this.shadowRoot?.querySelector(`[data-key="${tickledKey}"]`))?.scrollIntoView({
2210
+ (this.shadowRoot?.querySelector(`ul.tree-list [data-key="${CSS.escape(tickledKey)}"]`))?.scrollIntoView({
2027
2211
  block: `nearest`,
2028
2212
  behavior: `smooth`
2029
2213
  });
@@ -2043,6 +2227,7 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2043
2227
  @pointerleave=${(e) => this.tickled.handlePointerLeave(e)}
2044
2228
  tabindex="0"
2045
2229
  >
2230
+ ${this.#renderStickyHeaders()}
2046
2231
  ${this.root ? this.#renderNodes(nodes) : nothing}
2047
2232
  </div>
2048
2233
  ${this.search.renderField()}
@@ -2141,6 +2326,9 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2141
2326
  --tree-drop-nub-size: 5px;
2142
2327
  --tree-drop-into-outline: 2px solid var(--accent);
2143
2328
  --tree-drop-into-bg: color-mix(in srgb, var(--accent) 14%, transparent);
2329
+ --tree-sticky-bg: var(--tree-bg);
2330
+ --tree-sticky-border: var(--border, transparent);
2331
+ --tree-sticky-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
2144
2332
 
2145
2333
  position: relative;
2146
2334
  display: block;
@@ -2167,6 +2355,28 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2167
2355
  padding: var(--space-xs);
2168
2356
  }
2169
2357
 
2358
+ /* VS Code-style sticky headers: ancestors of the top-visible row, pinned
2359
+ to the top of the scroll container. Rows reuse the item class for
2360
+ identical metrics; the overlay itself is opaque so rows slide under it. */
2361
+ .sticky-headers {
2362
+ position: sticky;
2363
+ top: 0;
2364
+ z-index: 5;
2365
+ background: var(--tree-sticky-bg);
2366
+ border-bottom: 1px solid var(--tree-sticky-border);
2367
+ box-shadow: var(--tree-sticky-shadow);
2368
+ }
2369
+
2370
+ .sticky-header {
2371
+ margin: 0;
2372
+ border-radius: 0;
2373
+ }
2374
+
2375
+ .sticky-header:focus-visible {
2376
+ outline: 2px solid var(--accent);
2377
+ outline-offset: -2px;
2378
+ }
2379
+
2170
2380
  .item {
2171
2381
  position: relative; /* anchor for the .drop-line before/after indicator */
2172
2382
  display: flex;
@@ -2340,6 +2550,14 @@ __decorate([property({
2340
2550
  toAttribute: (value) => value ? null : `false`
2341
2551
  }
2342
2552
  })], TreeBaseElement.prototype, "reorderable", void 0);
2553
+ __decorate([property({
2554
+ reflect: true,
2555
+ attribute: `sticky-headers`,
2556
+ converter: {
2557
+ fromAttribute: (value) => value !== `false`,
2558
+ toAttribute: (value) => value ? null : `false`
2559
+ }
2560
+ })], TreeBaseElement.prototype, "stickyHeaders", void 0);
2343
2561
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewRenderer", void 0);
2344
2562
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "dragPreviewSource", void 0);
2345
2563
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "canDrag", void 0);
@@ -2348,6 +2566,7 @@ __decorate([property({ attribute: false })], TreeBaseElement.prototype, "parseEx
2348
2566
  __decorate([property({ attribute: false })], TreeBaseElement.prototype, "applyInternalDrop", void 0);
2349
2567
  __decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
2350
2568
  __decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
2569
+ __decorate([state()], TreeBaseElement.prototype, "_stickyKeys", void 0);
2351
2570
  TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
2352
2571
  //#endregion
2353
2572
  //#region src/tree/tree-list.ts
@@ -2632,4 +2851,4 @@ var TreeController = class {
2632
2851
  //#endregion
2633
2852
  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 };
2634
2853
 
2635
- //# sourceMappingURL=tree-C25xYUa6.js.map
2854
+ //# sourceMappingURL=tree-Bm2TzTfe.js.map