@ixfx/components 0.15.4 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/bundle/index.d.ts +1223 -48
  2. package/bundle/index.js +875 -700
  3. package/dist/ac-text.d.ts +1 -1
  4. package/dist/{ac-token-Cq7lsYf-.d.ts → ac-token-qEs3FsQv.d.ts} +1 -1
  5. package/dist/ac-token.d.ts +1 -1
  6. package/dist/colour-picker.d.ts +1 -1
  7. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  8. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  9. package/dist/crumbs.d.ts +2 -115
  10. package/dist/crumbs.js +1 -1179
  11. package/dist/{data-grid-CbiubdG2.d.ts → data-grid-CIG2DVip.d.ts} +58 -9
  12. package/dist/{data-grid-Dx3l3dxw.js → data-grid-rMkLX2qo.js} +630 -215
  13. package/dist/data-grid.d.ts +3 -3
  14. package/dist/data-grid.js +1 -1
  15. package/dist/{detail-list-ClkMV0SN.d.ts → detail-list-6iMTNP4N.d.ts} +35 -5
  16. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  17. package/dist/detail-list.d.ts +4 -4
  18. package/dist/detail-list.js +1 -1
  19. package/dist/{flat-list-0xTZdXzh.d.ts → flat-list-JPgQSmLW.d.ts} +28 -5
  20. package/dist/flat-list.d.ts +2 -2
  21. package/dist/flat-list.js +123 -4
  22. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  23. package/dist/{grid-list-BnWJUIBC.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  24. package/dist/grid-list.d.ts +3 -3
  25. package/dist/grid-list.js +1 -1
  26. package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +1 -1
  27. package/dist/grouped-item-lister.d.ts +1 -1
  28. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +1 -1
  29. package/dist/icons.d.ts +2 -2
  30. package/dist/incr-search.d.ts +1 -1
  31. package/dist/index-9ttcdwlL.d.ts +71 -0
  32. package/dist/{index-BAuqR9El.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  33. package/dist/index-CYRc6Dah.d.ts +475 -0
  34. package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +1 -1
  35. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +1 -1
  36. package/dist/{index-Bz3loIAf.d.ts → index-DN2BWctP.d.ts} +1 -1
  37. package/dist/{index-BPTuCRGb.d.ts → index-Dr80Zdl6.d.ts} +2 -2
  38. package/dist/{index-cnaRPQPp.d.ts → index-DxyyVqzA.d.ts} +2 -2
  39. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +1 -1
  40. package/dist/index.d.ts +380 -33
  41. package/dist/index.js +2247 -866
  42. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +1 -1
  43. package/dist/labelled-radial-input.d.ts +2 -2
  44. package/dist/labelled-range-input.d.ts +1 -1
  45. package/dist/labelled-slider-input.d.ts +1 -1
  46. package/dist/list-group-header.d.ts +1 -1
  47. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  48. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  49. package/dist/{menu-item-B0zqw2xp.d.ts → menu-item-bF6narIz.d.ts} +1 -1
  50. package/dist/menu.d.ts +2 -2
  51. package/dist/miller-BFRfn0ve.js +2237 -0
  52. package/dist/miller.d.ts +4 -437
  53. package/dist/miller.js +1 -1815
  54. package/dist/panel.d.ts +1 -1
  55. package/dist/pending-BGidHppL.js +91 -0
  56. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  57. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-CophkNeU.d.ts} +42 -4
  58. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  59. package/dist/split-layout.d.ts +1 -1
  60. package/dist/split-layout.js +1 -1
  61. package/dist/sunburst-tree.d.ts +2 -0
  62. package/dist/sunburst-tree.js +2 -0
  63. package/dist/sunbursttree-list-B_VxOHlo.js +1556 -0
  64. package/dist/sunbursttree-list-D6j5BxVQ.d.ts +303 -0
  65. package/dist/{tab-list-wC0xNz1b.d.ts → tab-list-Df371rqw.d.ts} +1 -1
  66. package/dist/tabs.d.ts +1 -1
  67. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +1 -1
  68. package/dist/titlebar.d.ts +1 -1
  69. package/dist/tree-CftHwtEb.js +146 -0
  70. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  71. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  72. package/dist/tree.d.ts +3 -3
  73. package/dist/tree.js +1 -1
  74. package/dist/types-LU1u0CB8.d.ts +87 -0
  75. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  76. package/dist/{vertical-list-B8FoU-pU.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  77. package/dist/vertical-list.d.ts +3 -3
  78. package/dist/vertical-list.js +1 -1789
  79. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  80. package/docs-user/README.md +3 -0
  81. package/docs-user/crumbs.md +27 -0
  82. package/docs-user/data-grid.md +77 -5
  83. package/docs-user/detail-list.md +54 -0
  84. package/docs-user/dock.md +54 -12
  85. package/docs-user/flat-list.md +27 -0
  86. package/docs-user/grid-list.md +64 -11
  87. package/docs-user/hyperbolic-tree.md +31 -4
  88. package/docs-user/icicle-tree.md +36 -0
  89. package/docs-user/index.json +17 -2
  90. package/docs-user/llms.txt +3 -0
  91. package/docs-user/miller.md +52 -21
  92. package/docs-user/multicol-list.md +50 -8
  93. package/docs-user/split-layout.md +45 -17
  94. package/docs-user/sunburst-tree.md +223 -0
  95. package/docs-user/tree.md +46 -7
  96. package/docs-user/treemap.md +32 -0
  97. package/docs-user/twosplit-layout.md +22 -0
  98. package/docs-user/user-catalog.md +1 -1
  99. package/docs-user/vertical-list.md +55 -7
  100. package/llms.txt +3 -0
  101. package/package.json +5 -1
@@ -5,8 +5,11 @@ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js"
5
5
  import { c as createElementListSearchAdapter, d as createDataListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-JIhwB49y.js";
6
6
  import { i as resolveGroupKey, r as partitionByGroup } from "./grouping-RYPnHkrS.js";
7
7
  import { a as listItemFadeKeyframes, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
8
- import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-BeE70lbB.js";
8
+ import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-5L0Nj9tR.js";
9
+ import { c as keysAsFlags, f as withoutFlag, i as capFlags, l as keysToArray, o as flagsFromRecord, s as flagsToRecord, t as EMPTY_FLAGS } from "./pending-BGidHppL.js";
10
+ import { n as createScrollHoverGuard } from "./scroll-hover-guard-Cjw6179Q.js";
9
11
  import "./interaction-E-uMGHkJ.js";
12
+ import { t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
10
13
  import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
11
14
  import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
12
15
  import "./pan-text-CqtvjPyK.js";
@@ -669,6 +672,90 @@ var DataGridListSource = class {
669
672
  }
670
673
  };
671
674
  //#endregion
675
+ //#region src/grid-list/grid-list-view-state.ts
676
+ /**
677
+ * - live collapsed groups that no longer exist → pending `false`
678
+ * - pending groups that now exist → applied (`false` collapses, `true` opens)
679
+ *
680
+ * Returns the inputs untouched (and `changed: false`) when nothing moved.
681
+ */
682
+ function reconcileGroups(collapsed, pending, has) {
683
+ let nextCollapsed;
684
+ let nextPending;
685
+ for (const key of collapsed) {
686
+ if (has(key)) continue;
687
+ nextCollapsed ??= new Set(collapsed);
688
+ nextCollapsed.delete(key);
689
+ nextPending ??= new Map(pending);
690
+ nextPending.set(key, false);
691
+ }
692
+ for (const [key, open] of pending) {
693
+ if (!has(key)) continue;
694
+ nextPending ??= new Map(pending);
695
+ nextPending.delete(key);
696
+ if (open === (nextCollapsed ?? collapsed).has(key)) {
697
+ nextCollapsed ??= new Set(collapsed);
698
+ if (open) nextCollapsed.delete(key);
699
+ else nextCollapsed.add(key);
700
+ }
701
+ }
702
+ return {
703
+ collapsed: nextCollapsed ?? collapsed,
704
+ pending: nextPending ? capFlags(nextPending) : pending,
705
+ changed: nextCollapsed !== void 0 || nextPending !== void 0
706
+ };
707
+ }
708
+ /** `expanded` record: collapsed groups as `false`, plus pending choices when asked. */
709
+ function expandedRecord(collapsed, pending, withPending) {
710
+ return flagsToRecord(withPending ? pending : [], keysAsFlags(collapsed, false));
711
+ }
712
+ /**
713
+ * Capture the scroll position against the virtual layout. The anchor is the
714
+ * first item of the first line intersecting the viewport, and the offset is
715
+ * how far the viewport start sits past that line's start. `keyAt` returns
716
+ * `undefined` for keys that can't anchor (auto-assigned keyless items).
717
+ */
718
+ function captureGridScroll(layout, mainOffset, crossOffset, keyAt) {
719
+ const base = layout.horizontal ? {
720
+ top: crossOffset,
721
+ left: mainOffset
722
+ } : crossOffset === 0 ? { top: mainOffset } : {
723
+ top: mainOffset,
724
+ left: crossOffset
725
+ };
726
+ const win = layout.window(mainOffset, 0);
727
+ if (win.ranges.length === 0) return base;
728
+ const index = win.startIndex;
729
+ const key = keyAt(index);
730
+ if (key === void 0) return base;
731
+ const lineStart = layout.offsetOfAnchor(index, 0);
732
+ return {
733
+ ...base,
734
+ anchor: {
735
+ key,
736
+ offset: Math.round(mainOffset - lineStart)
737
+ }
738
+ };
739
+ }
740
+ /**
741
+ * Where to scroll for a saved {@link ScrollState}, or `undefined` while the
742
+ * anchor key isn't in the effective order yet. `extent` is the viewport's
743
+ * main-axis size.
744
+ */
745
+ function gridScrollTarget(scroll, layout, indexOfKey, extent) {
746
+ let target = layout.horizontal ? scroll.left ?? 0 : scroll.top;
747
+ if (scroll.anchor) {
748
+ const index = indexOfKey(scroll.anchor.key);
749
+ if (index < 0) return void 0;
750
+ target = layout.offsetOfAnchor(index, 0) + scroll.anchor.offset;
751
+ }
752
+ const max = Math.max(0, layout.totalExtent - extent);
753
+ return {
754
+ offset: Math.max(0, Math.min(target, max)),
755
+ reached: target <= max + 1
756
+ };
757
+ }
758
+ //#endregion
672
759
  //#region src/grid-list/grid-list.ts
673
760
  const ITEM_SELECTOR = `:scope > *`;
674
761
  const MIN_ITEM_SIZE = 16;
@@ -707,7 +794,7 @@ let GridListElement = class GridListElement extends LitElement {
707
794
  this.selectionMode = `single`;
708
795
  this.interactionMode = `standard`;
709
796
  this.disableHoverTickle = false;
710
- this.disableScrollHoverTickle = false;
797
+ this.scrollHoverTickle = false;
711
798
  this.itemSize = 160;
712
799
  this.gap = 8;
713
800
  this.orientation = `grid`;
@@ -730,6 +817,11 @@ let GridListElement = class GridListElement extends LitElement {
730
817
  this._scrollLeft = 0;
731
818
  this._viewportHeight = 0;
732
819
  this._viewportWidth = 0;
820
+ this.#viewState = new ViewStateController(this, {
821
+ kind: `grid-list`,
822
+ capture: () => this.getViewState()
823
+ });
824
+ this.#pendingExpanded = EMPTY_FLAGS;
733
825
  this.#viewportRef = createRef();
734
826
  this.#scrollRaf = 0;
735
827
  this.#snapTimer = 0;
@@ -775,14 +867,24 @@ let GridListElement = class GridListElement extends LitElement {
775
867
  onTickle: (k) => {
776
868
  this.#stopActiveCaptionPan();
777
869
  if (k !== void 0) this.#emitItemEvent(`list-tickle`, k);
870
+ if (this.tickled.inputMode === `keyboard`) {
871
+ if (k !== void 0 && !this.#viewState.isRestoring) this.#pendingFocus = void 0;
872
+ this.#viewState.notify();
873
+ }
778
874
  },
779
875
  onStateChange: () => this._syncItemStates(),
780
876
  itemWrap: this.#itemWrap,
781
877
  hoverDisabled: () => this.disableHoverTickle || this.#dragSelect.isDragging || this.#nativeDrag.isDragging
782
878
  });
783
- this.#scrollHoverSuppress = false;
784
- this.#lastPointerX = NaN;
785
- this.#lastPointerY = NaN;
879
+ this.#scrollHover = createScrollHoverGuard({
880
+ host: this,
881
+ enabled: () => !this.scrollHoverTickle,
882
+ onResume: (x, y) => {
883
+ if (this.disableHoverTickle) return;
884
+ const key = this.shadowRoot?.elementFromPoint?.(x, y)?.closest(`.cell`)?.getAttribute(`data-key`);
885
+ if (key) this.tickled.handlePointerEnter(key);
886
+ }
887
+ });
786
888
  this.#marqueePreview = /* @__PURE__ */ new Set();
787
889
  this.#nativeDragging = false;
788
890
  this.#dragOriginKey = void 0;
@@ -863,12 +965,13 @@ let GridListElement = class GridListElement extends LitElement {
863
965
  }
864
966
  });
