@ixfx/components 0.10.3 → 0.10.4

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 (98) hide show
  1. package/bundle/index.d.ts +76 -28
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +220 -5
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  7. package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  8. package/dist/ac-token.d.ts +1 -1
  9. package/dist/{button-DsFbV4XD.js → button-B7mgqtxR.js} +36 -2
  10. package/dist/button-B7mgqtxR.js.map +1 -0
  11. package/dist/button-BYuf72aR.d.ts +63 -0
  12. package/dist/button-BYuf72aR.d.ts.map +1 -0
  13. package/dist/button.d.ts +2 -21
  14. package/dist/button.js +1 -1
  15. package/dist/colour-picker.d.ts +1 -1
  16. package/dist/crumbs.d.ts +1 -1
  17. package/dist/{data-grid-f9W2uzaR.d.ts → data-grid-DE1_gZNk.d.ts} +10 -5
  18. package/dist/data-grid-DE1_gZNk.d.ts.map +1 -0
  19. package/dist/data-grid.d.ts +2 -2
  20. package/dist/data-grid.js +56 -0
  21. package/dist/data-grid.js.map +1 -1
  22. package/dist/{detail-list-C3tozzBT.d.ts → detail-list-CaStSzYN.d.ts} +7 -5
  23. package/dist/detail-list-CaStSzYN.d.ts.map +1 -0
  24. package/dist/detail-list.d.ts +4 -3
  25. package/dist/detail-list.js +61 -1
  26. package/dist/detail-list.js.map +1 -1
  27. package/dist/{grid-list-CM1XhTaW.d.ts → grid-list-BJDxBL0x.d.ts} +3 -3
  28. package/dist/{grid-list-CM1XhTaW.d.ts.map → grid-list-BJDxBL0x.d.ts.map} +1 -1
  29. package/dist/{grid-list-WE92nfX5.js → grid-list-CaMcdpFJ.js} +22 -3
  30. package/dist/grid-list-CaMcdpFJ.js.map +1 -0
  31. package/dist/grid-list.d.ts +2 -2
  32. package/dist/grid-list.js +1 -1
  33. package/dist/{grouped-item-lister-CEZ72VOs.d.ts → grouped-item-lister-DETte0Dh.d.ts} +3 -3
  34. package/dist/{grouped-item-lister-CEZ72VOs.d.ts.map → grouped-item-lister-DETte0Dh.d.ts.map} +1 -1
  35. package/dist/grouped-item-lister.d.ts +1 -1
  36. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
  37. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  38. package/dist/icons.d.ts +2 -2
  39. package/dist/incr-search.d.ts +1 -1
  40. package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
  41. package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  42. package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +2 -2
  43. package/dist/{index-garxxqgV.d.ts.map → index-CYndvTWH.d.ts.map} +1 -1
  44. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
  45. package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
  46. package/dist/{index-BXafRK-_.d.ts → index-CsaQwNkz.d.ts} +2 -2
  47. package/dist/{index-BXafRK-_.d.ts.map → index-CsaQwNkz.d.ts.map} +1 -1
  48. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
  49. package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
  50. package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
  51. package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
  52. package/dist/index.d.ts +21 -39
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -2
  55. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
  56. package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
  57. package/dist/labelled-radial-input.d.ts +2 -2
  58. package/dist/labelled-radial-input.d.ts.map +1 -1
  59. package/dist/labelled-range-input.d.ts +1 -1
  60. package/dist/labelled-slider-input.d.ts +1 -1
  61. package/dist/{list-selection-types-B5ioL-dG.d.ts → list-selection-types-PjKdhF-a.d.ts} +1 -9
  62. package/dist/list-selection-types-PjKdhF-a.d.ts.map +1 -0
  63. package/dist/list-viewport-types-BRZsLcvp.d.ts +23 -0
  64. package/dist/list-viewport-types-BRZsLcvp.d.ts.map +1 -0
  65. package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
  66. package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
  67. package/dist/menu.d.ts +2 -2
  68. package/dist/miller.d.ts +1 -1
  69. package/dist/panel.d.ts +1 -1
  70. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
  71. package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
  72. package/dist/split-layout.d.ts +1 -1
  73. package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
  74. package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
  75. package/dist/tabs.d.ts +1 -1
  76. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +2 -2
  77. package/dist/{titlebar-B5zSTZnF.d.ts.map → titlebar-Tqu-VvMU.d.ts.map} +1 -1
  78. package/dist/titlebar.d.ts +1 -1
  79. package/dist/tree.d.ts +1 -1
  80. package/dist/{vertical-list-DdaYcvHQ.d.ts → vertical-list-DfcNsa4p.d.ts} +11 -3
  81. package/dist/vertical-list-DfcNsa4p.d.ts.map +1 -0
  82. package/dist/vertical-list.d.ts +4 -3
  83. package/dist/vertical-list.js +47 -1
  84. package/dist/vertical-list.js.map +1 -1
  85. package/docs-user/data-grid.md +9 -0
  86. package/docs-user/detail-list.md +8 -0
  87. package/docs-user/grid-list.md +1 -1
  88. package/docs-user/index.json +2 -2
  89. package/docs-user/vertical-list.md +7 -0
  90. package/package.json +1 -1
  91. package/dist/button-DsFbV4XD.js.map +0 -1
  92. package/dist/button.d.ts.map +0 -1
  93. package/dist/data-grid-f9W2uzaR.d.ts.map +0 -1
  94. package/dist/detail-list-C3tozzBT.d.ts.map +0 -1
  95. package/dist/grid-list-WE92nfX5.js.map +0 -1
  96. package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
  97. package/dist/list-selection-types-B5ioL-dG.d.ts.map +0 -1
  98. package/dist/vertical-list-DdaYcvHQ.d.ts.map +0 -1
