@ixfx/components 0.9.8 → 0.10.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 (98) hide show
  1. package/bundle/index.d.ts +747 -4
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +12240 -9065
  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.d.ts +1 -1
  10. package/dist/button.d.ts.map +1 -1
  11. package/dist/colour-picker.d.ts +1 -1
  12. package/dist/crumbs.d.ts +2 -2
  13. package/dist/{data-grid-BUeZjnvj.d.ts → data-grid-C5hk_pzK.d.ts} +5 -4
  14. package/dist/{data-grid-BUeZjnvj.d.ts.map → data-grid-C5hk_pzK.d.ts.map} +1 -1
  15. package/dist/data-grid.d.ts +2 -2
  16. package/dist/data-grid.js +3 -1
  17. package/dist/data-grid.js.map +1 -1
  18. package/dist/{detail-list-CQguZg9h.d.ts → detail-list-CxPpv89T.d.ts} +6 -5
  19. package/dist/detail-list-CxPpv89T.d.ts.map +1 -0
  20. package/dist/detail-list.d.ts +3 -3
  21. package/dist/detail-list.js +3 -1
  22. package/dist/detail-list.js.map +1 -1
  23. package/dist/{grid-list-lTwg4lp7.d.ts → grid-list-BGvoWbZz.d.ts} +28 -4
  24. package/dist/{grid-list-lTwg4lp7.d.ts.map → grid-list-BGvoWbZz.d.ts.map} +1 -1
  25. package/dist/{grid-list-40blkeza.js → grid-list-DSnYOGh7.js} +60 -2
  26. package/dist/grid-list-DSnYOGh7.js.map +1 -0
  27. package/dist/grid-list.d.ts +2 -2
  28. package/dist/grid-list.js +1 -1
  29. package/dist/{grouped-item-lister-CEZ72VOs.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +2 -2
  30. package/dist/{grouped-item-lister-CEZ72VOs.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
  31. package/dist/grouped-item-lister.d.ts +1 -1
  32. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
  33. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  34. package/dist/icons.d.ts +2 -2
  35. package/dist/incr-search.d.ts +1 -1
  36. package/dist/{index--qnhBWZJ.d.ts → index--5BSdbnL.d.ts} +2 -2
  37. package/dist/{index--qnhBWZJ.d.ts.map → index--5BSdbnL.d.ts.map} +1 -1
  38. package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
  39. package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  40. package/dist/{index-DsjpDksO.d.ts → index-CS9yM8L-.d.ts} +2 -2
  41. package/dist/{index-DsjpDksO.d.ts.map → index-CS9yM8L-.d.ts.map} +1 -1
  42. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
  43. package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
  44. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
  45. package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
  46. package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
  47. package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
  48. package/dist/index.d.ts +647 -22
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4804 -2313
  51. package/dist/index.js.map +1 -1
  52. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
  53. package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
  54. package/dist/labelled-radial-input.d.ts +2 -2
  55. package/dist/labelled-radial-input.d.ts.map +1 -1
  56. package/dist/labelled-range-input.d.ts +1 -1
  57. package/dist/labelled-slider-input.d.ts +1 -1
  58. package/dist/{list-native-drag-controller-B0hWdbtM.js → list-native-drag-controller-D97TYjn1.js} +36 -5
  59. package/dist/list-native-drag-controller-D97TYjn1.js.map +1 -0
  60. package/dist/{list-native-drag-controller-Cmpx7jnx.d.ts → list-native-drag-controller-DWiXQZou.d.ts} +3 -2
  61. package/dist/list-native-drag-controller-DWiXQZou.d.ts.map +1 -0
  62. package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
  63. package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
  64. package/dist/menu.d.ts +2 -2
  65. package/dist/miller.d.ts +2 -2
  66. package/dist/panel.d.ts +1 -1
  67. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
  68. package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
  69. package/dist/split-layout.d.ts +1 -1
  70. package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
  71. package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
  72. package/dist/tabs.d.ts +1 -1
  73. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +2 -2
  74. package/dist/{titlebar-B5zSTZnF.d.ts.map → titlebar-Tqu-VvMU.d.ts.map} +1 -1
  75. package/dist/titlebar.d.ts +1 -1
  76. package/dist/{tree-CVj7TIk2.js → tree-C25xYUa6.js} +4 -2
  77. package/dist/tree-C25xYUa6.js.map +1 -0
  78. package/dist/{tree-component-BW2iCFq9.d.ts → tree-component-mw8x-n2Y.d.ts} +4 -2
  79. package/dist/tree-component-mw8x-n2Y.d.ts.map +1 -0
  80. package/dist/tree.d.ts +4 -3
  81. package/dist/tree.d.ts.map +1 -1
  82. package/dist/tree.js +1 -1
  83. package/dist/{vertical-list-CHrXmmQ3.d.ts → vertical-list--tFanQwM.d.ts} +92 -4
  84. package/dist/vertical-list--tFanQwM.d.ts.map +1 -0
  85. package/dist/vertical-list.d.ts +2 -2
  86. package/dist/vertical-list.js +617 -28
  87. package/dist/vertical-list.js.map +1 -1
  88. package/docs-user/grid-list.md +14 -0
  89. package/docs-user/index.json +1 -1
  90. package/package.json +1 -1
  91. package/dist/detail-list-CQguZg9h.d.ts.map +0 -1
  92. package/dist/grid-list-40blkeza.js.map +0 -1
  93. package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
  94. package/dist/list-native-drag-controller-B0hWdbtM.js.map +0 -1
  95. package/dist/list-native-drag-controller-Cmpx7jnx.d.ts.map +0 -1
  96. package/dist/tree-CVj7TIk2.js.map +0 -1
  97. package/dist/tree-component-BW2iCFq9.d.ts.map +0 -1
  98. package/dist/vertical-list-CHrXmmQ3.d.ts.map +0 -1
@@ -2,14 +2,19 @@ import { t as TickledController } from "./tickled-controller-Dh1Uh_vG.js";
2
2
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
3
3
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
4
4
  import { c as createElementListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-C05WjbZh.js";
5
- import { t as ListNativeDragController } from "./list-native-drag-controller-B0hWdbtM.js";
5
+ import { t as ListNativeDragController } from "./list-native-drag-controller-D97TYjn1.js";
6
6
  import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-SA4piSBi.js";
7
7
  import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
8
8
  import { LitElement, css, html, render } from "lit";
9
9
  import { property } from "lit/decorators.js";
10
10
  import { createCommandRegistry } from "@clinth/ui-commands";
11
11
  //#region src/vertical-list/vertical-list.ts
12
- const ITEM_SELECTOR = `li, [data-list-item]`;
12
+ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data-virtual-spacer])`;
13
+ /**
14
+ * Rows rendered above/below the viewport as scroll margin. Grid-list uses 2;
15
+ * vertical rows are shorter relative to the viewport, so 3.
16
+ */
17
+ const VIRTUAL_OVERSCAN = 3;
13
18
  let VerticalListElement = class VerticalListElement extends LitElement {
14
19
  constructor(..._args) {
15
20
  super(..._args);
@@ -17,11 +22,24 @@ let VerticalListElement = class VerticalListElement extends LitElement {
17
22
  this.interactionMode = `standard`;
18
23
  this.itemWrap = `stop`;
19
24
  this.itemsDraggable = false;
25
+ this.keyProperty = ``;
26
+ this.virtualize = false;
27
+ this.itemHeight = 0;
20
28
  this.#customWrappers = /* @__PURE__ */ new Map();
21
29
  this.#wrappedElements = /* @__PURE__ */ new Set();
30
+ this.#itemData = /* @__PURE__ */ new WeakMap();
22
31
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
32
+ this.#allItems = [];
33
+ this.#pool = document.createDocumentFragment();
34
+ this.#windowStart = 0;
35
+ this.#windowEnd = 0;
36
+ this.#pendingAnimate = [];
37
+ this.#suspendAdapterRebuild = false;
38
+ this.#scrollRaf = 0;
39
+ this.#lastViewportH = 0;
40
+ this.#virtualWarned = false;
23
41
  this.selection = new ListSelectionController({
24
- getOrderedItems: () => this._getVisibleItems(),
42
+ getOrderedItems: () => this.#selectionOrder(),
25
43
  isSelectable: (el) => this._isCheckable(el),
26
44
  getModes: () => ({
27
45
  selectionMode: this.selectionMode,
@@ -63,6 +81,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
63
81
  prepareSelection: (originKey) => this.#prepareNativeDrag(originKey),
64
82
  getMetrics: () => ({}),
65
83
  getRenderer: () => this.dragPreviewRenderer,
84
+ getDragPreviewSourceResolver: () => this.dragPreviewSource,
66
85
  dispatch: (detail) => this._dispatchCancelable(`list-native-drag-start`, detail),
67
86
  onActiveChange: (active, originKey) => {
68
87
  this.#nativeDragging = active;
@@ -85,7 +104,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
85
104
  });
86
105
  this.#dragSelect = new ListDragSelectController(this, {
87
106
  getSurface: () => this.renderRoot.querySelector(`.container`),
88
- isEnabled: () => this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
107
+ isEnabled: () => !this.#virtualActive() && this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
89
108
  hitTest: (rect) => this.#marqueeHitTest(rect),
90
109
  getBaseSelection: () => this.selection.selected,
91
110
  onPreview: (preview, rect) => {
@@ -109,6 +128,14 @@ let VerticalListElement = class VerticalListElement extends LitElement {
109
128
  getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
110
129
  highlightKey: `vertical-list-search`
111
130
  }) });
131
+ this.#onScroll = () => {
132
+ if (!this.#virtualActive()) return;
133
+ if (this.#scrollRaf !== 0) return;
134
+ this.#scrollRaf = requestAnimationFrame(() => {
135
+ this.#scrollRaf = 0;
136
+ this.#syncVirtualWindow();
137
+ });
138
+ };
112
139
  this._onPointerOver = (e) => {
113
140
  const item = this._findListItem(e.composedPath());
114
141
  if (item) this.tickled.handlePointerEnter(item);
@@ -176,10 +203,34 @@ let VerticalListElement = class VerticalListElement extends LitElement {
176
203
  }
177
204
  #customWrappers;
178
205
  #wrappedElements;
206
+ /** Source object behind each adapter-built row (`items` mode). */
207
+ #itemData;
179
208
  /** Watches for items added to the light DOM so they can be animated in. */
180
209
  #itemObserver;
181
210
  /** Items that have already played (or been skipped for) their enter animation. */
182
211
  #animatedItems;
212
+ /**
213
+ * Global row order while virtualized — the source of truth both modes
214
+ * write to. Holds canonical rows (checked-mode wrappers, not inners).
215
+ * Non-virtualized mode ignores it and reads the DOM directly.
216
+ */
217
+ #allItems;
218
+ /** Off-DOM pool for recycled offscreen rows. `querySelectorAll` works on it. */
219
+ #pool;
220
+ /** Current rendered window as registry indices `[start, end)`. */
221
+ #windowStart;
222
+ #windowEnd;
223
+ /** Window rows queued for their enter animation after placement. */
224
+ #pendingAnimate;
225
+ /** Skip one adapter rebuild (updateItem's write-through to `items`). */
226
+ #suspendAdapterRebuild;
227
+ #scrollRaf;
228
+ #resizeObserver;
229
+ #topSpacer;
230
+ #bottomSpacer;
231
+ /** Last measured viewport height — kept so a 0 read can't collapse the window. */
232
+ #lastViewportH;
233
+ #virtualWarned;
183
234
  #nativeDragging;
184
235
  #postDragSuppressTimer;
185
236
  #nativeDrag;
@@ -250,9 +301,13 @@ let VerticalListElement = class VerticalListElement extends LitElement {
250
301
  }
251
302
  #effectiveSearchModes() {
252
303
  const raw = this.searchModes?.trim();
253
- if (raw === void 0) return [...LIST_SEARCH_MODES];
254
- const requested = new Set(raw.split(/\s+/));
255
- return LIST_SEARCH_MODES.filter((m) => requested.has(m));
304
+ let modes;
305
+ if (raw === void 0) modes = [...LIST_SEARCH_MODES];
306
+ else {
307
+ const requested = new Set(raw.split(/\s+/));
308
+ modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
309
+ }
310
+ return this.#virtualActive() ? modes.filter((m) => m !== `filter`) : modes;
256
311
  }
257
312
  #syncSearchCommands() {
258
313
  if (!this.#searchRegistry) return;
@@ -273,6 +328,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
273
328
  this.#itemObserver.observe(this, { childList: true });
274
329
  this.#searchRegistry ??= createCommandRegistry();
275
330
  this.#syncSearchCommands();
331
+ this.#ensureResizeObserved();
276
332
  }
277
333
  disconnectedCallback() {
278
334
  super.disconnectedCallback();
@@ -282,6 +338,12 @@ let VerticalListElement = class VerticalListElement extends LitElement {
282
338
  this.removeEventListener(`click`, this._onClick);
283
339
  this.removeEventListener(`dblclick`, this._onDblClick);
284
340
  this.#itemObserver?.disconnect();
341
+ this.#resizeObserver?.disconnect();
342
+ this.#resizeObserver = void 0;
343
+ if (this.#scrollRaf !== 0) {
344
+ cancelAnimationFrame(this.#scrollRaf);
345
+ this.#scrollRaf = 0;
346
+ }
285
347
  this.#unregisterSearchCommands?.();
286
348
  this.#unregisterSearchCommands = void 0;
287
349
  this.#searchRegistry = void 0;
@@ -298,6 +360,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
298
360
  */
299
361
  #onItemsMutated(records) {
300
362
  if (!this.isConnected) return;
363
+ if (this.#virtualActive()) {
364
+ this.#syncVirtualWindow(this.#reconcileRecords(records));
365
+ return;
366
+ }
301
367
  const fresh = [];
302
368
  for (const record of records) for (const node of record.addedNodes) {
303
369
  if (!(node instanceof HTMLElement) || node.parentElement !== this) continue;
@@ -307,13 +373,43 @@ let VerticalListElement = class VerticalListElement extends LitElement {
307
373
  fresh.push(node);
308
374
  }
309
375
  if (fresh.length === 0) return;
310
- const total = this.querySelectorAll(ITEM_SELECTOR).length;
311
- if (fresh.length >= total) return;
376
+ if (this.#isBulkPopulate(fresh.length)) return;
312
377
  for (const el of fresh) animateListItemIn(el);
313
378
  }
379
+ /**
380
+ * Whether `freshCount` newly-added rows amount to a bulk populate (first
381
+ * render / wholesale replace) rather than an incremental add — the single
382
+ * threshold both the virtual and non-virtual mutation-observer paths use
383
+ * to decide whether to skip the enter animation.
384
+ */
385
+ #isBulkPopulate(freshCount) {
386
+ return freshCount >= this.querySelectorAll(ITEM_SELECTOR).length;
387
+ }
314
388
  updated(changed) {
315
389
  super.updated(changed);
316
390
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
391
+ if (changed.has(`virtualize`) || changed.has(`itemHeight`)) {
392
+ const wasActive = (changed.has(`virtualize`) ? changed.get(`virtualize`) : this.virtualize) === true && ((changed.has(`itemHeight`) ? changed.get(`itemHeight`) : this.itemHeight) ?? 0) > 0;
393
+ if (this.#virtualActive()) {
394
+ if (!wasActive) {
395
+ this.#pool = document.createDocumentFragment();
396
+ this.#allItems = [...this.querySelectorAll(ITEM_SELECTOR)];
397
+ for (const el of this.#allItems) this.#animatedItems.add(el);
398
+ this.#pendingAnimate = [];
399
+ }
400
+ this.#syncVirtualWindow(true);
401
+ } else if (wasActive) this.#teardownVirtualWindow();
402
+ else if (this.virtualize && !this.#virtualWarned) {
403
+ this.#virtualWarned = true;
404
+ console.warn(`ixfx-vertical-list: virtualize needs item-height > 0 — running non-virtualized.`);
405
+ }
406
+ this.#syncSearchCommands();
407
+ }
408
+ if ((changed.has(`items`) || changed.has(`formatter`) || changed.has(`keyProperty`)) && this.items !== void 0) {
409
+ if (this.#suspendAdapterRebuild) this.#suspendAdapterRebuild = false;
410
+ else if (this.#virtualActive()) this.#rebuildAdapterVirtual();
411
+ else this.#rebuildFromAdapter();
412
+ }
317
413
  if (changed.has(`interactionMode`)) {
318
414
  this.selection.clearAnchor();
319
415
  const previousSelected = this.selection.selected;
@@ -350,15 +446,28 @@ let VerticalListElement = class VerticalListElement extends LitElement {
350
446
  } else el = item;
351
447
  if (options?.notChecked) el.setAttribute(`not-checked`, ``);
352
448
  this.appendChild(el);
449
+ if (this.#virtualActive()) {
450
+ this.#syncVirtualWindow(true);
451
+ return;
452
+ }
353
453
  if (this.interactionMode === `checked`) this._syncCheckboxes();
354
454
  }
355
455
  /**
356
456
  * Collapse + fade `item` out, then detach it. The rows below slide up to
357
457
  * close the gap. Resolves once the element has actually been removed.
358
458
  * Under `prefers-reduced-motion: reduce` it's removed immediately.
459
+ * A pooled (offscreen) row under virtualization is removed immediately.
359
460
  */
360
461
  removeItem(item) {
361
462
  this.selection.deselect(item);
463
+ if (this.#virtualActive()) return (item.parentNode !== this ? Object.assign(Promise.resolve(), { animation: void 0 }) : animateListItemOut(item)).then(() => {
464
+ const idx = this.#registryIndexOf(item);
465
+ if (idx >= 0) this.#allItems.splice(idx, 1);
466
+ this.#cleanupWrapperRefs(item);
467
+ item.remove();
468
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
469
+ this.#syncVirtualWindow(true);
470
+ });
362
471
  return animateListItemOut(item).then(() => {
363
472
  item.remove();
364
473
  if (this.interactionMode === `checked`) this._syncCheckboxes();
@@ -370,9 +479,15 @@ let VerticalListElement = class VerticalListElement extends LitElement {
370
479
  }
371
480
  clearItems() {
372
481
  this.#customWrappers.clear();
482
+ if (this.#virtualActive()) {
483
+ this.#allItems = [];
484
+ this.#pool = document.createDocumentFragment();
485
+ this.#pendingAnimate = [];
486
+ }
373
487
  this.replaceChildren();
374
488
  this.selection.reset();
375
489
  this._syncItemStates();
490
+ if (this.#virtualActive()) this.#syncVirtualWindow(true);
376
491
  }
377
492
  /**
378
493
  * Replace the entire item list in one call.
@@ -380,7 +495,16 @@ let VerticalListElement = class VerticalListElement extends LitElement {
380
495
  * Preserves `interactionMode` checkbox wrapping.
381
496
  */
382
497
  setItems(items) {
383
- this.clearItems();
498
+ if (this.#virtualActive()) {
499
+ this.#customWrappers.clear();
500
+ this.#pool = document.createDocumentFragment();
501
+ this.#pendingAnimate = [];
502
+ this.#allItems = [...items];
503
+ for (const el of this.#allItems) this.#animatedItems.add(el);
504
+ this.selection.reset();
505
+ this.#syncVirtualWindow(true);
506
+ return;
507
+ }
384
508
  for (const item of items) this.appendChild(item);
385
509
  if (this.interactionMode === `checked`) this._syncCheckboxes();
386
510
  }
@@ -428,10 +552,23 @@ let VerticalListElement = class VerticalListElement extends LitElement {
428
552
  removeSelectedKeys(keys) {
429
553
  this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
430
554
  }
431
- /** Move the cursor to the item with this key and scroll it into view. */
555
+ /**
556
+ * Move the cursor to the item with this key and scroll it into view.
557
+ * Under virtualization the key's index is scrolled into the rendered
558
+ * window first (a no-op for unknown keys).
559
+ */
432
560
  focusKey(key) {
561
+ if (this.#virtualActive()) {
562
+ const index = this.#allItems.findIndex((el) => keyForElement(el) === key);
563
+ if (index < 0) return;
564
+ const ul = this.renderRoot.querySelector(`ul`);
565
+ if (ul) {
566
+ ul.scrollTop = index * this.itemHeight;
567
+ this.#syncVirtualWindow(true);
568
+ }
569
+ }
433
570
  const el = this.#itemForKey(key);
434
- if (!el) return;
571
+ if (!el || el.parentNode !== this) return;
435
572
  this.tickled.focus(el);
436
573
  el.scrollIntoView({
437
574
  block: `nearest`,
@@ -439,25 +576,435 @@ let VerticalListElement = class VerticalListElement extends LitElement {
439
576
  });
440
577
  }
441
578
  #itemForKey(key) {
579
+ if (this.#virtualActive()) return this.#allItems.find((el) => keyForElement(el) === key);
442
580
  for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
443
581
  if (this.#wrappedElements.has(el)) continue;
444
582
  if (keyForElement(el) === key) return el;
445
583
  }
446
584
  }
585
+ /** The item carrying this `data-key`, or `undefined`. */
586
+ getItemByKey(key) {
587
+ return this.#itemForKey(key);
588
+ }
589
+ /**
590
+ * Keys of items currently intersecting the viewport — for prioritising
591
+ * viewport-first work (e.g. metadata/thumbnail fetches). A rect-intersection
592
+ * test against the scroll container over the already-created rows (the same
593
+ * cheap trick as `ixfx-detail-list.visibleKeys()`); hidden (filtered-out)
594
+ * and keyless items are omitted.
595
+ */
596
+ visibleKeys() {
597
+ const container = this.renderRoot.querySelector(`ul`);
598
+ if (!container) return [];
599
+ const box = container.getBoundingClientRect();
600
+ const out = [];
601
+ for (const el of this._getVisibleItems()) {
602
+ const key = keyForElement(el);
603
+ if (key === void 0) continue;
604
+ const r = el.getBoundingClientRect();
605
+ if (r.bottom > box.top && r.top < box.bottom) out.push(key);
606
+ }
607
+ return out;
608
+ }
609
+ /**
610
+ * Keys of the currently rendered window (viewport + overscan) — the
611
+ * priority set for prefetching, mirroring `ixfx-grid-list.renderedKeys()`.
612
+ * Use {@link visibleKeys} for the strict viewport intersection. Keyless
613
+ * rows are omitted. Without virtualization, every visible row's key.
614
+ */
615
+ renderedKeys() {
616
+ if (!this.#virtualActive()) {
617
+ const out = [];
618
+ for (const el of this._getVisibleItems()) {
619
+ const key = keyForElement(el);
620
+ if (key !== void 0) out.push(key);
621
+ }
622
+ return out;
623
+ }
624
+ const out = [];
625
+ for (let i = this.#windowStart; i < this.#windowEnd; i++) {
626
+ const el = this.#allItems[i];
627
+ if (!el) continue;
628
+ const key = keyForElement(el);
629
+ if (key !== void 0) out.push(key);
630
+ }
631
+ return out;
632
+ }
447
633
  *selectedFilter(kind) {
448
- const wanted = (el) => {
634
+ const source = this.#virtualActive() ? this.#allItems : this.querySelectorAll(ITEM_SELECTOR);
635
+ const isChecked = (el) => {
636
+ if (this.interactionMode !== `checked`) return false;
637
+ return !(this.#customWrappers.get(el) ?? el).hasAttribute(`not-checked`) && this.selection.selected.has(el);
638
+ };
639
+ for (const el of source) {
640
+ if (!this.#virtualActive() && this.#wrappedElements.has(el)) continue;
641
+ if (el.hidden) continue;
449
642
  switch (kind) {
450
- case `selected`: return el.hasAttribute(`data-selected`);
451
- case `checked`: return el.hasAttribute(`data-checked`);
452
- case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);
643
+ case `selected`:
644
+ if (this.selection.selected.has(el)) yield el;
645
+ break;
646
+ case `checked`:
647
+ if (isChecked(el)) yield el;
648
+ break;
649
+ case `either`: if (this.selection.selected.has(el) || isChecked(el)) yield el;
650
+ }
651
+ }
652
+ }
653
+ /**
654
+ * Get the source object behind a row built from `items`. Returns
655
+ * `undefined` for declarative / programmatic elements.
656
+ */
657
+ getItemData(item) {
658
+ return this.#itemData.get(item);
659
+ }
660
+ /**
661
+ * Adapter mode: re-render the row carrying `key` in place — scroll and
662
+ * selection are untouched. Unknown keys, and keys on rows not built from
663
+ * `items`, are ignored. A no-op unless `items` is set.
664
+ *
665
+ * Rows whose formatter returns a full row element are swapped for the fresh
666
+ * element; `<li>` rows are refilled. (A custom-element row with a
667
+ * non-element formatter result cannot be re-rendered in place and is left
668
+ * alone — have the formatter return a replacement element instead.)
669
+ */
670
+ updateItem(key, data) {
671
+ if (this.items === void 0) return;
672
+ const el = this.getItemByKey(key);
673
+ if (!el || !this.#itemData.has(el)) return;
674
+ const index = this.#adapterIndexOf(el);
675
+ const fresh = this.formatter ? this.formatter(data, index) : data;
676
+ if (el instanceof HTMLLIElement) {
677
+ for (const child of [...el.childNodes]) child.remove();
678
+ this.#fillAdapterItem(el, fresh, data);
679
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
680
+ this._syncItemStates();
681
+ } else if (fresh instanceof Element && this.#isRowElement(fresh)) {
682
+ if (fresh.tagName !== `LI` && !fresh.hasAttribute(`data-list-item`)) fresh.setAttribute(`data-list-item`, ``);
683
+ this.#stampAdapterKey(fresh, data);
684
+ this.#itemData.set(fresh, data);
685
+ this.#itemData.delete(el);
686
+ this.selection.remap((item) => item === el ? fresh : item);
687
+ if (this.#virtualActive()) this.#swapRegistry(el, fresh);
688
+ el.replaceWith(fresh);
689
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
690
+ this._syncItemStates();
691
+ }
692
+ if (this.keyProperty) {
693
+ const sourceIndex = this.items.findIndex((entry) => this.#adapterProperty(entry, this.keyProperty) !== void 0 && String(this.#adapterProperty(entry, this.keyProperty)) === key);
694
+ if (sourceIndex >= 0) {
695
+ const next = [...this.items];
696
+ next[sourceIndex] = data;
697
+ this.#suspendAdapterRebuild = true;
698
+ this.items = next;
699
+ }
700
+ }
701
+ }
702
+ #rebuildFromAdapter() {
703
+ const source = this.items ?? [];
704
+ const keyOf = (el) => el.getAttribute(`data-key`);
705
+ const previousKeys = new Set([...this.selection.selected].map(keyOf).filter((k) => k !== null));
706
+ const anchorKey = this.selection.anchor ? keyOf(this.selection.anchor) : null;
707
+ this.#customWrappers.clear();
708
+ this.replaceChildren(...source.map((item, index) => this.#createAdapterItem(item, index)));
709
+ if (previousKeys.size > 0 || anchorKey !== null) {
710
+ const next = [];
711
+ let anchor;
712
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
713
+ const key = keyOf(el);
714
+ if (key !== null && previousKeys.has(key)) next.push(el);
715
+ if (key !== null && key === anchorKey) anchor = el;
716
+ }
717
+ this.selection.seed(next, anchor);
718
+ }
719
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
720
+ this._syncItemStates();
721
+ this._syncNativeDraggable();
722
+ this.#nativeDrag.refreshDraggable();
723
+ }
724
+ #createAdapterItem(item, index) {
725
+ const content = this.formatter ? this.formatter(item, index) : item;
726
+ if (content instanceof Element && this.#isRowElement(content)) {
727
+ if (content.tagName !== `LI` && !content.hasAttribute(`data-list-item`)) content.setAttribute(`data-list-item`, ``);
728
+ this.#stampAdapterKey(content, item);
729
+ this.#itemData.set(content, item);
730
+ return content;
731
+ }
732
+ const li = document.createElement(`li`);
733
+ this.#fillAdapterItem(li, content, item);
734
+ return li;
735
+ }
736
+ #fillAdapterItem(li, content, item) {
737
+ if (typeof content === `string`) li.textContent = content;
738
+ else if (content instanceof Node) li.appendChild(content);
739
+ else if (this.formatter && content !== void 0 && content !== null) render(content, li);
740
+ this.#stampAdapterKey(li, item);
741
+ this.#itemData.set(li, item);
742
+ }
743
+ /**
744
+ * Whether a formatter-produced element is a whole row (used directly)
745
+ * rather than row content (rendered into a `<li>`): an `<li>`, already
746
+ * marked `[data-list-item]`, or a custom element.
747
+ */
748
+ #isRowElement(content) {
749
+ return content.tagName === `LI` || content.hasAttribute(`data-list-item`) || content.tagName.includes(`-`);
750
+ }
751
+ #stampAdapterKey(el, item) {
752
+ if (!this.keyProperty) return;
753
+ const key = this.#adapterProperty(item, this.keyProperty);
754
+ if (key !== void 0 && key !== null) el.setAttribute(`data-key`, String(key));
755
+ }
756
+ #adapterProperty(item, prop) {
757
+ if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
758
+ return item[prop];
759
+ }
760
+ /** Ordinal of an adapter-built row among adapter-built rows in DOM order. */
761
+ #adapterIndexOf(el) {
762
+ if (this.#virtualActive()) return this.#registryIndexOf(el);
763
+ let index = 0;
764
+ for (const item of this.querySelectorAll(ITEM_SELECTOR)) {
765
+ if (item === el) return index;
766
+ if (this.#itemData.has(item)) index++;
767
+ }
768
+ return index;
769
+ }
770
+ /**
771
+ * Adapter rebuild for the virtualized window: same content as
772
+ * {@link #rebuildFromAdapter} but rows become the recycle registry and
773
+ * only the window attaches.
774
+ */
775
+ #rebuildAdapterVirtual() {
776
+ const source = this.items ?? [];
777
+ const keyOf = (el) => el.getAttribute(`data-key`);
778
+ const previousKeys = new Set([...this.selection.selected].map(keyOf).filter((k) => k !== null));
779
+ const anchorKey = this.selection.anchor ? keyOf(this.selection.anchor) : null;
780
+ this.#customWrappers.clear();
781
+ this.#pool = document.createDocumentFragment();
782
+ this.#pendingAnimate = [];
783
+ this.#allItems = source.map((item, index) => this.#createAdapterItem(item, index));
784
+ for (const el of this.#allItems) this.#animatedItems.add(el);
785
+ if (previousKeys.size > 0 || anchorKey !== null) {
786
+ const next = [];
787
+ let anchor;
788
+ for (const el of this.#allItems) {
789
+ const key = keyOf(el);
790
+ if (key !== null && previousKeys.has(key)) next.push(el);
791
+ if (key !== null && key === anchorKey) anchor = el;
792
+ }
793
+ this.selection.seed(next, anchor);
794
+ }
795
+ this.#syncVirtualWindow(true);
796
+ }
797
+ #virtualActive() {
798
+ return this.virtualize && this.itemHeight > 0;
799
+ }
800
+ /**
801
+ * Selection order: the full registry while virtualized (so `selectAll` and
802
+ * range operations span pooled rows too — they hold live element refs), the
803
+ * attached rows otherwise.
804
+ */
805
+ #selectionOrder() {
806
+ if (!this.#virtualActive()) return this._getVisibleItems();
807
+ return this.#allItems.filter((el) => {
808
+ if (el.hidden) return false;
809
+ if (this.filterPredicate && !this.filterPredicate(el)) return false;
810
+ return true;
811
+ });
812
+ }
813
+ /** Registry index of a row, resolving wrapped inners to their wrapper. */
814
+ #registryIndexOf(el) {
815
+ const direct = this.#allItems.indexOf(el);
816
+ if (direct >= 0) return direct;
817
+ if (this.#wrappedElements.has(el)) {
818
+ for (const [wrapper, inner] of this.#customWrappers) if (inner === el) return this.#allItems.indexOf(wrapper);
819
+ }
820
+ return -1;
821
+ }
822
+ #swapRegistry(oldEl, newEl) {
823
+ const i = this.#allItems.indexOf(oldEl);
824
+ if (i >= 0) this.#allItems[i] = newEl;
825
+ }
826
+ #cleanupWrapperRefs(el) {
827
+ if (this.#customWrappers.has(el)) {
828
+ const inner = this.#customWrappers.get(el);
829
+ this.#customWrappers.delete(el);
830
+ if (inner) this.#wrappedElements.delete(inner);
831
+ }
832
+ if (this.#wrappedElements.has(el)) {
833
+ for (const [wrapper, inner] of this.#customWrappers) if (inner === el) {
834
+ this.#customWrappers.delete(wrapper);
835
+ break;
453
836
  }
837
+ this.#wrappedElements.delete(el);
838
+ }
839
+ }
840
+ /**
841
+ * Global registry index for an attached row, derived from its DOM position:
842
+ * nodes after the bottom spacer are end-of-list appends, nodes before the
843
+ * top spacer are prepends, otherwise window-start plus preceding rows.
844
+ */
845
+ #globalIndexForAttached(el) {
846
+ let count = 0;
847
+ let sib = el.previousElementSibling;
848
+ while (sib) {
849
+ if (sib === this.#topSpacer) return this.#windowStart + count;
850
+ if (sib === this.#bottomSpacer) return this.#allItems.length;
851
+ if (sib.matches(ITEM_SELECTOR)) count++;
852
+ sib = sib.previousElementSibling;
853
+ }
854
+ return count;
855
+ }
856
+ /**
857
+ * Fold consumer-side DOM edits into the registry. Our own window moves
858
+ * reconcile to no-ops (known nodes already pooled or correctly placed);
859
+ * genuinely new nodes are spliced by DOM position; removed-then-gone nodes
860
+ * are dropped. Returns whether the registry changed.
861
+ */
862
+ #reconcileRecords(records) {
863
+ let changed = false;
864
+ const fresh = [];
865
+ for (const record of records) {
866
+ for (const node of record.addedNodes) {
867
+ if (!(node instanceof Element) || node.parentNode !== this) continue;
868
+ if (node.hasAttribute(`data-virtual-spacer`) || !node.matches(ITEM_SELECTOR)) continue;
869
+ const known = this.#registryIndexOf(node);
870
+ if (known < 0) {
871
+ const at = Math.min(this.#globalIndexForAttached(node), this.#allItems.length);
872
+ this.#allItems.splice(at, 0, node);
873
+ fresh.push(node);
874
+ changed = true;
875
+ } else if (this.#globalIndexForAttached(node) !== known) {
876
+ const at = Math.min(this.#globalIndexForAttached(node), this.#allItems.length - 1);
877
+ this.#allItems.splice(known, 1);
878
+ this.#allItems.splice(at, 0, node);
879
+ changed = true;
880
+ }
881
+ }
882
+ for (const node of record.removedNodes) {
883
+ if (!(node instanceof Element) || node.hasAttribute(`data-virtual-spacer`)) continue;
884
+ const known = this.#registryIndexOf(node);
885
+ if (known < 0) continue;
886
+ if (node.parentNode === this.#pool || node.parentNode === this) continue;
887
+ this.#allItems.splice(known, 1);
888
+ changed = true;
889
+ }
890
+ }
891
+ if (fresh.length > 0) {
892
+ if (this.#isBulkPopulate(fresh.length)) for (const el of fresh) this.#animatedItems.add(el);
893
+ else this.#pendingAnimate.push(...fresh);
894
+ }
895
+ if (changed && this.interactionMode === `checked`) this._syncCheckboxes();
896
+ return changed;
897
+ }
898
+ /** Consume queued observer records first so a sync never drops a row. */
899
+ #drainMutations() {
900
+ const pending = this.#itemObserver?.takeRecords() ?? [];
901
+ return pending.length > 0 ? this.#reconcileRecords(pending) : false;
902
+ }
903
+ #scrollList() {
904
+ return this.renderRoot.querySelector(`ul`);
905
+ }
906
+ #measureViewport(ul) {
907
+ if (!ul) return this.#lastViewportH;
908
+ const h = ul.clientHeight || ul.getBoundingClientRect().height || 0;
909
+ if (h > 0) this.#lastViewportH = h;
910
+ return this.#lastViewportH;
911
+ }
912
+ #ensureSpacers() {
913
+ if (this.#topSpacer && this.#bottomSpacer) return;
914
+ const make = () => {
915
+ const li = document.createElement(`li`);
916
+ li.setAttribute(`data-virtual-spacer`, ``);
917
+ li.setAttribute(`aria-hidden`, `true`);
918
+ return li;
454
919
  };
455
- for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
456
- if (this.#wrappedElements.has(el)) continue;
457
- if (el.hidden) continue;
458
- if (wanted(el)) yield el;
920
+ this.#topSpacer = make();
921
+ this.#bottomSpacer = make();
922
+ }
923
+ #ensureResizeObserved() {
924
+ const ul = this.#scrollList();
925
+ if (!ul) return;
926
+ this.#resizeObserver ??= new ResizeObserver(() => {
927
+ this.#syncVirtualWindow();
928
+ });
929
+ this.#resizeObserver.observe(ul);
930
+ }
931
+ #onScroll;
932
+ /**
933
+ * Reconcile the attached light DOM with the overscanned window. Rows are
934
+ * moved by identity (never recreated); selection, cursor and component
935
+ * state travel with them.
936
+ */
937
+ #syncVirtualWindow(force = false) {
938
+ if (!this.#virtualActive() || !this.isConnected) return;
939
+ const changed = this.#drainMutations() || force;
940
+ this.#ensureResizeObserved();
941
+ const count = this.#allItems.length;
942
+ const ul = this.#scrollList();
943
+ const stride = this.itemHeight;
944
+ const viewportH = this.#measureViewport(ul);
945
+ const maxScroll = Math.max(0, count * stride - viewportH);
946
+ const offset = Math.min(Math.max(0, ul ? ul.scrollTop : 0), maxScroll);
947
+ if (ul && Math.abs(ul.scrollTop - offset) > .5) ul.scrollTop = offset;
948
+ const firstVisible = Math.floor(offset / stride);
949
+ const lastVisible = Math.ceil((offset + viewportH) / stride);
950
+ const start = count === 0 ? 0 : Math.max(0, firstVisible - VIRTUAL_OVERSCAN);
951
+ const end = Math.min(count, lastVisible + VIRTUAL_OVERSCAN);
952
+ if (!changed && start === this.#windowStart && end === this.#windowEnd) return;
953
+ if (count === 0) {
954
+ if (this.#allItems.length > 0) this.#pool.append(...this.#allItems);
955
+ this.replaceChildren();
956
+ } else {
957
+ this.#ensureSpacers();
958
+ const top = this.#topSpacer;
959
+ const bottom = this.#bottomSpacer;
960
+ if (!top || !bottom) return;
961
+ top.style.height = `${start * stride}px`;
962
+ bottom.style.height = `${Math.max(0, (count - end) * stride)}px`;
963
+ const rows = this.#allItems.slice(start, end);
964
+ const parked = [...this.#allItems.slice(0, start), ...this.#allItems.slice(end)];
965
+ if (parked.length > 0) this.#pool.append(...parked);
966
+ this.replaceChildren(top, ...rows, bottom);
967
+ }
968
+ this.#windowStart = start;
969
+ this.#windowEnd = end;
970
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
971
+ this._syncItemStates();
972
+ for (let i = start; i < end; i++) {
973
+ const el = this.#allItems[i];
974
+ if (el) this.#applyRowState(el);
975
+ }
976
+ const pending = this.#pendingAnimate;
977
+ this.#pendingAnimate = [];
978
+ for (const el of pending) {
979
+ this.#animatedItems.add(el);
980
+ if (el.parentNode === this) animateListItemIn(el);
459
981
  }
460
982
  }
983
+ /** Per-attach state the attached sweep can't cover (it only sees the window). */
984
+ #applyRowState(el) {
985
+ if (el instanceof HTMLElement) el.draggable = this.itemsDraggable && this._isCheckable(el);
986
+ el.removeAttribute(`data-marquee`);
987
+ el.removeAttribute(`data-drag-origin`);
988
+ }
989
+ /**
990
+ * Leave virtualization: reattach every registry row in order and drop the
991
+ * spacers and pool. Selection (live refs) and cursor survive untouched.
992
+ */
993
+ #teardownVirtualWindow() {
994
+ for (const el of this.#allItems) this.#animatedItems.add(el);
995
+ if (this.#allItems.length > 0) this.replaceChildren(...this.#allItems);
996
+ else this.replaceChildren();
997
+ this.#pool = document.createDocumentFragment();
998
+ this.#topSpacer = void 0;
999
+ this.#bottomSpacer = void 0;
1000
+ this.#windowStart = 0;
1001
+ this.#windowEnd = 0;
1002
+ this.#pendingAnimate = [];
1003
+ this._syncCheckboxes();
1004
+ this._syncItemStates();
1005
+ this._syncNativeDraggable();
1006
+ this.#nativeDrag.refreshDraggable();
1007
+ }
461
1008
  _getVisibleItems() {
462
1009
  return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
463
1010
  if (this.#wrappedElements.has(el)) return false;
@@ -485,9 +1032,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
485
1032
  _syncCheckboxes() {
486
1033
  const inCheckedMode = this.interactionMode === `checked`;
487
1034
  const wantsCustom = typeof this.checkboxRenderer === `function`;
488
- if (inCheckedMode) for (const inner of Array.from(this.children)) {
1035
+ const roots = this.#virtualActive() ? [this, this.#pool] : [this];
1036
+ if (inCheckedMode) for (const root of roots) for (const inner of Array.from(root.children)) {
489
1037
  if (!inner.hasAttribute(`data-list-item`)) continue;
490
- if (inner.parentElement !== this) continue;
1038
+ if (inner.parentNode !== root) continue;
491
1039
  const wrapper = document.createElement(`li`);
492
1040
  wrapper.className = `ixfx-custom-row`;
493
1041
  const key = inner.getAttribute(`data-key`);
@@ -495,17 +1043,22 @@ let VerticalListElement = class VerticalListElement extends LitElement {
495
1043
  if (key !== null) wrapper.setAttribute(`data-key`, key);
496
1044
  if (label !== null) wrapper.setAttribute(`data-search-label`, label);
497
1045
  inner.removeAttribute(`data-list-item`);
498
- this.insertBefore(wrapper, inner);
1046
+ root.insertBefore(wrapper, inner);
499
1047
  wrapper.appendChild(inner);
500
1048
  this.#customWrappers.set(wrapper, inner);
501
1049
  this.#wrappedElements.add(inner);
1050
+ if (this.#itemData.has(inner)) this.#itemData.set(wrapper, this.#itemData.get(inner));
1051
+ if (this.#virtualActive()) this.#swapRegistry(inner, wrapper);
502
1052
  }
503
1053
  else {
504
1054
  for (const [wrapper, inner] of this.#customWrappers) {
505
- if (wrapper.parentElement !== this) continue;
1055
+ if (wrapper.parentNode !== this && wrapper.parentNode !== this.#pool) continue;
506
1056
  inner.setAttribute(`data-list-item`, ``);
507
- this.insertBefore(inner, wrapper);
1057
+ const parent = wrapper.parentNode;
1058
+ if (parent) parent.insertBefore(inner, wrapper);
508
1059
  wrapper.remove();
1060
+ this.#itemData.delete(wrapper);
1061
+ if (this.#virtualActive()) this.#swapRegistry(wrapper, inner);
509
1062
  }
510
1063
  this.#customWrappers.clear();
511
1064
  this.#wrappedElements.clear();
@@ -681,8 +1234,11 @@ let VerticalListElement = class VerticalListElement extends LitElement {
681
1234
  outline: 1px solid transparent;
682
1235
  }
683
1236
 
684
- /* Custom shadow-DOM components handle their own internal layout/padding */
685
- ::slotted([data-list-item]) {
1237
+ /* Custom shadow-DOM components handle their own internal layout/padding.
1238
+ Wrapped in :where() so the selector stays at zero specificity and a
1239
+ consumer's own :host rule (e.g. display: flex) wins without
1240
+ needing !important. */
1241
+ :where(::slotted([data-list-item])) {
686
1242
  display: block;
687
1243
  margin: 0 var(--list-gutter, 4px);
688
1244
  cursor: pointer;
@@ -692,6 +1248,23 @@ let VerticalListElement = class VerticalListElement extends LitElement {
692
1248
  outline: 1px solid transparent;
693
1249
  }
694
1250
 
1251
+ /* Virtualization spacers: plain height holders bracketing the rendered
1252
+ window. Attribute specificity beats ::slotted(li) so none of the row
1253
+ layout (flex, padding, min-height, gutters) applies; pointer events are
1254
+ off so clicks land on the scroll container, not a phantom row. */
1255
+ ::slotted([data-virtual-spacer]) {
1256
+ display: block;
1257
+ padding: 0;
1258
+ margin: 0;
1259
+ border: none;
1260
+ min-height: 0;
1261
+ overflow: hidden;
1262
+ cursor: default;
1263
+ pointer-events: none;
1264
+ outline: none;
1265
+ user-select: none;
1266
+ }
1267
+
695
1268
  /* In checked mode, only the checkbox column is clickable; the row body
696
1269
  gets a default cursor so it doesn't suggest otherwise. */
697
1270
  :host([interaction-mode="checked"]) ::slotted(li),
@@ -766,7 +1339,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
766
1339
  render() {
767
1340
  return html`
768
1341
  <div class="container">
769
- <ul part="list">
1342
+ <ul part="list" @scroll=${this.#onScroll}>
770
1343
  <slot></slot>
771
1344
  </ul>
772
1345
  ${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
@@ -800,6 +1373,22 @@ __decorate([property({
800
1373
  reflect: true
801
1374
  })], VerticalListElement.prototype, "itemsDraggable", void 0);
802
1375
  __decorate([property({ attribute: false })], VerticalListElement.prototype, "dragPreviewRenderer", void 0);
1376
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "dragPreviewSource", void 0);
1377
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "items", void 0);
1378
+ __decorate([property({
1379
+ type: String,
1380
+ attribute: `key-property`
1381
+ })], VerticalListElement.prototype, "keyProperty", void 0);
1382
+ __decorate([property({ attribute: false })], VerticalListElement.prototype, "formatter", void 0);
1383
+ __decorate([property({
1384
+ type: Boolean,
1385
+ attribute: `virtualize`,
1386
+ reflect: true
1387
+ })], VerticalListElement.prototype, "virtualize", void 0);
1388
+ __decorate([property({
1389
+ type: Number,
1390
+ attribute: `item-height`
1391
+ })], VerticalListElement.prototype, "itemHeight", void 0);
803
1392
  VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
804
1393
  //#endregion
805
1394
  export { VerticalListElement };