865
967
  this.#groupByChanged = false;
968
+ this.#resolveKey = (key) => this.#source.indexOfKey(key) >= 0 ? key : void 0;
866
969
  this.#markedTickled = /* @__PURE__ */ new Set();
867
970
  this.#markedSelected = /* @__PURE__ */ new Set();
868
971
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
869
972
  this.#onScroll = (e) => {
870
973
  const el = e.target;
871
- this.#markScrollHoverSuppress();
974
+ this.#scrollHover.onScroll();
872
975
  this.#cancelScrollRaf();
873
976
  this.#scrollRaf = requestAnimationFrame(() => {
874
977
  this.#scrollRaf = 0;
@@ -877,6 +980,10 @@ let GridListElement = class GridListElement extends LitElement {
877
980
  this.#emitViewportChange();
878
981
  });
879
982
  this.#scheduleSnap();
983
+ const offset = this.orientation === `row` ? el.scrollLeft : el.scrollTop;
984
+ const echo = this.#restoredScrollOffset !== void 0 && Math.abs(offset - this.#restoredScrollOffset) <= 1;
985
+ this.#restoredScrollOffset = void 0;
986
+ if (!echo && !this.#viewState.pendingScroll) this.#viewState.notify();
880
987
  };
881
988
  this.#onWheel = (e) => {
882
989
  if (this.orientation !== `row`) return;
@@ -885,23 +992,10 @@ let GridListElement = class GridListElement extends LitElement {
885
992
  const vp = this.#viewportRef.value ?? e.currentTarget;
886
993
  if (!vp) return;
887
994
  if (vp.scrollWidth <= vp.clientWidth + 1) return;
888
- this.#markScrollHoverSuppress();
995
+ this.#scrollHover.onScroll();
889
996
  vp.scrollLeft += e.deltaX + e.deltaY;
890
997
  e.preventDefault();
891
998
  };
892
- this.#onHostPointerMove = (e) => {
893
- const moved = !Number.isNaN(this.#lastPointerX) && (e.clientX !== this.#lastPointerX || e.clientY !== this.#lastPointerY);
894
- this.#lastPointerX = e.clientX;
895
- this.#lastPointerY = e.clientY;
896
- if (!moved || !this.#scrollHoverSuppress) return;
897
- this.#endScrollHoverSuppress();
898
- if (this.disableHoverTickle) return;
899
- const key = this.shadowRoot?.elementFromPoint?.(e.clientX, e.clientY)?.closest(`.cell`)?.getAttribute(`data-key`);
900
- if (key) this.tickled.handlePointerEnter(key);
901
- };
902
- this.#onHostPointerLeave = () => {
903
- this.#endScrollHoverSuppress();
904
- };
905
999
  this._onKeyDown = (e) => {
906
1000
  if ((e.ctrlKey || e.metaKey) && e.key === `f`) return;
907
1001
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
@@ -1210,7 +1304,7 @@ let GridListElement = class GridListElement extends LitElement {
1210
1304
  }
1211
1305
  :host(:not([caption-style="separate"])) .cell.tickled,
1212
1306
  /* [data-scroll-hover-suppress] is set while a scroll gesture moves
1213
- cells beneath a stationary pointer (see disable-scroll-hover-tickle):
1307
+ cells beneath a stationary pointer (see scroll-hover-tickle):
1214
1308
  the :hover highlight is gated so the visual cursor holds still too. */
1215
1309
  :host(:not([caption-style="separate"]):not([disable-hover-tickle]):not([data-scroll-hover-suppress])) .cell:hover {
1216
1310
  background: var(--item-bg-tickled);
@@ -1390,6 +1484,13 @@ let GridListElement = class GridListElement extends LitElement {
1390
1484
  `
1391
1485
  ];
1392
1486
  }
1487
+ #viewState;
1488
+ /** Saved open / closed choices for groups not currently present. */
1489
+ #pendingExpanded;
1490
+ /** Saved keyboard-cursor key, waiting for its item to appear. */
1491
+ #pendingFocus;
1492
+ /** Main-axis offset the last scroll restore wrote, so its `scroll` echo isn't reported as `user`. */
1493
+ #restoredScrollOffset;
1393
1494
  #viewportRef;
1394
1495
  #resizeObserver;
1395
1496
  #mutationObserver;
@@ -1449,16 +1550,8 @@ let GridListElement = class GridListElement extends LitElement {
1449
1550
  */
1450
1551
  #sourceElement;
1451
1552
  #source;
1452
- /**
1453
- * True while cell `pointerenter` tickling is suppressed by an active or
1454
- * just-finished scroll gesture (`disable-scroll-hover-tickle`). While set,
1455
- * the host carries `[data-scroll-hover-suppress]`, which gates the CSS
1456
- * `:hover` highlight so the visual cursor does not churn either.
1457
- */
1458
- #scrollHoverSuppress;
1459
- /** Last pointer position seen over the host; real movement ends suppression. */
1460
- #lastPointerX;
1461
- #lastPointerY;
1553
+ /** Holds the cursor still while a scroll gesture moves cells beneath a stationary pointer. */
1554
+ #scrollHover;
1462
1555
  /** Live drag-select preview: covered keys + the surface-relative rect. */
1463
1556
  #marqueePreview;
1464
1557
  #marqueeRect;
@@ -1610,8 +1703,9 @@ let GridListElement = class GridListElement extends LitElement {
1610
1703
  [`caption-backdrop`, this.captionBackdrop]
1611
1704
  ]) if (!this.hasAttribute(attr)) this.setAttribute(attr, value);
1612
1705
  this.addEventListener(`keydown`, this._onKeyDown);
1613
- this.addEventListener(`pointermove`, this.#onHostPointerMove, { passive: true });
1614
- this.addEventListener(`pointerleave`, this.#onHostPointerLeave);
1706
+ this.addEventListener(`pointermove`, this.#scrollHover.onPointerMove, { passive: true });
1707
+ this.addEventListener(`pointerleave`, this.#scrollHover.onPointerLeave);
1708
+ this.#viewState.observeScrollTarget(this);
1615
1709
  this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
1616
1710
  this.#mutationObserver.observe(this, { childList: true });
1617
1711
  this.#itemAttrObserver ??= new MutationObserver((records) => this.#onItemAttributesChanged(records));
@@ -1631,9 +1725,9 @@ let GridListElement = class GridListElement extends LitElement {
1631
1725
  super.disconnectedCallback();
1632
1726
  this.#stopActiveCaptionPan();
1633
1727
  this.removeEventListener(`keydown`, this._onKeyDown);
1634
- this.removeEventListener(`pointermove`, this.#onHostPointerMove);
1635
- this.removeEventListener(`pointerleave`, this.#onHostPointerLeave);
1636
- this.#endScrollHoverSuppress();
1728
+ this.removeEventListener(`pointermove`, this.#scrollHover.onPointerMove);
1729
+ this.removeEventListener(`pointerleave`, this.#scrollHover.onPointerLeave);
1730
+ this.#scrollHover.end();
1637
1731
  this.#resizeObserver?.disconnect();
1638
1732
  this.#resizeObserver = void 0;
1639
1733
  this.#mutationObserver?.disconnect();
@@ -1665,10 +1759,11 @@ let GridListElement = class GridListElement extends LitElement {
1665
1759
  if (removedKeys.length > 0) {
1666
1760
  const present = new Set(this.#source.keys());
1667
1761
  const gone = removedKeys.filter((k) => !present.has(k));
1668
- if (gone.length > 0) this.selection.removeKeys(gone, (k) => k);
1762
+ if (gone.length > 0) this.#demoteGone(gone);
1669
1763
  const tickledId = this.tickled.tickledId;
1670
- if (tickledId && gone.includes(tickledId)) this.tickled.focus(void 0);
1764
+ if (tickledId && gone.includes(tickledId)) this.#demoteFocus(tickledId);
1671
1765
  }
1766
+ this.#reconcileViewState();
1672
1767
  this._syncItemStates(true);
1673
1768
  this.#noteChildMutation(previousLength, records);
1674
1769
  this.requestUpdate();
@@ -1753,6 +1848,7 @@ let GridListElement = class GridListElement extends LitElement {
1753
1848
  this.#resizeObserver.observe(el);
1754
1849
  this.#measureViewport(false);
1755
1850
  this.#syncItems();
1851
+ this.#reconcileViewState();
1756
1852
  this.requestUpdate();
1757
1853
  this.#nativeDrag.refreshDraggable();
1758
1854
  }
@@ -1788,7 +1884,8 @@ let GridListElement = class GridListElement extends LitElement {
1788
1884
  this.#applyFilters();
1789
1885
  this.updateComplete.then(() => this.#emitViewportChange());
1790
1886
  }
1791
- if (changed.has(`items`) || changed.has(`itemAdapter`)) {
1887
+ const dataModeChanged = changed.has(`items`) || changed.has(`itemAdapter`);
1888
+ if (dataModeChanged) {
1792
1889
  this.#zoomAnchor = void 0;
1793
1890
  const prevKeys = this.#source?.keys() ?? [];
1794
1891
  const modeFlipped = this.itemAdapter !== void 0 !== this.#source instanceof DataGridListSource;
@@ -1796,9 +1893,9 @@ let GridListElement = class GridListElement extends LitElement {
1796
1893
  const nextKeys = this.#source.keys();
1797
1894
  const nextKeySet = new Set(nextKeys);
1798
1895
  const removedKeys = prevKeys.filter((k) => !nextKeySet.has(k));
1799
- if (removedKeys.length > 0) this.selection.removeKeys(removedKeys, (k) => k);
1896
+ if (removedKeys.length > 0) this.#demoteGone(removedKeys);
1800
1897
  const tickledId = this.tickled.tickledId;
1801
- if (tickledId && !nextKeySet.has(tickledId)) this.tickled.focus(void 0);
1898
+ if (tickledId && !nextKeySet.has(tickledId)) this.#demoteFocus(tickledId);
1802
1899
  this._syncItemStates();
1803
1900
  this.updateComplete.then(() => this.#emitViewportChange());
1804
1901
  if (modeFlipped) {
@@ -1837,6 +1934,7 @@ let GridListElement = class GridListElement extends LitElement {
1837
1934
  this.#captureCentreAnchor(prevOffset, prevExtent, prevItemSize, prevGap, prevCaptionStyle, prevCaptionHeight, prevOrientation);
1838
1935
  }
1839
1936
  }
1937
+ if (dataModeChanged || groupsChanged || changed.has(`filterPredicate`) || changed.has(`itemFilterPredicate`)) this.#reconcileViewState();
1840
1938
  }
1841
1939
  #groupByChanged;
1842
1940
  /** Grouping config for the active source, or `undefined` when ungrouped. */
@@ -1880,6 +1978,8 @@ let GridListElement = class GridListElement extends LitElement {
1880
1978
  if (collapsed) next.add(groupKey);
1881
1979
  else next.delete(groupKey);
1882
1980
  this._collapsedGroups = next;
1981
+ this.#pendingExpanded = withoutFlag(this.#pendingExpanded, groupKey);
1982
+ this.#viewState.notify();
1883
1983
  this._dispatch(`list-group-toggle`, {
1884
1984
  groupKey,
1885
1985
  collapsed
@@ -1916,7 +2016,7 @@ let GridListElement = class GridListElement extends LitElement {
1916
2016
  this.selection.clearAnchor();
1917
2017
  this.#syncAriaMultiselectable();
1918
2018
  }
1919
- if (changed.has(`disableScrollHoverTickle`) && !this.disableScrollHoverTickle) this.#endScrollHoverSuppress();
2019
+ if (changed.has(`scrollHoverTickle`) && this.scrollHoverTickle) this.#scrollHover.end();
1920
2020
  if (changed.has(`itemsDraggable`)) this.#nativeDrag.refreshDraggable();
1921
2021
  if (changed.has(`orientation`)) {
1922
2022
  const vp = this.#viewportRef.value;
@@ -1936,6 +2036,7 @@ let GridListElement = class GridListElement extends LitElement {
1936
2036
  if (Math.abs(vp.scrollLeft - this._scrollLeft) > .5) vp.scrollLeft = this._scrollLeft;
1937
2037
  }
1938
2038
  this.#applyZoomAnchor();
2039
+ this.#viewState.applyPendingScroll((scroll) => this.#applyScroll(scroll));
1939
2040
  this.#activateTickledPan();
1940
2041
  if (this.#groupByChanged) {
1941
2042
  this.#groupByChanged = false;
@@ -2099,8 +2200,9 @@ let GridListElement = class GridListElement extends LitElement {
2099
2200
  }
2100
2201
  /**
2101
2202
  * Data mode: replace the whole item array. Selection and cursor keep any key
2102
- * that is still present; keys that vanish are dropped (one `list-select`).
2103
- * A no-op in element mode.
2203
+ * that is still present; keys that vanish are deselected silently and held
2204
+ * as pending view state, so they come back if the key reappears. A no-op in
2205
+ * element mode.
2104
2206
  */
2105
2207
  setItems(items) {
2106
2208
  if (this.itemAdapter) this.items = items;
@@ -2128,8 +2230,9 @@ let GridListElement = class GridListElement extends LitElement {
2128
2230
  this.items = next;
2129
2231
  }
2130
2232
  /**
2131
- * Data mode: remove the items carrying these keys. Removed keys are dropped
2132
- * from selection and cursor (via the `items` change). A no-op in element mode.
2233
+ * Data mode: remove the items carrying these keys. Removed keys leave the
2234
+ * selection and cursor silently (via the `items` change) and are held as
2235
+ * pending view state. A no-op in element mode.
2133
2236
  */
2134
2237
  removeItems(keys) {
2135
2238
  if (!this.itemAdapter) return;
@@ -2139,18 +2242,22 @@ let GridListElement = class GridListElement extends LitElement {
2139
2242
  }
2140
2243
  /**
2141
2244
  * Data mode: drop every item. Selection and cursor are reset and one
2142
- * `list-select` with empty sets is emitted. A no-op in element mode.
2245
+ * `list-select` with empty sets is emitted; their keys are held as pending
2246
+ * view state, so re-adding the same keys restores them. A no-op in element
2247
+ * mode.
2143
2248
  */
2144
2249
  clearItems() {
2145
2250
  if (!this.itemAdapter) return;
2146
2251
  this.selection.reset();
2147
- this.tickled.focus(void 0);
2252
+ const tickled = this.tickled.tickledId;
2253
+ if (tickled !== void 0) this.#demoteFocus(tickled);
2148
2254
  this.items = [];
2149
2255
  this.#emitSelect(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2150
2256
  }
2151
2257
  /** Move the cursor to the item with this key and scroll it into view. */
2152
2258
  focusKey(key) {
2153
2259
  if (this.#source.indexOfKey(key) < 0) return;
2260
+ this.#pendingFocus = void 0;
2154
2261
  this.tickled.focus(key);
2155
2262
  this.#scrollItemIntoView(key);
2156
2263
  }
@@ -2158,6 +2265,121 @@ let GridListElement = class GridListElement extends LitElement {
2158
2265
  get tickledKey() {
2159
2266
  return this.tickled.tickledId;
2160
2267
  }
2268
+ /**
2269
+ * Snapshot selection, collapsed groups, keyboard cursor and scroll as plain
2270
+ * JSON, keyed by item / group key. Includes pending state for keys not
2271
+ * currently present unless `includePending: false`.
2272
+ */
2273
+ getViewState(options) {
2274
+ const withPending = options?.includePending ?? true;
2275
+ const tickled = this.tickled.tickledId;
2276
+ const focus = this.tickled.inputMode === `keyboard` && tickled !== void 0 && !isSyntheticKey(tickled) ? tickled : void 0;
2277
+ return this.#viewState.shape({
2278
+ selected: keysToArray(this.selectedKeys, withPending ? [...this.selection.pendingKeys].filter((k) => !isSyntheticKey(k)) : []),
2279
+ expanded: expandedRecord(this._collapsedGroups, this.#pendingExpanded, withPending),
2280
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0),
2281
+ scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? this.#captureScroll()
2282
+ }, options);
2283
+ }
2284
+ /**
2285
+ * Apply saved view state. Fields present replace the current ones; keys
2286
+ * (items or groups) that aren't present yet are applied as they appear.
2287
+ * Restored selection fires `list-select`.
2288
+ */
2289
+ setViewState(state) {
2290
+ if (!this.#viewState.accepts(state)) return;
2291
+ this.#viewState.restoring(() => {
2292
+ if (state.expanded) {
2293
+ this.#pendingExpanded = capFlags(flagsFromRecord(state.expanded));
2294
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
2295
+ }
2296
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
2297
+ if (state.scroll) this.#viewState.pendingScroll = state.scroll;
2298
+ this.#reconcileGroups();
2299
+ if (state.selected) this.selection.restoreKeys(state.selected, this.#resolveKey);
2300
+ this.#reconcileViewState();
2301
+ });
2302
+ this.requestUpdate();
2303
+ }
2304
+ /** Drop view state still waiting for its keys to appear. */
2305
+ clearPendingViewState() {
2306
+ this.selection.clearPending();
2307
+ this.#pendingExpanded = EMPTY_FLAGS;
2308
+ this.#pendingFocus = void 0;
2309
+ this.#viewState.pendingScroll = void 0;
2310
+ }
2311
+ #resolveKey;
2312
+ /** Move state for missing keys to pending; apply pending state for keys now present. */
2313
+ #reconcileViewState() {
2314
+ this.#viewState.restoring(() => {
2315
+ this.#reconcileGroups();
2316
+ this.selection.resolvePending(this.#resolveKey);
2317
+ const focus = this.#pendingFocus;
2318
+ if (focus !== void 0 && this.#source.indexOfKey(focus) >= 0) {
2319
+ this.tickled.setInputMode(`keyboard`);
2320
+ this.tickled.focus(focus);
2321
+ this.#pendingFocus = void 0;
2322
+ }
2323
+ });
2324
+ }
2325
+ #reconcileGroups() {
2326
+ if (this._collapsedGroups.size === 0 && this.#pendingExpanded.size === 0) return;
2327
+ const present = this.groupBy === null ? /* @__PURE__ */ new Set() : new Set(this.#source.groups().map((g) => g.key));
2328
+ const r = reconcileGroups(this._collapsedGroups, this.#pendingExpanded, (key) => present.has(key));
2329
+ if (!r.changed) return;
2330
+ this.#pendingExpanded = r.pending;
2331
+ if (r.collapsed !== this._collapsedGroups) {
2332
+ this._collapsedGroups = r.collapsed;
2333
+ this.#applyGroupingToSource();
2334
+ this.#dropUnreachableCursor();
2335
+ this.#viewState.notify(`data`);
2336
+ }
2337
+ }
2338
+ /** Items left the data: deselect them silently, keeping their keys pending. */
2339
+ #demoteGone(keys) {
2340
+ const synthetic = keys.filter(isSyntheticKey);
2341
+ if (this.selection.demote(keys.filter((k) => !isSyntheticKey(k)))) {
2342
+ this._syncItemStates();
2343
+ this.#viewState.notify(`data`);
2344
+ }
2345
+ if (synthetic.length > 0) this.selection.removeKeys(synthetic, (k) => k);
2346
+ }
2347
+ /** The cursor's item left the data: clear it, remembering a keyboard cursor. */
2348
+ #demoteFocus(key) {
2349
+ if (this.tickled.inputMode === `keyboard` && !isSyntheticKey(key)) this.#pendingFocus ??= key;
2350
+ this.tickled.focus(void 0);
2351
+ }
2352
+ #captureScroll() {
2353
+ const vp = this.#viewportRef.value;
2354
+ if (!vp) return void 0;
2355
+ const horizontal = this.orientation === `row`;
2356
+ const main = horizontal ? vp.scrollLeft : vp.scrollTop;
2357
+ const cross = horizontal ? vp.scrollTop : vp.scrollLeft;
2358
+ const keys = this.#source.keys();
2359
+ return captureGridScroll(this.#layout(), main, cross, (i) => {
2360
+ const key = keys[i];
2361
+ return key === void 0 || isSyntheticKey(key) ? void 0 : key;
2362
+ });
2363
+ }
2364
+ /** Returns `true` once the saved position is fully reached. */
2365
+ #applyScroll(scroll) {
2366
+ const vp = this.#viewportRef.value;
2367
+ const horizontal = this.orientation === `row`;
2368
+ const extent = horizontal ? this._viewportWidth : this._viewportHeight;
2369
+ if (!vp || extent <= 0) return false;
2370
+ const target = gridScrollTarget(scroll, this.#layout(), (key) => this.#source.indexOfKey(key), extent);
2371
+ if (!target) return false;
2372
+ if (horizontal) {
2373
+ if (Math.abs(vp.scrollLeft - target.offset) > .5) this.#restoredScrollOffset = target.offset;
2374
+ vp.scrollLeft = target.offset;
2375
+ this._scrollLeft = target.offset;
2376
+ } else {
2377
+ if (Math.abs(vp.scrollTop - target.offset) > .5) this.#restoredScrollOffset = target.offset;
2378
+ vp.scrollTop = target.offset;
2379
+ this._scrollTop = target.offset;
2380
+ }
2381
+ return target.reached;
2382
+ }
2161
2383
  /** Element mode: the light-DOM element carrying `key`, or `undefined`. */
2162
2384
  getItem(key) {
2163
2385
  return this.#source.resolveKeyToElement(key);
@@ -2296,6 +2518,7 @@ let GridListElement = class GridListElement extends LitElement {
2296
2518
  this.setAttribute(`data-input-mode`, this.tickled.inputMode);
2297
2519
  }
2298
2520
  #emitSelect(selected, previous) {
2521
+ this.#viewState.notify();
2299
2522
  const selectedEls = /* @__PURE__ */ new Set();
2300
2523
  const previousEls = /* @__PURE__ */ new Set();
2301
2524
  for (const key of selected) {
@@ -2592,35 +2815,13 @@ let GridListElement = class GridListElement extends LitElement {
2592
2815
  #onWheel;
2593
2816
  /**
2594
2817
  * Cell `pointerenter` funnel. While a scroll gesture moves cells beneath a
2595
- * stationary pointer (`disable-scroll-hover-tickle`), the freshly
2596
- * virtualised cell's enter event is ignored so the cursor holds still.
2818
+ * stationary pointer (see `scrollHoverTickle`), the freshly virtualised
2819
+ * cell's enter event is ignored so the cursor holds still.
2597
2820
  */
2598
2821
  #handleCellPointerEnter(key) {
2599
- if (this.#scrollHoverSuppress) return;
2822
+ if (this.#scrollHover.suppressed()) return;
2600
2823
  this.tickled.handlePointerEnter(key);
2601
2824
  }
2602
- /** Open the scroll-hover suppression window (no-op unless opted in and not already open). */
2603
- #markScrollHoverSuppress() {
2604
- if (!this.disableScrollHoverTickle || this.#scrollHoverSuppress) return;
2605
- this.#scrollHoverSuppress = true;
2606
- this.toggleAttribute(`data-scroll-hover-suppress`, true);
2607
- }
2608
- #endScrollHoverSuppress() {
2609
- if (!this.#scrollHoverSuppress) return;
2610
- this.#scrollHoverSuppress = false;
2611
- this.toggleAttribute(`data-scroll-hover-suppress`, false);
2612
- }
2613
- /**
2614
- * A real pointer movement ends scroll-hover suppression. The first
2615
- * `pointerenter` on a real move is swallowed by design (it can be
2616
- * indistinguishable from a virtualisation enter), so the cell already under
2617
- * the pointer is re-tickled here — otherwise the cursor would lag one cell
2618
- * boundary behind the pointer. The very first tracked `pointermove` only
2619
- * establishes the baseline; it cannot know the pointer moved.
2620
- */
2621
- #onHostPointerMove;
2622
- /** The pointer leaving the list ends suppression, so re-entry hovers normally. */
2623
- #onHostPointerLeave;
2624
2825
  #cancelScrollRaf() {
2625
2826
  if (this.#scrollRaf !== 0) {
2626
2827
  cancelAnimationFrame(this.#scrollRaf);
@@ -2718,9 +2919,9 @@ __decorate([property({
2718
2919
  })], GridListElement.prototype, "disableHoverTickle", void 0);
2719
2920
  __decorate([property({
2720
2921
  type: Boolean,
2721
- attribute: `disable-scroll-hover-tickle`,
2922
+ attribute: `scroll-hover-tickle`,
2722
2923
  reflect: true
2723
- })], GridListElement.prototype, "disableScrollHoverTickle", void 0);
2924
+ })], GridListElement.prototype, "scrollHoverTickle", void 0);
2724
2925
  __decorate([property({
2725
2926
  type: Number,
2726
2927
  attribute: `item-size`