package/bundle/index.js CHANGED
@@ -5715,6 +5715,20 @@ const buttonStyles = i$3`
5715
5715
  :host([subtle]) button:focus-visible:not(:disabled) {
5716
5716
  background: color-mix(in oklch, var(--surface-4, #ccc) 70%, transparent);
5717
5717
  }
5718
+
5719
+ :host([toggleable]) button:not(:disabled) {
5720
+ opacity: 0.7;
5721
+ }
5722
+
5723
+ :host([toggleable]) button:hover:not(:disabled) {
5724
+ opacity: 0.85;
5725
+ }
5726
+
5727
+ :host([toggleable][value]) button:not(:disabled) {
5728
+ opacity: 1;
5729
+ outline: var(--ixfx-button-toggle-outline-width, 2px) solid var(--ixfx-button-toggle-outline-color, oklch(from var(--accent, #666) l c calc(h + 130)));
5730
+ outline-offset: var(--ixfx-button-toggle-outline-offset, 2px);
5731
+ }
5718
5732
  `;
5719
5733
  //#endregion
5720
5734
  //#region src/button/button.ts
@@ -5726,7 +5740,19 @@ let ButtonElement = class ButtonElement extends i {
5726
5740
  this.hideLabel = false;
5727
5741
  this.hasIcon = false;
5728
5742
  this.subtle = false;
5743
+ this.toggleable = false;
5744
+ this.value = false;
5745
+ this.#handleClick = () => {
5746
+ if (!this.toggleable || this.disabled) return;
5747
+ this.value = !this.value;
5748
+ this.dispatchEvent(new CustomEvent(`toggle`, {
5749
+ detail: { value: this.value },
5750
+ bubbles: true,
5751
+ composed: true
5752
+ }));
5753
+ };
5729
5754
  }
5755
+ #handleClick;
5730
5756
  _renderIcon() {
5731
5757
  if (this.hideIcon || !this.icon) return A;
5732
5758
  if (this.icon.startsWith(`<svg`)) return b`<span class="button-icon">${unsafeHTML(this.icon)}</span>`;
@@ -5736,7 +5762,7 @@ let ButtonElement = class ButtonElement extends i {
5736
5762
  render() {
5737
5763
  this.hasIcon = !this.hideIcon && !!this.icon;
5738
5764
  const label = this.hideLabel ? A : b`<span class="button-label"><slot></slot></span>`;
5739
- return b`<button ?disabled=${this.disabled}>${this._renderIcon()}${label}</button>`;
5765
+ return b`<button ?disabled=${this.disabled} @click=${this.#handleClick}>${this._renderIcon()}${label}</button>`;
5740
5766
  }
5741
5767
  static {
5742
5768
  this.styles = [prominenceStyles, buttonStyles];
@@ -5763,6 +5789,14 @@ __decorate([property({
5763
5789
  type: Boolean,
5764
5790
  reflect: true
5765
5791
  })], ButtonElement.prototype, "subtle", void 0);
5792
+ __decorate([property({
5793
+ type: Boolean,
5794
+ reflect: true
5795
+ })], ButtonElement.prototype, "toggleable", void 0);
5796
+ __decorate([property({
5797
+ type: Boolean,
5798
+ reflect: true
5799
+ })], ButtonElement.prototype, "value", void 0);
5766
5800
  ButtonElement = __decorate([safeCustomElement(`ixfx-button`)], ButtonElement);
5767
5801
  //#endregion
5768
5802
  //#region src/button/radio-group.ts
@@ -14627,6 +14661,12 @@ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
14627
14661
  PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
14628
14662
  //#endregion
14629
14663
  //#region src/data-grid/data-grid.ts
14664
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
14665
+ function sameKeys$3(a, b) {
14666
+ if (a === void 0 || a.length !== b.length) return false;
14667
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
14668
+ return true;
14669
+ }
14630
14670
  let DataGridElement = class DataGridElement extends i {
14631
14671
  constructor(..._args) {
14632
14672
  super(..._args);
@@ -14719,6 +14759,7 @@ let DataGridElement = class DataGridElement extends i {
14719
14759
  this._colOffsets = [];
14720
14760
  this._colWidthSum = 0;
14721
14761
  this._raf = 0;
14762
+ this.#viewportChangeRaf = 0;
14722
14763
  this.#appendInProgress = false;
14723
14764
  this.#resortRaf = 0;
14724
14765
  this._drag = null;
@@ -14740,6 +14781,9 @@ let DataGridElement = class DataGridElement extends i {
14740
14781
  this._scrollLeft = el.scrollLeft;
14741
14782
  if (this._headerScrollEl) this._headerScrollEl.scrollLeft = el.scrollLeft;
14742
14783
  this.requestUpdate();
14784
+ cancelAnimationFrame(this.#viewportChangeRaf);
14785
+ this.#viewportChangeRaf = 0;
14786
+ this.#emitViewportChangeIfChanged();
14743
14787
  });
14744
14788
  };
14745
14789
  this._onKeyDown = (e) => {
@@ -15173,6 +15217,10 @@ let DataGridElement = class DataGridElement extends i {
15173
15217
  #enteringRowKeys;
15174
15218
  #rowReflowTimer;
15175
15219
  #dataGridRenderedOnce;
15220
+ #viewportChangeRaf;
15221
+ /** Key sets as of the last `datagrid-viewport-change` dispatch, for dedupe. */
15222
+ #lastViewportRenderedKeys;
15223
+ #lastViewportVisibleKeys;
15176
15224
  #appendInProgress;
15177
15225
  #resortRaf;
15178
15226
  connectedCallback() {
@@ -15188,6 +15236,8 @@ let DataGridElement = class DataGridElement extends i {
15188
15236
  this.removeEventListener(`keydown`, this._onKeyDown);
15189
15237
  this.removeEventListener(`focusout`, this._onFocusOut);
15190
15238
  cancelAnimationFrame(this._raf);
15239
+ cancelAnimationFrame(this.#viewportChangeRaf);
15240
+ this.#viewportChangeRaf = 0;
15191
15241
  this.#unregisterSearchCommands?.();
15192
15242
  this.#unregisterSearchCommands = void 0;
15193
15243
  this.#searchRegistry = void 0;
@@ -15228,6 +15278,7 @@ let DataGridElement = class DataGridElement extends i {
15228
15278
  if (w === this._viewportW && h === this._viewportH) return;
15229
15279
  this._viewportW = w;
15230
15280
  this._viewportH = h;
15281
+ this.#scheduleViewportChangeEmit();
15231
15282
  }
15232
15283
  /**
15233
15284
  * Force a fresh re-measure of the viewport and re-render. Normally
@@ -15242,6 +15293,7 @@ let DataGridElement = class DataGridElement extends i {
15242
15293
  }
15243
15294
  updated(changed) {
15244
15295
  super.updated(changed);
15296
+ if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`filterPredicate`) || changed.has(`rowHeight`)) this.#scheduleViewportChangeEmit();
15245
15297
  if (changed.has(`_editing`) || changed.has(`_editingRow`)) requestAnimationFrame(() => {
15246
15298
  let sel = null;
15247
15299
  if (this._editing) sel = `.row[data-key="${this._editing.rowKey}"] .cell[data-col="${this._editing.colKey}"] input, .row[data-key="${this._editing.rowKey}"] .cell[data-col="${this._editing.colKey}"] select`;
@@ -16021,6 +16073,44 @@ let DataGridElement = class DataGridElement extends i {
16021
16073
  const { startRow, endRow } = this.getVisibleRange();
16022
16074
  return this._flat.slice(startRow, endRow).filter((f) => f.type === `row`).map((f) => f.key);
16023
16075
  }
16076
+ /**
16077
+ * Row keys strictly intersecting the viewport (no overscan), mirroring
16078
+ * `ixfx-grid-list.visibleKeys()` for the `datagrid-viewport-change` detail.
16079
+ */
16080
+ #visibleKeys() {
16081
+ const rh = this.rowHeight;
16082
+ const startRow = Math.max(0, Math.floor(this._scrollTop / rh));
16083
+ const endRow = Math.min(this._flat.length, Math.ceil((this._scrollTop + this._viewportH) / rh));
16084
+ return this._flat.slice(startRow, endRow).filter((f) => f.type === `row`).map((f) => f.key);
16085
+ }
16086
+ /** Coalesce viewport-change dispatches to at most one per animation frame. */
16087
+ #scheduleViewportChangeEmit() {
16088
+ if (this.#viewportChangeRaf !== 0) return;
16089
+ this.#viewportChangeRaf = requestAnimationFrame(() => {
16090
+ this.#viewportChangeRaf = 0;
16091
+ this.#emitViewportChangeIfChanged();
16092
+ });
16093
+ }
16094
+ /**
16095
+ * Dispatch `datagrid-viewport-change` with the current key sets — unless
16096
+ * neither set changed since the last dispatch, so coalesced frames and
16097
+ * no-op scrolls stay silent.
16098
+ */
16099
+ #emitViewportChangeIfChanged() {
16100
+ const renderedKeys = this.renderedKeys();
16101
+ const visibleKeys = this.#visibleKeys();
16102
+ if (sameKeys$3(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys$3(this.#lastViewportVisibleKeys, visibleKeys)) return;
16103
+ this.#lastViewportRenderedKeys = renderedKeys;
16104
+ this.#lastViewportVisibleKeys = visibleKeys;
16105
+ this.dispatchEvent(new CustomEvent(`datagrid-viewport-change`, {
16106
+ detail: {
16107
+ renderedKeys,
16108
+ visibleKeys
16109
+ },
16110
+ bubbles: true,
16111
+ composed: true
16112
+ }));
16113
+ }
16024
16114
  _findFirstVisibleCol(scrollLeft) {
16025
16115
  let lo = 0;
16026
16116
  let hi = this._colOffsets.length - 1;
@@ -16619,6 +16709,12 @@ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableE
16619
16709
  const ITEM_SELECTOR$3 = `:scope > li`;
16620
16710
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
16621
16711
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
16712
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
16713
+ function sameKeys$2(a, b) {
16714
+ if (a === void 0 || a.length !== b.length) return false;
16715
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
16716
+ return true;
16717
+ }
16622
16718
  let DetailListElement = class DetailListElement extends i {
16623
16719
  constructor(..._args) {
16624
16720
  super(..._args);
@@ -16637,6 +16733,7 @@ let DetailListElement = class DetailListElement extends i {
16637
16733
  this.#positions = /* @__PURE__ */ new Map();
16638
16734
  this.#geometryScheduled = false;
16639
16735
  this.#sortRaf = 0;
16736
+ this.#viewportScrollRaf = 0;
16640
16737
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
16641
16738
  this.#builtFromProvider = [];
16642
16739
  this.#providerBuilt = 0;
@@ -16730,6 +16827,13 @@ let DetailListElement = class DetailListElement extends i {
16730
16827
  getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
16731
16828
  highlightKey: SEARCH_HIGHLIGHT_KEY
16732
16829
  }) });
16830
+ this.#onListScroll = () => {
16831
+ if (this.#viewportScrollRaf !== 0) return;
16832
+ this.#viewportScrollRaf = requestAnimationFrame(() => {
16833
+ this.#viewportScrollRaf = 0;
16834
+ this.#emitViewportChangeIfChanged();
16835
+ });
16836
+ };
16733
16837
  this._onPointerOver = (e) => {
16734
16838
  const item = this._findListItem(e.composedPath());
16735
16839
  if (item) this.tickled.handlePointerEnter(item);
@@ -16821,6 +16925,10 @@ let DetailListElement = class DetailListElement extends i {
16821
16925
  #positions;
16822
16926
  #geometryScheduled;
16823
16927
  #sortRaf;
16928
+ #viewportScrollRaf;
16929
+ /** Key sets as of the last viewport-change dispatch, for dedupe. */
16930
+ #lastViewportRenderedKeys;
16931
+ #lastViewportVisibleKeys;
16824
16932
  #resizeObserver;
16825
16933
  #mutationObserver;
16826
16934
  /** Items that have already played (or been skipped for) their enter animation. */
@@ -16960,6 +17068,8 @@ let DetailListElement = class DetailListElement extends i {
16960
17068
  this.removeEventListener(`dblclick`, this._onDblClick);
16961
17069
  this.removeEventListener(`wheel`, this._onWheel);
16962
17070
  cancelAnimationFrame(this.#sortRaf);
17071
+ cancelAnimationFrame(this.#viewportScrollRaf);
17072
+ this.#viewportScrollRaf = 0;
16963
17073
  this.#resizeObserver?.disconnect();
16964
17074
  this.#resizeObserver = void 0;
16965
17075
  this.#mutationObserver?.disconnect();
@@ -17312,6 +17422,7 @@ let DetailListElement = class DetailListElement extends i {
17312
17422
  col: colIdx,
17313
17423
  row: rowIdx
17314
17424
  })));
17425
+ this.#scheduleViewportChangeEmit();
17315
17426
  }
17316
17427
  #positionOf(el) {
17317
17428
  let pos = this.#positions.get(el);
@@ -17603,6 +17714,45 @@ let DetailListElement = class DetailListElement extends i {
17603
17714
  cancelable: true
17604
17715
  }));
17605
17716
  }
17717
+ /** Scroll on the `part="list"` element — rAF-coalesced viewport-change check. */
17718
+ #onListScroll;
17719
+ /** Coalesce non-scroll viewport checks to at most one per animation frame. */
17720
+ #scheduleViewportChangeEmit() {
17721
+ if (this.#viewportScrollRaf !== 0) return;
17722
+ this.#viewportScrollRaf = requestAnimationFrame(() => {
17723
+ this.#viewportScrollRaf = 0;
17724
+ this.#emitViewportChangeIfChanged();
17725
+ });
17726
+ }
17727
+ /**
17728
+ * Keys of everything currently rendered. The list has no virtual window —
17729
+ * every non-hidden item is in the DOM — so this is all keyed items, mirroring
17730
+ * `ixfx-vertical-list.renderedKeys()` in its non-virtualized mode.
17731
+ */
17732
+ #renderedKeys() {
17733
+ const out = [];
17734
+ for (const el of this._getVisibleItems()) {
17735
+ const key = keyForElement(el);
17736
+ if (key !== void 0) out.push(key);
17737
+ }
17738
+ return out;
17739
+ }
17740
+ /**
17741
+ * Dispatch `detail-list-viewport-change` with the current key sets — unless
17742
+ * neither set changed since the last dispatch, so coalesced frames and
17743
+ * no-op scrolls stay silent.
17744
+ */
17745
+ #emitViewportChangeIfChanged() {
17746
+ const renderedKeys = this.#renderedKeys();
17747
+ const visibleKeys = this.visibleKeys();
17748
+ if (sameKeys$2(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys$2(this.#lastViewportVisibleKeys, visibleKeys)) return;
17749
+ this.#lastViewportRenderedKeys = renderedKeys;
17750
+ this.#lastViewportVisibleKeys = visibleKeys;
17751
+ this._dispatch(`detail-list-viewport-change`, {
17752
+ renderedKeys,
17753
+ visibleKeys
17754
+ });
17755
+ }
17606
17756
  _activateItem(item) {
17607
17757
  if (this.interactionMode !== `manual`) this.selection.replace(item);
17608
17758
  this._dispatch(`list-activate`, { item });
@@ -17761,7 +17911,7 @@ let DetailListElement = class DetailListElement extends i {
17761
17911
  const progress = this.#data.progress;
17762
17912
  return b`
17763
17913
  <div class="container">
17764
- <ul part="list" class="scroll-fade-x">
17914
+ <ul part="list" class="scroll-fade-x" @scroll=${this.#onListScroll}>
17765
17915
  <slot></slot>
17766
17916
  </ul>
17767
17917
  ${this.#marqueeRect ? b`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
@@ -23629,6 +23779,12 @@ function columnOf(index, cols) {
23629
23779
  function rowOf(index, cols) {
23630
23780
  return cols <= 1 ? index : Math.floor(index / cols);
23631
23781
  }
23782
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
23783
+ function sameKeys$1(a, b) {
23784
+ if (a === void 0 || a.length !== b.length) return false;
23785
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
23786
+ return true;
23787
+ }
23632
23788
  let GridListElement = class GridListElement extends i {
23633
23789
  constructor(..._args) {
23634
23790
  super(..._args);
@@ -24523,10 +24679,23 @@ let GridListElement = class GridListElement extends i {
24523
24679
  this.requestUpdate();
24524
24680
  }, REFLOW_WINDOW_MS);
24525
24681
  }
24682
+ /** Key sets as of the last `grid-list-viewport-change` dispatch, for dedupe. */
24683
+ #lastViewportRenderedKeys;
24684
+ #lastViewportVisibleKeys;
24685
+ /**
24686
+ * Dispatch `grid-list-viewport-change` with the current key sets — unless
24687
+ * neither set changed since the last dispatch, so coalesced frames and
24688
+ * no-op scrolls stay silent.
24689
+ */
24526
24690
  #emitViewportChange() {
24691
+ const renderedKeys = this.renderedKeys();
24692
+ const visibleKeys = this.visibleKeys();
24693
+ if (sameKeys$1(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys$1(this.#lastViewportVisibleKeys, visibleKeys)) return;
24694
+ this.#lastViewportRenderedKeys = renderedKeys;
24695
+ this.#lastViewportVisibleKeys = visibleKeys;
24527
24696
  this._dispatch(`grid-list-viewport-change`, {
24528
- renderedKeys: this.renderedKeys(),
24529
- visibleKeys: this.visibleKeys()
24697
+ renderedKeys,
24698
+ visibleKeys
24530
24699
  });
24531
24700
  }
24532
24701
  firstUpdated() {
@@ -28257,6 +28426,12 @@ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data
28257
28426
  * vertical rows are shorter relative to the viewport, so 3.
28258
28427
  */
28259
28428
  const VIRTUAL_OVERSCAN = 3;
28429
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
28430
+ function sameKeys(a, b) {
28431
+ if (a === void 0 || a.length !== b.length) return false;
28432
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
28433
+ return true;
28434
+ }
28260
28435
  let VerticalListElement = class VerticalListElement extends i {
28261
28436
  constructor(..._args) {
28262
28437
  super(..._args);
@@ -28371,11 +28546,11 @@ let VerticalListElement = class VerticalListElement extends i {
28371
28546
  highlightKey: `vertical-list-search`
28372
28547
  }) });
28373
28548
  this.#onScroll = () => {
28374
- if (!this.#virtualActive()) return;
28375
28549
  if (this.#scrollRaf !== 0) return;
28376
28550
  this.#scrollRaf = requestAnimationFrame(() => {
28377
28551
  this.#scrollRaf = 0;
28378
28552
  this.#syncVirtualWindow();
28553
+ this.#emitViewportChangeIfChanged();
28379
28554
  });
28380
28555
  };
28381
28556
  this._onPointerOver = (e) => {
@@ -28468,6 +28643,9 @@ let VerticalListElement = class VerticalListElement extends i {
28468
28643
  #suspendAdapterRebuild;
28469
28644
  #scrollRaf;
28470
28645
  #resizeObserver;
28646
+ /** Key sets as of the last viewport-change dispatch, for dedupe. */
28647
+ #lastViewportRenderedKeys;
28648
+ #lastViewportVisibleKeys;
28471
28649
  #topSpacer;
28472
28650
  #bottomSpacer;
28473
28651
  /** Last measured viewport height — kept so a 0 read can't collapse the window. */
@@ -28595,6 +28773,14 @@ let VerticalListElement = class VerticalListElement extends i {
28595
28773
  }
28596
28774
  }
28597
28775
  /**
28776
+ * The shadow scroll list only exists after the first render; start
28777
+ * watching it so resizes move the window (virtualized) and the visible
28778
+ * set (plain) too.
28779
+ */
28780
+ firstUpdated() {
28781
+ this.#ensureResizeObserved();
28782
+ }
28783
+ /**
28598
28784
  * Animate in items appended to the light DOM (however they got there —
28599
28785
  * {@link addItem}, direct `appendChild`, a framework binding). A bulk populate
28600
28786
  * (roughly everything at once) is left alone, as are the wrapper `<li>`s the
@@ -28604,6 +28790,7 @@ let VerticalListElement = class VerticalListElement extends i {
28604
28790
  if (!this.isConnected) return;
28605
28791
  if (this.#virtualActive()) {
28606
28792
  this.#syncVirtualWindow(this.#reconcileRecords(records));
28793
+ this.#scheduleViewportChangeEmit();
28607
28794
  return;
28608
28795
  }
28609
28796
  const fresh = [];
@@ -28614,6 +28801,7 @@ let VerticalListElement = class VerticalListElement extends i {
28614
28801
  this.#animatedItems.add(node);
28615
28802
  fresh.push(node);
28616
28803
  }
28804
+ this.#scheduleViewportChangeEmit();
28617
28805
  if (fresh.length === 0) return;
28618
28806
  if (this.#isBulkPopulate(fresh.length)) return;
28619
28807
  for (const el of fresh) animateListItemIn(el);
@@ -29167,6 +29355,7 @@ let VerticalListElement = class VerticalListElement extends i {
29167
29355
  if (!ul) return;
29168
29356
  this.#resizeObserver ??= new ResizeObserver(() => {
29169
29357
  this.#syncVirtualWindow();
29358
+ this.#scheduleViewportChangeEmit();
29170
29359
  });
29171
29360
  this.#resizeObserver.observe(ul);
29172
29361
  }
@@ -29246,6 +29435,32 @@ let VerticalListElement = class VerticalListElement extends i {
29246
29435
  this._syncItemStates();
29247
29436
  this._syncNativeDraggable();
29248
29437
  this.#nativeDrag.refreshDraggable();
29438
+ this.#scheduleViewportChangeEmit();
29439
+ }
29440
+ /** Coalesce content/layout viewport checks to at most one animation frame. */
29441
+ #scheduleViewportChangeEmit() {
29442
+ if (this.#scrollRaf !== 0) return;
29443
+ this.#scrollRaf = requestAnimationFrame(() => {
29444
+ this.#scrollRaf = 0;
29445
+ this.#syncVirtualWindow();
29446
+ this.#emitViewportChangeIfChanged();
29447
+ });
29448
+ }
29449
+ /**
29450
+ * Dispatch `vertical-list-viewport-change` with the current key sets — unless
29451
+ * neither set changed since the last dispatch, so coalesced frames and
29452
+ * no-op scrolls stay silent.
29453
+ */
29454
+ #emitViewportChangeIfChanged() {
29455
+ const renderedKeys = this.renderedKeys();
29456
+ const visibleKeys = this.visibleKeys();
29457
+ if (sameKeys(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys(this.#lastViewportVisibleKeys, visibleKeys)) return;
29458
+ this.#lastViewportRenderedKeys = renderedKeys;
29459
+ this.#lastViewportVisibleKeys = visibleKeys;
29460
+ this._dispatch(`vertical-list-viewport-change`, {
29461
+ renderedKeys,
29462
+ visibleKeys
29463
+ });
29249
29464
  }
29250
29465
  _getVisibleItems() {
29251
29466
  return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {