@ixfx/components 0.11.7 → 0.11.9

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 (104) hide show
  1. package/bundle/index.d.ts +165 -2
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +1160 -164
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/{ac-token--_vCHXWw.d.ts → ac-token-TCLAZeVg.d.ts} +2 -2
  7. package/dist/{ac-token--_vCHXWw.d.ts.map → ac-token-TCLAZeVg.d.ts.map} +1 -1
  8. package/dist/ac-token.d.ts +1 -1
  9. package/dist/colour-picker.d.ts +1 -1
  10. package/dist/crumbs.d.ts +1 -1
  11. package/dist/crumbs.js +1 -1
  12. package/dist/{data-grid-C6EjCe5O.d.ts → data-grid-Oiyt5qbA.d.ts} +4 -4
  13. package/dist/{data-grid-C6EjCe5O.d.ts.map → data-grid-Oiyt5qbA.d.ts.map} +1 -1
  14. package/dist/data-grid.d.ts +1 -1
  15. package/dist/data-grid.js +1 -1
  16. package/dist/{detail-list-CgkOHYOa.d.ts → detail-list-B0waVNeS.d.ts} +37 -6
  17. package/dist/detail-list-B0waVNeS.d.ts.map +1 -0
  18. package/dist/detail-list.d.ts +4 -3
  19. package/dist/detail-list.js +271 -6
  20. package/dist/detail-list.js.map +1 -1
  21. package/dist/{grid-list-3fGhdaFA.js → grid-list-Bwaf61is.js} +659 -158
  22. package/dist/grid-list-Bwaf61is.js.map +1 -0
  23. package/dist/{grid-list-D6AfWfwn.d.ts → grid-list-bzKuNTtu.d.ts} +43 -2
  24. package/dist/grid-list-bzKuNTtu.d.ts.map +1 -0
  25. package/dist/grid-list.d.ts +3 -2
  26. package/dist/grid-list.js +2 -1
  27. package/dist/{grouped-item-lister-DETte0Dh.d.ts → grouped-item-lister-Bhyo-dkH.d.ts} +2 -2
  28. package/dist/{grouped-item-lister-DETte0Dh.d.ts.map → grouped-item-lister-Bhyo-dkH.d.ts.map} +1 -1
  29. package/dist/grouped-item-lister.d.ts +1 -1
  30. package/dist/grouping-CUKgmkMm.d.ts +63 -0
  31. package/dist/grouping-CUKgmkMm.d.ts.map +1 -0
  32. package/dist/grouping-RYPnHkrS.js +53 -0
  33. package/dist/grouping-RYPnHkrS.js.map +1 -0
  34. package/dist/{icon-CDGprSdE.d.ts → icon-DR_3rSyZ.d.ts} +2 -2
  35. package/dist/icon-DR_3rSyZ.d.ts.map +1 -0
  36. package/dist/icons.d.ts +2 -2
  37. package/dist/incr-search.d.ts +1 -1
  38. package/dist/{index-tq6f8007.d.ts → index-BOUaDIW-.d.ts} +2 -2
  39. package/dist/{index-tq6f8007.d.ts.map → index-BOUaDIW-.d.ts.map} +1 -1
  40. package/dist/{index-CoqtMSd9.d.ts → index-BUB5SICW.d.ts} +2 -2
  41. package/dist/{index-CoqtMSd9.d.ts.map → index-BUB5SICW.d.ts.map} +1 -1
  42. package/dist/{index-CMsmZ6Or.d.ts → index-BvxHQUME.d.ts} +2 -2
  43. package/dist/{index-CMsmZ6Or.d.ts.map → index-BvxHQUME.d.ts.map} +1 -1
  44. package/dist/{index-UvnoZUAo.d.ts → index-CJ5fshD3.d.ts} +2 -2
  45. package/dist/{index-UvnoZUAo.d.ts.map → index-CJ5fshD3.d.ts.map} +1 -1
  46. package/dist/{index-DyTSt9OK.d.ts → index-CW2QL33o.d.ts} +3 -3
  47. package/dist/{index-DyTSt9OK.d.ts.map → index-CW2QL33o.d.ts.map} +1 -1
  48. package/dist/{index-CYndvTWH.d.ts → index-garxxqgV.d.ts} +2 -2
  49. package/dist/{index-CYndvTWH.d.ts.map → index-garxxqgV.d.ts.map} +1 -1
  50. package/dist/index.d.ts +20 -18
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +7 -5
  53. package/dist/index.js.map +1 -1
  54. package/dist/{labelled-input-base-DiTme1JO.d.ts → labelled-input-base-Dp_9KP0G.d.ts} +2 -2
  55. package/dist/{labelled-input-base-DiTme1JO.d.ts.map → labelled-input-base-Dp_9KP0G.d.ts.map} +1 -1
  56. package/dist/labelled-radial-input.d.ts +2 -2
  57. package/dist/labelled-radial-input.d.ts.map +1 -1
  58. package/dist/labelled-range-input.d.ts +1 -1
  59. package/dist/labelled-slider-input.d.ts +1 -1
  60. package/dist/list-group-header.d.ts +26 -0
  61. package/dist/list-group-header.d.ts.map +1 -0
  62. package/dist/list-group-header.js +122 -0
  63. package/dist/list-group-header.js.map +1 -0
  64. package/dist/{list-selection-controller-SA4piSBi.js → list-selection-controller-BeE70lbB.js} +42 -4
  65. package/dist/list-selection-controller-BeE70lbB.js.map +1 -0
  66. package/dist/{menu-CsBUyUuS.js → menu-DaAlsyhf.js} +2 -2
  67. package/dist/{menu-CsBUyUuS.js.map → menu-DaAlsyhf.js.map} +1 -1
  68. package/dist/{menu-item-BbgdcwBX.d.ts → menu-item-B0zqw2xp.d.ts} +14 -2
  69. package/dist/menu-item-B0zqw2xp.d.ts.map +1 -0
  70. package/dist/{menu-item-CfndBGO8.js → menu-item-CDPFgaSF.js} +32 -1
  71. package/dist/menu-item-CDPFgaSF.js.map +1 -0
  72. package/dist/menu.d.ts +2 -2
  73. package/dist/menu.js +2 -2
  74. package/dist/miller.d.ts +1 -1
  75. package/dist/panel.d.ts +1 -1
  76. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-C5SYbDrq.d.ts} +2 -2
  77. package/dist/{split-layout-JK3xWVX2.d.ts.map → split-layout-C5SYbDrq.d.ts.map} +1 -1
  78. package/dist/split-layout.d.ts +1 -1
  79. package/dist/{tab-list-pqfKaVAE.d.ts → tab-list-v8mKX7Fu.d.ts} +2 -2
  80. package/dist/{tab-list-pqfKaVAE.d.ts.map → tab-list-v8mKX7Fu.d.ts.map} +1 -1
  81. package/dist/tabs.d.ts +1 -1
  82. package/dist/{titlebar-Tqu-VvMU.d.ts → titlebar-B5zSTZnF.d.ts} +2 -2
  83. package/dist/{titlebar-Tqu-VvMU.d.ts.map → titlebar-B5zSTZnF.d.ts.map} +1 -1
  84. package/dist/titlebar.d.ts +1 -1
  85. package/dist/tree.d.ts +1 -1
  86. package/dist/{vertical-list-DTp-UIUE.d.ts → vertical-list-DheZSEmB.d.ts} +2 -2
  87. package/dist/{vertical-list-DTp-UIUE.d.ts.map → vertical-list-DheZSEmB.d.ts.map} +1 -1
  88. package/dist/vertical-list.d.ts +1 -1
  89. package/dist/vertical-list.js +1 -1
  90. package/docs-user/README.md +1 -0
  91. package/docs-user/detail-list.md +28 -0
  92. package/docs-user/grid-list.md +35 -3
  93. package/docs-user/index.json +7 -2
  94. package/docs-user/list-group-header.md +14 -0
  95. package/docs-user/llms.txt +1 -0
  96. package/llms.txt +1 -0
  97. package/package.json +5 -1
  98. package/dist/detail-list-CgkOHYOa.d.ts.map +0 -1
  99. package/dist/grid-list-3fGhdaFA.js.map +0 -1
  100. package/dist/grid-list-D6AfWfwn.d.ts.map +0 -1
  101. package/dist/icon-CDGprSdE.d.ts.map +0 -1
  102. package/dist/list-selection-controller-SA4piSBi.js.map +0 -1
  103. package/dist/menu-item-BbgdcwBX.d.ts.map +0 -1
  104. package/dist/menu-item-CfndBGO8.js.map +0 -1
package/bundle/index.js CHANGED
@@ -5549,6 +5549,56 @@ __decorate([state()], RadioGroup.prototype, "_icons", void 0);
5549
5549
  __decorate([query(`slot`)], RadioGroup.prototype, "_slot", void 0);
5550
5550
  RadioGroup = __decorate([safeCustomElement(`ixfx-radio-group`)], RadioGroup);
5551
5551
  //#endregion
5552
+ //#region src/interaction/grouping.ts
5553
+ /** Label shown for the empty group key. */
5554
+ const EMPTY_GROUP_LABEL = `(none)`;
5555
+ /** Default header label: the key, or `(none)` for the empty key. */
5556
+ function defaultGroupLabel(key) {
5557
+ return key === `` ? EMPTY_GROUP_LABEL : key;
5558
+ }
5559
+ /** Coerce a raw group value to a key, as data-grid does. */
5560
+ function toGroupKey(value) {
5561
+ return String(value ?? ``);
5562
+ }
5563
+ /**
5564
+ * Group key for `item`. A string `groupBy` is resolved through `readNamed`
5565
+ * (property or `data-*` lookup, supplied by the host); a function is called.
5566
+ */
5567
+ function resolveGroupKey(groupBy, item, index, readNamed) {
5568
+ if (typeof groupBy === `string`) return toGroupKey(readNamed(item, groupBy));
5569
+ return toGroupKey(groupBy(item, index));
5570
+ }
5571
+ /**
5572
+ * Split `items` into groups in first-encounter order. Single pass, O(n).
5573
+ * Item order within a group is preserved.
5574
+ */
5575
+ function partitionByGroup(items, keyOf, labelOf) {
5576
+ const byKey = /* @__PURE__ */ new Map();
5577
+ for (let i = 0; i < items.length; i++) {
5578
+ const key = keyOf(items[i], i);
5579
+ const existing = byKey.get(key);
5580
+ if (existing) existing.items.push(items[i]);
5581
+ else byKey.set(key, {
5582
+ key,
5583
+ items: [items[i]],
5584
+ firstIndex: i
5585
+ });
5586
+ }
5587
+ return [...byKey.values()].map((g) => ({
5588
+ key: g.key,
5589
+ items: g.items,
5590
+ firstIndex: g.firstIndex,
5591
+ label: labelOf ? labelOf(g.key, g.items) : defaultGroupLabel(g.key)
5592
+ }));
5593
+ }
5594
+ /** Immutable toggle of `key` in `set`. */
5595
+ function toggleInSet(set, key) {
5596
+ const next = new Set(set);
5597
+ if (next.has(key)) next.delete(key);
5598
+ else next.add(key);
5599
+ return next;
5600
+ }
5601
+ //#endregion
5552
5602
  //#region src/interaction/list-drag-preview-default.ts
5553
5603
  const CARD_DEFAULT = 72;
5554
5604
  const CARD_MIN = 48;
@@ -6670,6 +6720,10 @@ var ListSelectionController = class {
6670
6720
  if (next.has(item)) next.delete(item);
6671
6721
  else {
6672
6722
  if (!this.canSelectMultiple()) next.clear();
6723
+ if (this.#clamped()) {
6724
+ const g = this.#groupOf(item);
6725
+ for (const other of [...next]) if (this.#groupOf(other) !== g) next.delete(other);
6726
+ }
6673
6727
  next.add(item);
6674
6728
  }
6675
6729
  this.#apply(next);
@@ -6688,6 +6742,22 @@ var ListSelectionController = class {
6688
6742
  this.replace(item);
6689
6743
  return;
6690
6744
  }
6745
+ if (this.#clamped()) {
6746
+ const group = this.#groupOf(this.#anchor);
6747
+ if (group !== this.#groupOf(item)) {
6748
+ this.replace(item);
6749
+ return;
6750
+ }
6751
+ const inGroup = ordered.filter((t) => this.#groupOf(t) === group);
6752
+ const a = inGroup.indexOf(this.#anchor);
6753
+ const b = inGroup.indexOf(item);
6754
+ if (a === -1 || b === -1) {
6755
+ this.replace(item);
6756
+ return;
6757
+ }
6758
+ this.#apply(new Set(inGroup.slice(Math.min(a, b), Math.max(a, b) + 1).filter((t) => this.#config.isSelectable(t))));
6759
+ return;
6760
+ }
6691
6761
  const start = Math.min(anchorIdx, targetIdx);
6692
6762
  const end = Math.max(anchorIdx, targetIdx);
6693
6763
  this.#apply(new Set(ordered.slice(start, end + 1).filter((t) => this.#config.isSelectable(t))));
@@ -6699,8 +6769,12 @@ var ListSelectionController = class {
6699
6769
  }
6700
6770
  /** Select every ordered item that passes `isSelectable`. */
6701
6771
  selectAll() {
6702
- const ordered = this.#config.getOrderedItems();
6772
+ let ordered = this.#config.getOrderedItems();
6703
6773
  if (ordered.length === 0) return;
6774
+ if (this.#clamped()) {
6775
+ const group = (this.#anchor === void 0 ? null : this.#groupOf(this.#anchor)) ?? this.#config.getFallbackGroupKey?.() ?? null;
6776
+ if (group !== null) ordered = ordered.filter((t) => this.#groupOf(t) === group);
6777
+ }
6704
6778
  const next = new Set(ordered.filter((t) => this.#config.isSelectable(t)));
6705
6779
  if (next.size === 0) return;
6706
6780
  this.#apply(next);
@@ -6712,7 +6786,7 @@ var ListSelectionController = class {
6712
6786
  selectMany(items) {
6713
6787
  const { selectionMode } = this.#config.getModes();
6714
6788
  if (selectionMode !== `multiple`) throw new Error(`selectMany requires selectionMode="multiple" (current: "${selectionMode}")`);
6715
- const next = new Set([...items].filter((t) => this.#config.isSelectable(t)));
6789
+ const next = this.#clampToFirstGroup(new Set([...items].filter((t) => this.#config.isSelectable(t))));
6716
6790
  if (next.size === 0) return;
6717
6791
  this.#anchor = [...next].at(-1);
6718
6792
  this.#apply(next);
@@ -6737,7 +6811,7 @@ var ListSelectionController = class {
6737
6811
  */
6738
6812
  applyDragSelection(items) {
6739
6813
  if (!this.canDragSelect()) return;
6740
- const next = new Set([...items].filter((t) => this.#config.isSelectable(t)));
6814
+ const next = this.#clampToFirstGroup(new Set([...items].filter((t) => this.#config.isSelectable(t))));
6741
6815
  if (next.size === 0 && this.#selected.size === 0) return;
6742
6816
  this.#anchor = [...next].at(-1);
6743
6817
  this.#apply(next);
@@ -6866,6 +6940,20 @@ var ListSelectionController = class {
6866
6940
  this.#selected = new Set(items);
6867
6941
  this.#anchor = anchor;
6868
6942
  }
6943
+ /** Whether gestures must stay inside one group. */
6944
+ #clamped() {
6945
+ return this.#config.getGroupKey !== void 0 && !(this.#config.crossGroupSelection?.() ?? false);
6946
+ }
6947
+ #groupOf(item) {
6948
+ return this.#config.getGroupKey?.(item) ?? null;
6949
+ }
6950
+ /** Keep only items in the group of the first item (in display order) when clamped. */
6951
+ #clampToFirstGroup(items) {
6952
+ if (!this.#clamped() || items.size === 0) return new Set(items);
6953
+ const first = this.#config.getOrderedItems().find((t) => items.has(t)) ?? [...items][0];
6954
+ const group = this.#groupOf(first);
6955
+ return new Set([...items].filter((t) => this.#groupOf(t) === group));
6956
+ }
6869
6957
  #apply(next) {
6870
6958
  const previous = this.#selected;
6871
6959
  this.#selected = next;
@@ -7249,6 +7337,8 @@ let MenuTrigger = class MenuTrigger extends LitElement {
7249
7337
  this._anchorName = `--mt-${_MenuTrigger._count++}`;
7250
7338
  this._clickListener = this._handleClickOutside.bind(this);
7251
7339
  this._focusBeforeOpen = null;
7340
+ this._mouseHeld = false;
7341
+ this._openOnRelease = false;
7252
7342
  this._handlePopoverToggle = (e) => {
7253
7343
  if (e.newState === `closed`) {
7254
7344
  this._isOpen = false;
@@ -7271,6 +7361,11 @@ let MenuTrigger = class MenuTrigger extends LitElement {
7271
7361
  this._restoreFocus();
7272
7362
  }
7273
7363
  };
7364
+ this._handleRelease = () => {
7365
+ const shouldOpen = this._openOnRelease;
7366
+ this._removeReleaseListeners();
7367
+ if (shouldOpen) setTimeout(() => this._openMenu(), 0);
7368
+ };
7274
7369
  }
7275
7370
  static {
7276
7371
  this._count = 0;
@@ -7287,6 +7382,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
7287
7382
  this.removeEventListener(`command`, this._handleCommand);
7288
7383
  this.removeEventListener(`radio-change`, this._handleRadioChange);
7289
7384
  document.removeEventListener(`click`, this._clickListener);
7385
+ this._removeReleaseListeners();
7290
7386
  }
7291
7387
  firstUpdated() {
7292
7388
  this.style.setProperty(`--anchor-name`, this._anchorName);
@@ -7453,6 +7549,24 @@ let MenuTrigger = class MenuTrigger extends LitElement {
7453
7549
  composed: true
7454
7550
  }));
7455
7551
  }
7552
+ _handlePointerDown(e) {
7553
+ if (this.mode !== `context` || e.pointerType !== `mouse`) return;
7554
+ this._mouseHeld = true;
7555
+ document.addEventListener(`pointerup`, this._handleRelease, {
7556
+ capture: true,
7557
+ once: true
7558
+ });
7559
+ document.addEventListener(`pointercancel`, this._handleRelease, {
7560
+ capture: true,
7561
+ once: true
7562
+ });
7563
+ }
7564
+ _removeReleaseListeners() {
7565
+ document.removeEventListener(`pointerup`, this._handleRelease, { capture: true });
7566
+ document.removeEventListener(`pointercancel`, this._handleRelease, { capture: true });
7567
+ this._mouseHeld = false;
7568
+ this._openOnRelease = false;
7569
+ }
7456
7570
  _handleContextMenu(e) {
7457
7571
  if (this.mode !== `context`) return;
7458
7572
  e.preventDefault();
@@ -7460,6 +7574,10 @@ let MenuTrigger = class MenuTrigger extends LitElement {
7460
7574
  this._popup.style.left = `${e.clientX}px`;
7461
7575
  this._popup.style.position = `fixed`;
7462
7576
  this._popup.style.positionAnchor = `none`;
7577
+ if (this._mouseHeld) {
7578
+ this._openOnRelease = true;
7579
+ return;
7580
+ }
7463
7581
  this._openMenu();
7464
7582
  }
7465
7583
  static {
@@ -7572,6 +7690,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
7572
7690
  class="trigger-area"
7573
7691
  @click=${this._handleTriggerClick}
7574
7692
  @contextmenu=${this._handleContextMenu}
7693
+ @pointerdown=${this._handlePointerDown}
7575
7694
  >
7576
7695
  ${showButton ? html`<button class="trigger-btn" part="base" tabindex=${this.tabStop ? `0` : `-1`}>${this.iconName ? html`<ixfx-icon name=${this.iconName}></ixfx-icon>` : this.label || html`<ixfx-icon name="ellipsis-vertical"></ixfx-icon>`}</button>` : null}
7577
7696
  ${this.mode === `context` ? html`<slot name="context-target" @slotchange=${this._handleContextTargetSlotChange}></slot>` : null}
@@ -17173,8 +17292,125 @@ __decorate([state()], DataTableElement.prototype, "_detailIndex", void 0);
17173
17292
  __decorate([state()], DataTableElement.prototype, "_stacked", void 0);
17174
17293
  DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableElement);
17175
17294
  //#endregion
17295
+ //#region src/list-group-header/list-group-header.ts
17296
+ let ListGroupHeaderElement = class ListGroupHeaderElement extends LitElement {
17297
+ constructor(..._args) {
17298
+ super(..._args);
17299
+ this.label = ``;
17300
+ this.count = 0;
17301
+ this.collapsed = false;
17302
+ this.orientation = `horizontal`;
17303
+ }
17304
+ static {
17305
+ this.styles = css`
17306
+ :host {
17307
+ --_height: var(--list-group-header-height, 28px);
17308
+ --_border: var(--list-group-header-border, var(--border));
17309
+
17310
+ display: flex;
17311
+ align-items: center;
17312
+ gap: var(--space-s);
17313
+ box-sizing: border-box;
17314
+ min-height: var(--_height);
17315
+ padding: 0 var(--space-s);
17316
+ background: transparent;
17317
+ color: var(--list-group-header-text, var(--surface-3-text));
17318
+ border-bottom: 1px solid var(--_border);
17319
+ font-weight: 600;
17320
+ cursor: pointer;
17321
+ user-select: none;
17322
+ }
17323
+
17324
+ .caret {
17325
+ display: inline-flex;
17326
+ transition: transform 0.15s;
17327
+ }
17328
+
17329
+ :host([collapsed]) .caret {
17330
+ transform: rotate(-90deg);
17331
+ }
17332
+
17333
+ .count {
17334
+ opacity: 0.6;
17335
+ font-weight: 400;
17336
+ font-size: 0.9em;
17337
+ }
17338
+
17339
+ .label {
17340
+ overflow: hidden;
17341
+ text-overflow: ellipsis;
17342
+ white-space: nowrap;
17343
+ min-width: 0;
17344
+ }
17345
+
17346
+ .divider {
17347
+ flex: 1;
17348
+ height: 1px;
17349
+ background: var(--_border);
17350
+ margin-left: var(--space-s);
17351
+ }
17352
+
17353
+ /* Single-row (grid-list \`row\` orientation) layout: a narrow column cell. */
17354
+ :host([orientation="vertical"]) {
17355
+ flex-direction: column;
17356
+ justify-content: flex-start;
17357
+ padding: var(--space-s) 0;
17358
+ border-bottom: 0;
17359
+ border-right: 1px solid var(--_border);
17360
+ min-height: 0;
17361
+ }
17362
+
17363
+ :host([orientation="vertical"]) .divider {
17364
+ width: 1px;
17365
+ height: auto;
17366
+ margin-left: 0;
17367
+ margin-top: var(--space-s);
17368
+ }
17369
+
17370
+ @media (prefers-reduced-motion: reduce) {
17371
+ .caret { transition: none; }
17372
+ }
17373
+ `;
17374
+ }
17375
+ connectedCallback() {
17376
+ super.connectedCallback();
17377
+ this.setAttribute(`role`, `button`);
17378
+ }
17379
+ willUpdate() {
17380
+ this.setAttribute(`aria-expanded`, this.collapsed ? `false` : `true`);
17381
+ this.setAttribute(`aria-label`, `${this.label}, ${this.count} ${this.count === 1 ? `item` : `items`}`);
17382
+ }
17383
+ render() {
17384
+ return html`
17385
+ <span class="caret"><ixfx-icon name="chevron-down" size="12"></ixfx-icon></span>
17386
+ <span class="label" part="label">${this.label}</span>
17387
+ <span class="count" part="count">(${this.count})</span>
17388
+ <span class="divider" part="divider"></span>
17389
+ `;
17390
+ }
17391
+ };
17392
+ __decorate([property({
17393
+ type: String,
17394
+ reflect: true
17395
+ })], ListGroupHeaderElement.prototype, "label", void 0);
17396
+ __decorate([property({
17397
+ type: Number,
17398
+ reflect: true
17399
+ })], ListGroupHeaderElement.prototype, "count", void 0);
17400
+ __decorate([property({
17401
+ type: Boolean,
17402
+ reflect: true
17403
+ })], ListGroupHeaderElement.prototype, "collapsed", void 0);
17404
+ __decorate([property({
17405
+ type: String,
17406
+ reflect: true
17407
+ })], ListGroupHeaderElement.prototype, "orientation", void 0);
17408
+ ListGroupHeaderElement = __decorate([safeCustomElement(`ixfx-list-group-header`)], ListGroupHeaderElement);
17409
+ //#endregion
17176
17410
  //#region src/detail-list/detail-list.ts
17177
17411
  const ITEM_SELECTOR$3 = `:scope > li`;
17412
+ const GROUP_HEADER_ATTR = `data-ixfx-group-header`;
17413
+ const GROUP_COLLAPSED_ATTR = `data-group-collapsed`;
17178
17414
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
17179
17415
  const NAV_KEYS = /* @__PURE__ */ new Set([
17180
17416
  `ArrowDown`,
@@ -17232,14 +17468,26 @@ let DetailListElement = class DetailListElement extends LitElement {
17232
17468
  this.keyProperty = ``;
17233
17469
  this.itemWrap = `stop`;
17234
17470
  this.itemsDraggable = false;
17471
+ this.groupBy = null;
17472
+ this.crossGroupSelection = false;
17473
+ this._collapsedGroups = /* @__PURE__ */ new Set();
17235
17474
  this.#itemData = /* @__PURE__ */ new WeakMap();
17236
17475
  this.#grid = [];
17237
17476
  this.#positions = /* @__PURE__ */ new Map();
17477
+ this.#tops = /* @__PURE__ */ new Map();
17238
17478
  this.#geometryScheduled = false;
17239
17479
  this.#sortRaf = 0;
17240
17480
  this.#viewportScrollRaf = 0;
17241
17481
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
17242
17482
  this.#builtFromProvider = [];
17483
+ this.#groupIndex = /* @__PURE__ */ new Map();
17484
+ this.#groupList = [];
17485
+ this.#headerEls = /* @__PURE__ */ new Map();
17486
+ this.#seq = /* @__PURE__ */ new WeakMap();
17487
+ this.#seqNext = 0;
17488
+ this.#wasGrouped = false;
17489
+ this.#groupsScheduled = false;
17490
+ this.#groupByChanged = false;
17243
17491
  this.#providerBuilt = 0;
17244
17492
  this.#data = new DataController(this);
17245
17493
  this.selection = new ListSelectionController({
@@ -17250,6 +17498,12 @@ let DetailListElement = class DetailListElement extends LitElement {
17250
17498
  interactionMode: this.interactionMode
17251
17499
  }),
17252
17500
  getKey: keyForElement,
17501
+ getGroupKey: (el) => this.#groupIndex.get(el) ?? null,
17502
+ crossGroupSelection: () => this.crossGroupSelection,
17503
+ getFallbackGroupKey: () => {
17504
+ const t = this.tickled.tickledId;
17505
+ return t ? this.#groupIndex.get(t) ?? null : null;
17506
+ },
17253
17507
  onChange: (selected, previous) => {
17254
17508
  this._syncItemStates();
17255
17509
  this._dispatch(`list-select`, {
@@ -17331,6 +17585,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17331
17585
  if (this._listEl) this._listEl.scrollLeft += dx;
17332
17586
  },
17333
17587
  shouldStart: (event) => {
17588
+ if (this.#findGroupHeader(event.composedPath())) return false;
17334
17589
  this.#blankItemPressPending = this.itemsDraggable && this.#isBlankItemPress(event);
17335
17590
  return !this.itemsDraggable || this.#blankItemPressPending;
17336
17591
  }
@@ -17422,7 +17677,13 @@ let DetailListElement = class DetailListElement extends LitElement {
17422
17677
  };
17423
17678
  this._onClick = (e) => {
17424
17679
  if (this.#dragSelect.isDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
17425
- const item = this._findListItem(e.composedPath());
17680
+ const path = e.composedPath();
17681
+ const header = this.#findGroupHeader(path);
17682
+ if (header) {
17683
+ this.toggleGroup(header.dataset.groupKey ?? ``);
17684
+ return;
17685
+ }
17686
+ const item = this._findListItem(path);
17426
17687
  if (!item) return;
17427
17688
  this.focus();
17428
17689
  this._dispatch(`list-item-click`, { item });
@@ -17441,6 +17702,8 @@ let DetailListElement = class DetailListElement extends LitElement {
17441
17702
  #itemData;
17442
17703
  #grid;
17443
17704
  #positions;
17705
+ /** Viewport-relative `top` per item at the last geometry pass — lines rows up across columns. */
17706
+ #tops;
17444
17707
  #geometryScheduled;
17445
17708
  #sortRaf;
17446
17709
  #viewportScrollRaf;
@@ -17452,6 +17715,17 @@ let DetailListElement = class DetailListElement extends LitElement {
17452
17715
  /** Items that have already played (or been skipped for) their enter animation. */
17453
17716
  #animatedItems;
17454
17717
  #builtFromProvider;
17718
+ /** Element → group key, for the selection controller. Empty while ungrouped. */
17719
+ #groupIndex;
17720
+ #groupList;
17721
+ #headerEls;
17722
+ /** Insertion sequence per item, so regrouping starts from the original order. */
17723
+ #seq;
17724
+ #seqNext;
17725
+ #wasGrouped;
17726
+ #groupsScheduled;
17727
+ #groupByChanged;
17728
+ #groupObserver;
17455
17729
  #providerBuilt;
17456
17730
  #data;
17457
17731
  #nativeDragging;
@@ -17568,6 +17842,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17568
17842
  this.#processChildren();
17569
17843
  this.#animateChildMutations(records);
17570
17844
  this.#scheduleGeometry();
17845
+ this.#scheduleGroups();
17571
17846
  });
17572
17847
  this.#mutationObserver.observe(this, {
17573
17848
  childList: true,
@@ -17577,6 +17852,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17577
17852
  for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#animatedItems.add(el);
17578
17853
  this.#searchRegistry ??= createCommandRegistry();
17579
17854
  this.#syncSearchCommands();
17855
+ this.#scheduleGroups();
17580
17856
  }
17581
17857
  /**
17582
17858
  * Collapse-in items appended to the light DOM (however they got there). A bulk
@@ -17618,6 +17894,8 @@ let DetailListElement = class DetailListElement extends LitElement {
17618
17894
  this.#resizeObserver = void 0;
17619
17895
  this.#mutationObserver?.disconnect();
17620
17896
  this.#mutationObserver = void 0;
17897
+ this.#groupObserver?.disconnect();
17898
+ this.#groupObserver = void 0;
17621
17899
  this.#unregisterSearchCommands?.();
17622
17900
  this.#unregisterSearchCommands = void 0;
17623
17901
  this.#searchRegistry = void 0;
@@ -17632,8 +17910,22 @@ let DetailListElement = class DetailListElement extends LitElement {
17632
17910
  this.#recomputeGeometry();
17633
17911
  this._syncNativeDraggable();
17634
17912
  }
17913
+ willUpdate(changed) {
17914
+ super.willUpdate(changed);
17915
+ if (changed.has(`groupBy`) && this.hasUpdated) {
17916
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
17917
+ this.#groupByChanged = true;
17918
+ }
17919
+ }
17635
17920
  updated(changed) {
17636
17921
  super.updated(changed);
17922
+ if (changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`)) {
17923
+ this.#scheduleGroups();
17924
+ if (this.#groupByChanged) {
17925
+ this.#groupByChanged = false;
17926
+ this._dispatch(`list-group-change`, { groupBy: this.groupBy });
17927
+ }
17928
+ }
17637
17929
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
17638
17930
  if (changed.has(`provider`)) {
17639
17931
  this.#builtFromProvider = [];
@@ -17650,6 +17942,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17650
17942
  if (changed.has(`filterPredicate`)) {
17651
17943
  this.#applyFilterPredicate();
17652
17944
  this.#scheduleGeometry();
17945
+ this.#scheduleGroups();
17653
17946
  }
17654
17947
  if (changed.has(`interactionMode`)) {
17655
17948
  this.selection.clearAnchor();
@@ -17689,6 +17982,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17689
17982
  this.#ensureIcon(el);
17690
17983
  if (this.sort !== `none`) this.#applySort();
17691
17984
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17985
+ this.#scheduleGroups();
17692
17986
  }
17693
17987
  /**
17694
17988
  * Collapse + fade `item` out, then detach it; the rows after it slide up to
@@ -17698,6 +17992,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17698
17992
  this.selection.deselect(item);
17699
17993
  return animateListItemOut(item).then(() => {
17700
17994
  item.remove();
17995
+ this.#scheduleGroups();
17701
17996
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17702
17997
  this.#recomputeGeometry();
17703
17998
  });
@@ -17708,6 +18003,8 @@ let DetailListElement = class DetailListElement extends LitElement {
17708
18003
  }
17709
18004
  clearItems() {
17710
18005
  this.replaceChildren();
18006
+ this.#headerEls.clear();
18007
+ this.#scheduleGroups();
17711
18008
  this.selection.reset();
17712
18009
  this.#builtFromProvider = [];
17713
18010
  this.#providerBuilt = 0;
@@ -17725,6 +18022,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17725
18022
  }
17726
18023
  if (this.sort !== `none`) this.#applySort();
17727
18024
  if (this.interactionMode === `checked`) this._syncCheckboxes();
18025
+ this.#scheduleGroups();
17728
18026
  }
17729
18027
  /**
17730
18028
  * Get the source object behind an `<li>` built from an `items` array or
@@ -17844,6 +18142,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17844
18142
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17845
18143
  this._syncItemStates();
17846
18144
  this.#scheduleGeometry();
18145
+ this.#scheduleGroups();
17847
18146
  }
17848
18147
  #createItemEl(item, index) {
17849
18148
  const li = document.createElement(`li`);
@@ -17895,6 +18194,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17895
18194
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17896
18195
  this._syncItemStates();
17897
18196
  this.#scheduleGeometry();
18197
+ this.#scheduleGroups();
17898
18198
  } else {
17899
18199
  this.#providerBuilt = 0;
17900
18200
  this.#rebuildFromObjects(items);
@@ -17913,6 +18213,16 @@ let DetailListElement = class DetailListElement extends LitElement {
17913
18213
  if (this.sort === `none`) return;
17914
18214
  const rows = [...this.querySelectorAll(ITEM_SELECTOR$3)];
17915
18215
  if (rows.length < 2) return;
18216
+ if (this.#groupingActive()) {
18217
+ this.#scheduleGroups();
18218
+ return;
18219
+ }
18220
+ for (const el of this.#sortRows(rows)) this.appendChild(el);
18221
+ this.#scheduleGeometry();
18222
+ this.#scheduleGroups();
18223
+ }
18224
+ /** Stable sort of `rows` by the current `sort` / `sortBy` / `sortComparator`. */
18225
+ #sortRows(rows) {
17916
18226
  const dir = this.sort === `descending` ? -1 : 1;
17917
18227
  const meta = (el) => ({
17918
18228
  element: el,
@@ -17942,9 +18252,147 @@ let DetailListElement = class DetailListElement extends LitElement {
17942
18252
  i
17943
18253
  }));
17944
18254
  indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
17945
- for (const { el } of indexed) this.appendChild(el);
18255
+ return indexed.map(({ el }) => el);
18256
+ }
18257
+ #groupingActive() {
18258
+ return this.groupBy !== null && this.groupBy !== void 0 && this.groupBy !== ``;
18259
+ }
18260
+ /** Coalesce regrouping to one pass per microtask (streaming batches, search keystrokes). */
18261
+ #scheduleGroups() {
18262
+ if (this.#groupsScheduled || !this.#groupingActive() && !this.#wasGrouped) return;
18263
+ this.#groupsScheduled = true;
18264
+ queueMicrotask(() => {
18265
+ this.#groupsScheduled = false;
18266
+ this.#syncGroups();
18267
+ });
18268
+ }
18269
+ #groupValue(el, name) {
18270
+ return this.#propertyValue(this.getItemData(el), name) ?? el.getAttribute(`data-${name}`);
18271
+ }
18272
+ #groupKeyOf(el) {
18273
+ const by = this.groupBy;
18274
+ if (typeof by === `string`) return toGroupKey(this.#groupValue(el, by));
18275
+ return toGroupKey(by(this.getItemData(el), el));
18276
+ }
18277
+ #isItemVisible(el) {
18278
+ return !el.hidden && (!this.filterPredicate || this.filterPredicate(el));
18279
+ }
18280
+ #headerFor(key) {
18281
+ let header = this.#headerEls.get(key);
18282
+ if (!header) {
18283
+ header = document.createElement(`ixfx-list-group-header`);
18284
+ header.setAttribute(GROUP_HEADER_ATTR, ``);
18285
+ header.dataset.groupKey = key;
18286
+ this.#headerEls.set(key, header);
18287
+ }
18288
+ return header;
18289
+ }
18290
+ /**
18291
+ * Bring the light DOM into grouped order: original order → sort → partition
18292
+ * into contiguous groups, each preceded by a header row. Idempotent — it only
18293
+ * touches the DOM when the order actually changes, so it never loops through
18294
+ * the mutation observer. Ungrouping restores the original (sorted) order.
18295
+ */
18296
+ #syncGroups() {
18297
+ const active = this.#groupingActive();
18298
+ if (!active && !this.#wasGrouped) return;
18299
+ const items = [...this.querySelectorAll(ITEM_SELECTOR$3)];
18300
+ for (const el of items) if (!this.#seq.has(el)) this.#seq.set(el, this.#seqNext++);
18301
+ let ordered = items.toSorted((a, b) => this.#seq.get(a) - this.#seq.get(b));
18302
+ if (this.sort !== `none` && ordered.length > 1) ordered = this.#sortRows(ordered);
18303
+ const nodes = [];
18304
+ const info = [];
18305
+ const index = /* @__PURE__ */ new Map();
18306
+ const liveHeaders = /* @__PURE__ */ new Set();
18307
+ if (active) {
18308
+ const label = this.groupLabel;
18309
+ const parts = partitionByGroup(ordered, (el) => this.#groupKeyOf(el), label ? (key, els) => label(key, els.map((el) => this.getItemData(el) ?? el)) : void 0);
18310
+ for (const part of parts) {
18311
+ const count = part.items.filter((el) => this.#isItemVisible(el)).length;
18312
+ const collapsed = count > 0 && this._collapsedGroups.has(part.key);
18313
+ if (count > 0) {
18314
+ const header = this.#headerFor(part.key);
18315
+ header.label = part.label;
18316
+ header.count = count;
18317
+ header.collapsed = collapsed;
18318
+ liveHeaders.add(part.key);
18319
+ nodes.push(header);
18320
+ info.push({
18321
+ key: part.key,
18322
+ label: part.label,
18323
+ count,
18324
+ collapsed: this._collapsedGroups.has(part.key)
18325
+ });
18326
+ }
18327
+ for (const el of part.items) {
18328
+ index.set(el, part.key);
18329
+ el.toggleAttribute(GROUP_COLLAPSED_ATTR, collapsed);
18330
+ nodes.push(el);
18331
+ }
18332
+ }
18333
+ } else {
18334
+ for (const el of ordered) el.removeAttribute(GROUP_COLLAPSED_ATTR);
18335
+ nodes.push(...ordered);
18336
+ }
18337
+ for (const [key, header] of this.#headerEls) if (!liveHeaders.has(key)) {
18338
+ header.remove();
18339
+ this.#headerEls.delete(key);
18340
+ }
18341
+ this.#groupIndex = index;
18342
+ this.#groupList = info;
18343
+ this.#wasGrouped = active;
18344
+ const current = [...this.children].filter((c) => c.matches(`li`) || c.hasAttribute(GROUP_HEADER_ATTR));
18345
+ if (current.length !== nodes.length || current.some((c, i) => c !== nodes[i])) this.append(...nodes);
18346
+ if (this.tickled.tickledId?.hasAttribute(GROUP_COLLAPSED_ATTR)) this.tickled.focus(void 0);
18347
+ this.#syncGroupObserver(active);
17946
18348
  this.#scheduleGeometry();
17947
18349
  }
18350
+ /** Filter-search hides rows via `hidden` on the `<li>`s — regroup when that changes. */
18351
+ #syncGroupObserver(active) {
18352
+ if (!active) {
18353
+ this.#groupObserver?.disconnect();
18354
+ this.#groupObserver = void 0;
18355
+ return;
18356
+ }
18357
+ if (this.#groupObserver || !this.isConnected) return;
18358
+ this.#groupObserver = new MutationObserver(() => this.#scheduleGroups());
18359
+ this.#groupObserver.observe(this, {
18360
+ subtree: true,
18361
+ attributes: true,
18362
+ attributeFilter: [`hidden`]
18363
+ });
18364
+ }
18365
+ /** Groups in display order (empty when ungrouped). */
18366
+ get groups() {
18367
+ return this.#groupList;
18368
+ }
18369
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
18370
+ setGroupCollapsed(groupKey, collapsed) {
18371
+ if (this._collapsedGroups.has(groupKey) === collapsed) return;
18372
+ const next = new Set(this._collapsedGroups);
18373
+ if (collapsed) next.add(groupKey);
18374
+ else next.delete(groupKey);
18375
+ this._collapsedGroups = next;
18376
+ this._dispatch(`list-group-toggle`, {
18377
+ groupKey,
18378
+ collapsed
18379
+ });
18380
+ }
18381
+ toggleGroup(groupKey) {
18382
+ this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
18383
+ }
18384
+ collapseAllGroups() {
18385
+ for (const g of this.#groupList) this.setGroupCollapsed(g.key, true);
18386
+ }
18387
+ expandAllGroups() {
18388
+ for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
18389
+ }
18390
+ #findGroupHeader(path) {
18391
+ for (const target of path) {
18392
+ if (target === this) break;
18393
+ if (target instanceof HTMLElement && target.parentElement === this && target.hasAttribute(GROUP_HEADER_ATTR)) return target;
18394
+ }
18395
+ }
17948
18396
  #scheduleGeometry() {
17949
18397
  if (this.#geometryScheduled) return;
17950
18398
  this.#geometryScheduled = true;
@@ -17956,11 +18404,13 @@ let DetailListElement = class DetailListElement extends LitElement {
17956
18404
  #recomputeGeometry() {
17957
18405
  this.#grid = [];
17958
18406
  this.#positions.clear();
18407
+ this.#tops.clear();
17959
18408
  if (!this.isConnected) return;
17960
18409
  const columns = [];
17961
18410
  for (const el of this._getVisibleItems()) {
17962
18411
  const rect = el.getBoundingClientRect();
17963
18412
  if (rect.width === 0 && rect.height === 0) continue;
18413
+ this.#tops.set(el, rect.top);
17964
18414
  let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
17965
18415
  if (ci === -1) {
17966
18416
  columns.push({
@@ -18081,6 +18531,30 @@ let DetailListElement = class DetailListElement extends LitElement {
18081
18531
  const next = this.#findNextCheckable(cur.col, cur.row, 0, -1);
18082
18532
  if (next) this.#navigateTo(next);
18083
18533
  }
18534
+ /**
18535
+ * Left/Right target. Ungrouped lists keep the row-index rule. Grouped lists
18536
+ * have headers at arbitrary heights, so an item's row index no longer equals
18537
+ * its visual row — pick the item in the neighbouring column whose top is
18538
+ * nearest the cursor's (ties go to the upper one).
18539
+ */
18540
+ #horizontalTarget(from, dir) {
18541
+ if (this.#groupList.length === 0) return this.#findNextCheckable(from.col, from.row, dir, 0);
18542
+ const column = this.#grid[from.col + dir];
18543
+ const current = this.tickled.tickledId;
18544
+ const top = current ? this.#tops.get(current) : void 0;
18545
+ if (!column || top === void 0) return void 0;
18546
+ let best;
18547
+ let bestDelta = Number.POSITIVE_INFINITY;
18548
+ for (const el of column) {
18549
+ if (!this._isCheckable(el)) continue;
18550
+ const delta = Math.abs((this.#tops.get(el) ?? Number.POSITIVE_INFINITY) - top);
18551
+ if (delta < bestDelta) {
18552
+ best = el;
18553
+ bestDelta = delta;
18554
+ }
18555
+ }
18556
+ return best;
18557
+ }
18084
18558
  #navigateRight() {
18085
18559
  this.#recomputeGeometry();
18086
18560
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
@@ -18088,7 +18562,7 @@ let DetailListElement = class DetailListElement extends LitElement {
18088
18562
  this.#navigateTo(this.#findFirstCheckable());
18089
18563
  return;
18090
18564
  }
18091
- const next = this.#findNextCheckable(cur.col, cur.row, 1, 0);
18565
+ const next = this.#horizontalTarget(cur, 1);
18092
18566
  if (next) this.#navigateTo(next);
18093
18567
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
18094
18568
  }
@@ -18099,13 +18573,14 @@ let DetailListElement = class DetailListElement extends LitElement {
18099
18573
  this.#navigateTo(this.#findFirstCheckable());
18100
18574
  return;
18101
18575
  }
18102
- const next = this.#findNextCheckable(cur.col, cur.row, -1, 0);
18576
+ const next = this.#horizontalTarget(cur, -1);
18103
18577
  if (next) this.#navigateTo(next);
18104
18578
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
18105
18579
  }
18106
18580
  _getVisibleItems() {
18107
18581
  return Array.from(this.querySelectorAll(ITEM_SELECTOR$3)).filter((el) => {
18108
18582
  if (el.hidden) return false;
18583
+ if (el.hasAttribute(GROUP_COLLAPSED_ATTR)) return false;
18109
18584
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
18110
18585
  return true;
18111
18586
  });
@@ -18394,6 +18869,20 @@ let DetailListElement = class DetailListElement extends LitElement {
18394
18869
  display: none !important;
18395
18870
  }
18396
18871
 
18872
+ /* Rows of a collapsed group leave the flow; their header stays. */
18873
+ ::slotted([data-group-collapsed]) {
18874
+ display: none !important;
18875
+ }
18876
+
18877
+ /* Group headers are rows in the column flow, the same size as an item. */
18878
+ ::slotted(ixfx-list-group-header) {
18879
+ box-sizing: border-box;
18880
+ flex: none;
18881
+ width: var(--detail-list-column-width, 220px);
18882
+ height: var(--detail-list-item-height, 28px);
18883
+ --list-group-header-height: var(--detail-list-group-header-height, var(--detail-list-item-height, 28px));
18884
+ }
18885
+
18397
18886
  ::slotted(li) {
18398
18887
  box-sizing: border-box;
18399
18888
  display: flex;
@@ -18570,6 +19059,14 @@ __decorate([property({
18570
19059
  })], DetailListElement.prototype, "itemsDraggable", void 0);
18571
19060
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewRenderer", void 0);
18572
19061
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewSource", void 0);
19062
+ __decorate([property({ attribute: `group-by` })], DetailListElement.prototype, "groupBy", void 0);
19063
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "groupLabel", void 0);
19064
+ __decorate([property({
19065
+ type: Boolean,
19066
+ attribute: `cross-group-selection`,
19067
+ reflect: true
19068
+ })], DetailListElement.prototype, "crossGroupSelection", void 0);
19069
+ __decorate([state()], DetailListElement.prototype, "_collapsedGroups", void 0);
18573
19070
  __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
18574
19071
  DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
18575
19072
  //#endregion
@@ -24209,7 +24706,374 @@ let FormElement = class FormElement extends LitElement {
24209
24706
  };
24210
24707
  FormElement = __decorate([safeCustomElement(`ixfx-form`)], FormElement);
24211
24708
  //#endregion
24709
+ //#region src/grid-list/grid-list-layout.ts
24710
+ /**
24711
+ * `itemWidth` is the square's width; `rowHeight` is the full cell height
24712
+ * (square, plus the caption band in `separate` / `under` styles).
24713
+ * `scrollOffset` is `scrollTop` for `grid`/`column` and `scrollLeft` for `row`.
24714
+ */
24715
+ function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
24716
+ const w = Math.max(1, itemWidth);
24717
+ const h = Math.max(1, rowHeight);
24718
+ const g = Math.max(0, gap);
24719
+ const colStride = w + g;
24720
+ const rowStride = h + g;
24721
+ const offset = Math.max(0, scrollOffset);
24722
+ if (orientation === `row`) {
24723
+ const rows = count === 0 ? 0 : 1;
24724
+ const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
24725
+ if (count === 0 || viewportWidth <= 0) return {
24726
+ cols: count,
24727
+ rows,
24728
+ totalHeight: 0,
24729
+ totalWidth,
24730
+ startIndex: 0,
24731
+ endIndex: 0,
24732
+ colStride,
24733
+ rowStride,
24734
+ horizontal: true
24735
+ };
24736
+ const firstVisible = Math.max(0, Math.floor(offset / colStride));
24737
+ const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
24738
+ return {
24739
+ cols: count,
24740
+ rows,
24741
+ totalHeight: 0,
24742
+ totalWidth,
24743
+ startIndex: Math.max(0, firstVisible - overscan),
24744
+ endIndex: Math.min(count, lastVisible + overscan),
24745
+ colStride,
24746
+ rowStride,
24747
+ horizontal: true
24748
+ };
24749
+ }
24750
+ const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
24751
+ const rows = count === 0 ? 0 : Math.ceil(count / cols);
24752
+ const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
24753
+ if (count === 0 || viewportHeight <= 0) return {
24754
+ cols,
24755
+ rows,
24756
+ totalHeight,
24757
+ totalWidth: 0,
24758
+ startIndex: 0,
24759
+ endIndex: 0,
24760
+ colStride,
24761
+ rowStride,
24762
+ horizontal: false
24763
+ };
24764
+ const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
24765
+ const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
24766
+ const rowStart = Math.max(0, firstVisibleRow - overscan);
24767
+ const rowEnd = Math.min(rows, lastVisibleRow + overscan);
24768
+ return {
24769
+ cols,
24770
+ rows,
24771
+ totalHeight,
24772
+ totalWidth: 0,
24773
+ startIndex: rowStart * cols,
24774
+ endIndex: Math.min(count, rowEnd * cols),
24775
+ colStride,
24776
+ rowStride,
24777
+ horizontal: false
24778
+ };
24779
+ }
24780
+ function buildSections(p, cols, stride, itemExtent) {
24781
+ const g = Math.max(0, p.gap);
24782
+ const horizontal = p.orientation === `row`;
24783
+ const lineCount = (n) => horizontal ? n : Math.ceil(n / cols);
24784
+ const extentFor = (n) => n === 0 ? 0 : (lineCount(n) - 1) * stride + itemExtent;
24785
+ if (!p.groups) {
24786
+ const end = extentFor(p.count);
24787
+ return {
24788
+ sections: [{
24789
+ group: void 0,
24790
+ first: 0,
24791
+ count: p.count,
24792
+ headerOffset: 0,
24793
+ itemsOffset: 0,
24794
+ end
24795
+ }],
24796
+ total: end,
24797
+ count: p.count
24798
+ };
24799
+ }
24800
+ const headerSize = horizontal ? p.headerWidth : p.headerHeight;
24801
+ const sections = [];
24802
+ let cursor = 0;
24803
+ let first = 0;
24804
+ for (const group of p.groups) {
24805
+ const count = group.collapsed ? 0 : group.count;
24806
+ const itemsOffset = cursor + headerSize + g;
24807
+ const end = count === 0 ? cursor + headerSize : itemsOffset + extentFor(count);
24808
+ sections.push({
24809
+ group,
24810
+ first,
24811
+ count,
24812
+ headerOffset: cursor,
24813
+ itemsOffset,
24814
+ end
24815
+ });
24816
+ first += count;
24817
+ cursor = end + g;
24818
+ }
24819
+ return {
24820
+ sections,
24821
+ total: sections.length === 0 ? 0 : sections[sections.length - 1].end,
24822
+ count: first
24823
+ };
24824
+ }
24825
+ /** Index of the last section whose `headerOffset` is <= `offset` (0 when before all). */
24826
+ function sectionAtOffset(sections, offset) {
24827
+ let lo = 0;
24828
+ let hi = sections.length - 1;
24829
+ while (lo < hi) {
24830
+ const mid = lo + hi + 1 >> 1;
24831
+ if (sections[mid].headerOffset <= offset) lo = mid;
24832
+ else hi = mid - 1;
24833
+ }
24834
+ return lo;
24835
+ }
24836
+ /** Index of the section holding effective item `index` (skips empty sections). */
24837
+ function sectionOfIndex(sections, index) {
24838
+ let lo = 0;
24839
+ let hi = sections.length - 1;
24840
+ while (lo < hi) {
24841
+ const mid = lo + hi + 1 >> 1;
24842
+ if (sections[mid].first <= index) lo = mid;
24843
+ else hi = mid - 1;
24844
+ }
24845
+ let s = lo;
24846
+ while (s < sections.length - 1 && sections[s].count === 0) s++;
24847
+ return s;
24848
+ }
24849
+ function buildLayout(p) {
24850
+ const horizontal = p.orientation === `row`;
24851
+ const w = Math.max(1, p.itemWidth);
24852
+ const h = Math.max(1, p.rowHeight);
24853
+ const g = Math.max(0, p.gap);
24854
+ const colStride = w + g;
24855
+ const rowStride = h + g;
24856
+ const cols = horizontal ? 1 : p.orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, p.viewportWidth) + g) / colStride));
24857
+ const stride = horizontal ? colStride : rowStride;
24858
+ const itemExtent = horizontal ? w : h;
24859
+ const { sections, total, count } = buildSections(p, cols, stride, itemExtent);
24860
+ const grouped = p.groups !== void 0;
24861
+ const headerSize = horizontal ? p.headerWidth : p.headerHeight;
24862
+ const viewportMain = horizontal ? p.viewportWidth : p.viewportHeight;
24863
+ const cellHeight = rowStride - g;
24864
+ const rowY = Math.max(0, (p.viewportHeight - cellHeight) / 2);
24865
+ const positionOf = (index) => {
24866
+ const si = sectionOfIndex(sections, index);
24867
+ const s = sections[si];
24868
+ const local = Math.max(0, index - s.first);
24869
+ if (horizontal) return {
24870
+ x: s.itemsOffset + local * colStride,
24871
+ y: rowY,
24872
+ col: 0,
24873
+ line: local,
24874
+ section: si
24875
+ };
24876
+ const col = cols <= 1 ? 0 : local % cols;
24877
+ const line = cols <= 1 ? local : Math.floor(local / cols);
24878
+ return {
24879
+ x: col * colStride,
24880
+ y: s.itemsOffset + line * rowStride,
24881
+ col,
24882
+ line,
24883
+ section: si
24884
+ };
24885
+ };
24886
+ const window = (offset, overscan = 2) => {
24887
+ const off = Math.max(0, offset);
24888
+ const headers = [];
24889
+ const ranges = [];
24890
+ const lo = off - overscan * stride;
24891
+ const hi = off + viewportMain + overscan * stride;
24892
+ let firstSection = 0;
24893
+ if (sections.length > 1) {
24894
+ let a = 0;
24895
+ let b = sections.length - 1;
24896
+ while (a < b) {
24897
+ const mid = a + b >> 1;
24898
+ if (sections[mid].end >= lo) b = mid;
24899
+ else a = mid + 1;
24900
+ }
24901
+ firstSection = a;
24902
+ }
24903
+ if (viewportMain > 0) for (let si = firstSection; si < sections.length; si++) {
24904
+ const s = sections[si];
24905
+ if (s.headerOffset > hi) break;
24906
+ if (s.group && s.headerOffset + headerSize >= lo) headers.push({
24907
+ key: s.group.key,
24908
+ label: s.group.label,
24909
+ count: s.group.count,
24910
+ collapsed: s.group.collapsed,
24911
+ offset: s.headerOffset,
24912
+ size: headerSize
24913
+ });
24914
+ if (s.count === 0) continue;
24915
+ const rel = off - s.itemsOffset;
24916
+ const viewport = rel < 0 ? viewportMain + rel : viewportMain;
24917
+ if (viewport <= 0) continue;
24918
+ const sw = computeGridWindow(s.count, w, h, g, horizontal ? viewport : p.viewportWidth, horizontal ? p.viewportHeight : viewport, Math.max(0, rel), p.orientation, overscan);
24919
+ if (sw.endIndex > sw.startIndex) ranges.push([s.first + sw.startIndex, s.first + sw.endIndex]);
24920
+ }
24921
+ return {
24922
+ cols: horizontal ? count : cols,
24923
+ rows: 0,
24924
+ totalHeight: horizontal ? 0 : total,
24925
+ totalWidth: horizontal ? total : 0,
24926
+ startIndex: ranges.length > 0 ? ranges[0][0] : 0,
24927
+ endIndex: ranges.length > 0 ? ranges[ranges.length - 1][1] : 0,
24928
+ colStride,
24929
+ rowStride,
24930
+ horizontal,
24931
+ headers,
24932
+ ranges
24933
+ };
24934
+ };
24935
+ const extentOf = (index) => {
24936
+ const { line, section } = positionOf(index);
24937
+ const s = sections[section];
24938
+ const start = s.itemsOffset + line * stride;
24939
+ return [line === 0 && s.group ? s.headerOffset : start, start + itemExtent];
24940
+ };
24941
+ const neighbour = (index, dLine) => {
24942
+ if (count === 0) return index;
24943
+ const si = sectionOfIndex(sections, index);
24944
+ const s = sections[si];
24945
+ const local = index - s.first;
24946
+ const col = local % cols;
24947
+ if (dLine > 0) {
24948
+ const t = local + cols;
24949
+ if (t <= s.count - 1) return s.first + t;
24950
+ const lastLine = Math.floor((s.count - 1) / cols);
24951
+ if (Math.floor(local / cols) < lastLine) return s.first + s.count - 1;
24952
+ for (let n = si + 1; n < sections.length; n++) if (sections[n].count > 0) return sections[n].first + Math.min(col, sections[n].count - 1);
24953
+ return s.first + s.count - 1;
24954
+ }
24955
+ const t = local - cols;
24956
+ if (t >= 0) return s.first + t;
24957
+ for (let n = si - 1; n >= 0; n--) {
24958
+ const prev = sections[n];
24959
+ if (prev.count > 0) {
24960
+ const line = Math.floor((prev.count - 1) / cols);
24961
+ return prev.first + Math.min(line * cols + col, prev.count - 1);
24962
+ }
24963
+ }
24964
+ return s.first;
24965
+ };
24966
+ const hitTest = (x0, x1, y0, y1) => {
24967
+ const out = [];
24968
+ if (count === 0) return out;
24969
+ const main0 = horizontal ? x0 : y0;
24970
+ const main1 = horizontal ? x1 : y1;
24971
+ const start = sectionAtOffset(sections, main0 - stride);
24972
+ for (let si = Math.max(0, start); si < sections.length; si++) {
24973
+ const s = sections[si];
24974
+ if (s.itemsOffset > main1) break;
24975
+ if (s.count === 0 || s.end < main0) continue;
24976
+ if (horizontal) {
24977
+ const a = Math.max(0, Math.floor((x0 - s.itemsOffset) / colStride));
24978
+ const b = Math.min(s.count - 1, Math.floor((x1 - s.itemsOffset) / colStride));
24979
+ for (let i = a; i <= b; i++) out.push(s.first + i);
24980
+ } else {
24981
+ const colLo = Math.max(0, Math.floor(x0 / colStride));
24982
+ const colHi = Math.min(cols - 1, Math.floor(x1 / colStride));
24983
+ const rowLo = Math.max(0, Math.floor((y0 - s.itemsOffset) / rowStride));
24984
+ const rowHi = Math.floor((y1 - s.itemsOffset) / rowStride);
24985
+ for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
24986
+ const local = row * cols + col;
24987
+ if (local >= 0 && local < s.count) out.push(s.first + local);
24988
+ }
24989
+ }
24990
+ }
24991
+ return out;
24992
+ };
24993
+ const anchorAt = (mainOffset) => {
24994
+ if (count === 0) return {
24995
+ index: 0,
24996
+ lineFrac: 0
24997
+ };
24998
+ let si = sectionAtOffset(sections, mainOffset);
24999
+ while (si < sections.length - 1 && sections[si].count === 0) si++;
25000
+ const s = sections[si];
25001
+ if (s.count === 0) return {
25002
+ index: 0,
25003
+ lineFrac: 0
25004
+ };
25005
+ const centreLine = Math.max(0, mainOffset - s.itemsOffset) / stride;
25006
+ const lineFloor = Math.max(0, Math.floor(centreLine));
25007
+ const local = horizontal ? Math.min(s.count - 1, lineFloor) : Math.min(s.count - 1, lineFloor * cols + Math.max(0, Math.floor((cols - 1) / 2)));
25008
+ return {
25009
+ index: s.first + local,
25010
+ lineFrac: centreLine - lineFloor
25011
+ };
25012
+ };
25013
+ const offsetOfAnchor = (index, lineFrac) => {
25014
+ const { line, section } = positionOf(index);
25015
+ return sections[section].itemsOffset + (line + lineFrac) * stride;
25016
+ };
25017
+ const snapTarget = (offset) => {
25018
+ const si = sectionAtOffset(sections, offset);
25019
+ const s = sections[si];
25020
+ let rel = Math.round((offset - s.itemsOffset) / stride);
25021
+ if (grouped) {
25022
+ const lines = s.count === 0 ? 0 : horizontal ? s.count : Math.ceil(s.count / cols);
25023
+ rel = Math.max(0, Math.min(Math.max(0, lines - 1), rel));
25024
+ }
25025
+ const candidates = [s.itemsOffset + rel * stride];
25026
+ if (grouped) {
25027
+ candidates.push(s.headerOffset);
25028
+ if (si + 1 < sections.length) candidates.push(sections[si + 1].headerOffset);
25029
+ }
25030
+ return candidates.reduce((best, c) => Math.abs(c - offset) < Math.abs(best - offset) ? c : best);
25031
+ };
25032
+ return {
25033
+ horizontal,
25034
+ cols,
25035
+ colStride,
25036
+ rowStride,
25037
+ grouped,
25038
+ count,
25039
+ totalExtent: total,
25040
+ window,
25041
+ positionOf,
25042
+ extentOf,
25043
+ neighbour,
25044
+ hitTest,
25045
+ anchorAt,
25046
+ offsetOfAnchor,
25047
+ snapTarget
25048
+ };
25049
+ }
25050
+ //#endregion
24212
25051
  //#region src/grid-list/grid-list-source.ts
25052
+ /** Reorder `effective` raw indices into contiguous groups, dropping collapsed ones. */
25053
+ function applyGrouping(effective, cfg, keyOfRaw) {
25054
+ const parts = partitionByGroup(effective, (raw) => cfg.groupKeyOf(raw), cfg.labelOf ? (key, raws) => cfg.labelOf(key, raws.map((r) => cfg.itemAt(r))) : void 0);
25055
+ const order = [];
25056
+ const groups = [];
25057
+ const groupByKey = /* @__PURE__ */ new Map();
25058
+ for (const part of parts) {
25059
+ const collapsed = cfg.collapsed.has(part.key);
25060
+ groups.push({
25061
+ key: part.key,
25062
+ label: part.label,
25063
+ count: part.items.length,
25064
+ collapsed
25065
+ });
25066
+ for (const raw of part.items) {
25067
+ groupByKey.set(keyOfRaw(raw), part.key);
25068
+ if (!collapsed) order.push(raw);
25069
+ }
25070
+ }
25071
+ return {
25072
+ order,
25073
+ groups,
25074
+ groupByKey
25075
+ };
25076
+ }
24213
25077
  const ITEM_SELECTOR$2 = `:scope > *`;
24214
25078
  const SYNTHETIC_PREFIX = `gl-auto-`;
24215
25079
  /** Whether `key` is an auto-assigned key for a keyless light-DOM item. */
@@ -24236,6 +25100,9 @@ var ElementGridListSource = class {
24236
25100
  #searchKeys;
24237
25101
  #effectiveIndices = [];
24238
25102
  #keysCache;
25103
+ #grouping;
25104
+ #groups = [];
25105
+ #groupByKey = /* @__PURE__ */ new Map();
24239
25106
  constructor(host) {
24240
25107
  this.#host = host;
24241
25108
  this.refresh();
@@ -24288,6 +25155,16 @@ var ElementGridListSource = class {
24288
25155
  if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
24289
25156
  this.#recomputeEffective();
24290
25157
  }
25158
+ setGrouping(config) {
25159
+ this.#grouping = config;
25160
+ this.#recomputeEffective();
25161
+ }
25162
+ groups() {
25163
+ return this.#groups;
25164
+ }
25165
+ groupKeyOfKey(key) {
25166
+ return this.#groupByKey.get(key) ?? null;
25167
+ }
24291
25168
  refresh() {
24292
25169
  this.#items = Array.from(this.#host.querySelectorAll(ITEM_SELECTOR$2)).filter((el) => !el.hidden);
24293
25170
  for (const el of this.#items) {
@@ -24330,9 +25207,21 @@ var ElementGridListSource = class {
24330
25207
  }
24331
25208
  effective.push(i);
24332
25209
  }
24333
- this.#effectiveIndices = effective;
25210
+ this.#applyGroupingTo(effective);
24334
25211
  this.#keysCache = void 0;
24335
25212
  }
25213
+ #applyGroupingTo(effective) {
25214
+ if (this.#grouping === void 0) {
25215
+ this.#effectiveIndices = effective;
25216
+ this.#groups = [];
25217
+ this.#groupByKey = /* @__PURE__ */ new Map();
25218
+ return;
25219
+ }
25220
+ const grouped = applyGrouping(effective, this.#grouping, (raw) => this.#itemKey(this.#items[raw]));
25221
+ this.#effectiveIndices = grouped.order;
25222
+ this.#groups = grouped.groups;
25223
+ this.#groupByKey = grouped.groupByKey;
25224
+ }
24336
25225
  #itemKey(el) {
24337
25226
  const userKey = keyForElement(el);
24338
25227
  if (userKey !== void 0) return userKey;
@@ -24355,6 +25244,9 @@ var DataGridListSource = class {
24355
25244
  #searchKeys;
24356
25245
  #effectiveIndices = [];
24357
25246
  #keysCache;
25247
+ #grouping;
25248
+ #groups = [];
25249
+ #groupByKey = /* @__PURE__ */ new Map();
24358
25250
  constructor(items, adapter) {
24359
25251
  this.#items = items;
24360
25252
  this.#adapter = adapter;
@@ -24420,6 +25312,16 @@ var DataGridListSource = class {
24420
25312
  if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
24421
25313
  this.#recomputeEffective();
24422
25314
  }
25315
+ setGrouping(config) {
25316
+ this.#grouping = config;
25317
+ this.#recomputeEffective();
25318
+ }
25319
+ groups() {
25320
+ return this.#groups;
25321
+ }
25322
+ groupKeyOfKey(key) {
25323
+ return this.#groupByKey.get(key) ?? null;
25324
+ }
24423
25325
  refresh() {
24424
25326
  this.#recomputeEffective();
24425
25327
  }
@@ -24444,7 +25346,16 @@ var DataGridListSource = class {
24444
25346
  if (this.#searchKeys !== void 0 && !this.#searchKeys.has(this.#adapter.key(this.#items[i], i))) continue;
24445
25347
  effective.push(i);
24446
25348
  }
24447
- this.#effectiveIndices = effective;
25349
+ if (this.#grouping === void 0) {
25350
+ this.#effectiveIndices = effective;
25351
+ this.#groups = [];
25352
+ this.#groupByKey = /* @__PURE__ */ new Map();
25353
+ } else {
25354
+ const grouped = applyGrouping(effective, this.#grouping, (raw) => this.#adapter.key(this.#items[raw], raw));
25355
+ this.#effectiveIndices = grouped.order;
25356
+ this.#groups = grouped.groups;
25357
+ this.#groupByKey = grouped.groupByKey;
25358
+ }
24448
25359
  this.#keysCache = void 0;
24449
25360
  }
24450
25361
  };
@@ -24460,76 +25371,11 @@ const SNAP_DEBOUNCE_MS = 120;
24460
25371
  * to finish. Kept a touch above the default `--grid-list-item-move-duration`.
24461
25372
  */
24462
25373
  const REFLOW_WINDOW_MS = 320;
24463
- function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
24464
- const w = Math.max(1, itemWidth);
24465
- const h = Math.max(1, rowHeight);
24466
- const g = Math.max(0, gap);
24467
- const colStride = w + g;
24468
- const rowStride = h + g;
24469
- const offset = Math.max(0, scrollOffset);
24470
- if (orientation === `row`) {
24471
- const rows = count === 0 ? 0 : 1;
24472
- const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
24473
- if (count === 0 || viewportWidth <= 0) return {
24474
- cols: count,
24475
- rows,
24476
- totalHeight: 0,
24477
- totalWidth,
24478
- startIndex: 0,
24479
- endIndex: 0,
24480
- colStride,
24481
- rowStride,
24482
- horizontal: true
24483
- };
24484
- const firstVisible = Math.max(0, Math.floor(offset / colStride));
24485
- const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
24486
- return {
24487
- cols: count,
24488
- rows,
24489
- totalHeight: 0,
24490
- totalWidth,
24491
- startIndex: Math.max(0, firstVisible - overscan),
24492
- endIndex: Math.min(count, lastVisible + overscan),
24493
- colStride,
24494
- rowStride,
24495
- horizontal: true
24496
- };
24497
- }
24498
- const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
24499
- const rows = count === 0 ? 0 : Math.ceil(count / cols);
24500
- const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
24501
- if (count === 0 || viewportHeight <= 0) return {
24502
- cols,
24503
- rows,
24504
- totalHeight,
24505
- totalWidth: 0,
24506
- startIndex: 0,
24507
- endIndex: 0,
24508
- colStride,
24509
- rowStride,
24510
- horizontal: false
24511
- };
24512
- const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
24513
- const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
24514
- const rowStart = Math.max(0, firstVisibleRow - overscan);
24515
- const rowEnd = Math.min(rows, lastVisibleRow + overscan);
24516
- return {
24517
- cols,
24518
- rows,
24519
- totalHeight,
24520
- totalWidth: 0,
24521
- startIndex: rowStart * cols,
24522
- endIndex: Math.min(count, rowEnd * cols),
24523
- colStride,
24524
- rowStride,
24525
- horizontal: false
24526
- };
24527
- }
24528
- function columnOf(index, cols) {
24529
- return cols <= 1 ? 0 : index % cols;
25374
+ function readProperty(item, name) {
25375
+ return typeof item === `object` && item !== null ? item[name] : void 0;
24530
25376
  }
24531
- function rowOf(index, cols) {
24532
- return cols <= 1 ? index : Math.floor(index / cols);
25377
+ function readAttribute(el, name) {
25378
+ return el.getAttribute(`data-${name}`);
24533
25379
  }
24534
25380
  /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
24535
25381
  function sameKeys$1(a, b) {
@@ -24557,6 +25403,11 @@ let GridListElement = class GridListElement extends LitElement {
24557
25403
  this.marqueeFromCellBlank = false;
24558
25404
  this.snapToItem = false;
24559
25405
  this.items = [];
25406
+ this.groupBy = null;
25407
+ this.crossGroupSelection = false;
25408
+ this.groupHeaderHeight = 28;
25409
+ this.groupHeaderWidth = 120;
25410
+ this._collapsedGroups = /* @__PURE__ */ new Set();
24560
25411
  this._scrollTop = 0;
24561
25412
  this._scrollLeft = 0;
24562
25413
  this._viewportHeight = 0;
@@ -24581,6 +25432,12 @@ let GridListElement = class GridListElement extends LitElement {
24581
25432
  interactionMode: this.interactionMode
24582
25433
  }),
24583
25434
  getKey: (k) => k,
25435
+ getGroupKey: (k) => this.groupBy === null ? null : this.#source.groupKeyOfKey(k),
25436
+ crossGroupSelection: () => this.crossGroupSelection,
25437
+ getFallbackGroupKey: () => {
25438
+ const k = this.tickled.tickledId;
25439
+ return k !== void 0 && this.groupBy !== null ? this.#source.groupKeyOfKey(k) : null;
25440
+ },
24584
25441
  onChange: (selected, previous) => {
24585
25442
  this._syncItemStates();
24586
25443
  this.requestUpdate();
@@ -24678,10 +25535,11 @@ let GridListElement = class GridListElement extends LitElement {
24678
25535
  },
24679
25536
  shouldStart: (event) => {
24680
25537
  if (this.marqueeFromCellBlank && this.#isCellBlankPress(event)) return true;
24681
- for (const node of event.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return false;
25538
+ for (const node of event.composedPath()) if (node instanceof Element && (node.classList.contains(`cell`) || node.classList.contains(`group-header`))) return false;
24682
25539
  return true;
24683
25540
  }
24684
25541
  });
25542
+ this.#groupByChanged = false;
24685
25543
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
24686
25544
  this.#onScroll = (e) => {
24687
25545
  const el = e.target;
@@ -24731,15 +25589,25 @@ let GridListElement = class GridListElement extends LitElement {
24731
25589
  const count = this.#source.count;
24732
25590
  if (count === 0) return;
24733
25591
  const win = this.#currentWindow();
24734
- const move = (delta) => {
24735
- const from = this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
24736
- const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
25592
+ const layout = this.#layout();
25593
+ const current = () => this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
25594
+ const goto = (nextIndex) => {
24737
25595
  const nextKey = this.#source.keyAt(nextIndex);
24738
25596
  if (nextKey) {
24739
25597
  this.tickled.handleKeyboardNavigation(nextKey);
24740
25598
  this.#scrollItemIntoView(nextKey);
24741
25599
  }
24742
25600
  };
25601
+ const move = (delta) => goto(Math.max(0, Math.min(count - 1, current() + delta)));
25602
+ const moveLines = (dir, lines = 1) => {
25603
+ let index = current();
25604
+ if (index < 0) {
25605
+ move(dir * lines * win.cols);
25606
+ return;
25607
+ }
25608
+ for (let i = 0; i < lines; i++) index = layout.neighbour(index, dir);
25609
+ goto(index);
25610
+ };
24743
25611
  const perLine = this.orientation === `grid` ? win.cols : 1;
24744
25612
  switch (e.key) {
24745
25613
  case `ArrowLeft`:
@@ -24749,10 +25617,12 @@ let GridListElement = class GridListElement extends LitElement {
24749
25617
  move(1);
24750
25618
  break;
24751
25619
  case `ArrowUp`:
24752
- move(-perLine);
25620
+ if (this.orientation === `grid`) moveLines(-1);
25621
+ else move(-perLine);
24753
25622
  break;
24754
25623
  case `ArrowDown`:
24755
- move(perLine);
25624
+ if (this.orientation === `grid`) moveLines(1);
25625
+ else move(perLine);
24756
25626
  break;
24757
25627
  case `Home`:
24758
25628
  this.tickled.handleKeyboardNavigation(`first`);
@@ -24767,7 +25637,8 @@ let GridListElement = class GridListElement extends LitElement {
24767
25637
  const mainExtent = win.horizontal ? this._viewportWidth : this._viewportHeight;
24768
25638
  const mainStride = win.horizontal ? win.colStride : win.rowStride;
24769
25639
  const visibleLines = Math.max(1, Math.floor(mainExtent / mainStride));
24770
- move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
25640
+ if (this.orientation === `grid`) moveLines(e.key === `PageDown` ? 1 : -1, visibleLines);
25641
+ else move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
24771
25642
  break;
24772
25643
  }
24773
25644
  case `Enter`: {
@@ -24848,6 +25719,11 @@ let GridListElement = class GridListElement extends LitElement {
24848
25719
  position: relative;
24849
25720
  width: 100%;
24850
25721
  }
25722
+ .group-header {
25723
+ position: absolute;
25724
+ inset: 0 auto auto 0;
25725
+ z-index: 1;
25726
+ }
24851
25727
  .cell {
24852
25728
  position: absolute;
24853
25729
  inset: 0 auto auto 0;
@@ -25141,6 +26017,9 @@ let GridListElement = class GridListElement extends LitElement {
25141
26017
  specificity. Survivors slide to their new slot; a freshly inserted cell
25142
26018
  also fades in. (A removed item's element is already gone, so there's
25143
26019
  nothing left to fade — the gap simply closes as the others slide.) */
26020
+ :host([reflowing]) .group-header {
26021
+ transition: transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1);
26022
+ }
25144
26023
  :host([reflowing]) .cell {
25145
26024
  transition:
25146
26025
  transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1),
@@ -25151,6 +26030,7 @@ let GridListElement = class GridListElement extends LitElement {
25151
26030
  animation: ixfx-list-item-in var(--grid-list-item-move-duration, 200ms) ease both;
25152
26031
  }
25153
26032
  @media (prefers-reduced-motion: reduce) {
26033
+ :host([reflowing]) .group-header { transition: none; }
25154
26034
  :host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
25155
26035
  .cell.entering { animation: none; }
25156
26036
  }
@@ -25292,31 +26172,13 @@ let GridListElement = class GridListElement extends LitElement {
25292
26172
  * so virtualized off-screen items are covered without DOM measurement.
25293
26173
  */
25294
26174
  #marqueeHitTest(rect) {
25295
- const count = this.#source.count;
25296
- if (count === 0) return [];
26175
+ if (this.#source.count === 0) return [];
25297
26176
  const horizontal = this.orientation === `row`;
25298
- const win = computeGridWindow(count, this.itemSize, this.#rowHeight(), this.gap, Math.max(1, this._viewportWidth), Math.max(1, this._viewportHeight), horizontal ? this._scrollLeft : this._scrollTop, this.orientation, 0);
25299
- const out = [];
25300
- if (horizontal) {
25301
- const x0 = rect.x + this._scrollLeft;
25302
- const x1 = x0 + rect.width;
25303
- const lo = Math.max(0, Math.floor(x0 / win.colStride));
25304
- const hi = Math.min(count - 1, Math.floor(x1 / win.colStride));
25305
- for (let i = lo; i <= hi; i++) out.push(i);
25306
- } else {
25307
- const x0 = rect.x + this._scrollLeft;
25308
- const x1 = x0 + rect.width;
25309
- const y0 = rect.y + this._scrollTop;
25310
- const y1 = y0 + rect.height;
25311
- const colLo = Math.max(0, Math.floor(x0 / win.colStride));
25312
- const colHi = Math.min(win.cols - 1, Math.floor(x1 / win.colStride));
25313
- const rowLo = Math.max(0, Math.floor(y0 / win.rowStride));
25314
- const rowHi = Math.floor(y1 / win.rowStride);
25315
- for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
25316
- const index = row * win.cols + col;
25317
- if (index >= 0 && index < count) out.push(index);
25318
- }
25319
- }
26177
+ const x0 = rect.x + this._scrollLeft;
26178
+ const x1 = x0 + rect.width;
26179
+ const y0 = horizontal ? 0 : rect.y + this._scrollTop;
26180
+ const y1 = horizontal ? 0 : y0 + rect.height;
26181
+ const out = this.#layout().hitTest(x0, x1, y0, y1);
25320
26182
  const keys = [];
25321
26183
  for (const index of out) {
25322
26184
  const key = this.#source.keyAt(index);
@@ -25466,6 +26328,10 @@ let GridListElement = class GridListElement extends LitElement {
25466
26328
  if (previousLength === 0 && removedCount === 0) return;
25467
26329
  if (added.length > 0 && added.length === this.#items.length) return;
25468
26330
  for (const el of added) this.#entering.add(el);
26331
+ this.#openReflowWindow();
26332
+ }
26333
+ /** Open (or extend) the `[reflowing]` window so moved cells and headers slide. */
26334
+ #openReflowWindow() {
25469
26335
  this.toggleAttribute(`reflowing`, true);
25470
26336
  if (this.#reflowTimer) clearTimeout(this.#reflowTimer);
25471
26337
  this.#reflowTimer = window.setTimeout(() => {
@@ -25522,6 +26388,7 @@ let GridListElement = class GridListElement extends LitElement {
25522
26388
  this.#sourceElement = new ElementGridListSource(this);
25523
26389
  this.#source = this.#sourceElement;
25524
26390
  }
26391
+ this.#source.setGrouping(this.#groupingConfig());
25525
26392
  this.#applyFilters();
25526
26393
  if (this.#searchFilterKeys) this.#source.setFilters({ searchKeys: this.#searchFilterKeys });
25527
26394
  }
@@ -25563,6 +26430,17 @@ let GridListElement = class GridListElement extends LitElement {
25563
26430
  }
25564
26431
  this.search?.refresh();
25565
26432
  }
26433
+ const groupsChanged = changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`);
26434
+ if (groupsChanged && this.hasUpdated) {
26435
+ if (changed.has(`groupBy`)) {
26436
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
26437
+ this.#groupByChanged = true;
26438
+ }
26439
+ this.#applyGroupingToSource();
26440
+ this.#dropUnreachableCursor();
26441
+ this.#openReflowWindow();
26442
+ this.updateComplete.then(() => this.#emitViewportChange());
26443
+ } else if (groupsChanged) this.#applyGroupingToSource();
25566
26444
  if (changed.has(`itemSize`) || changed.has(`gap`)) {
25567
26445
  this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
25568
26446
  this.gap = Math.max(0, Math.round(this.gap));
@@ -25583,6 +26461,62 @@ let GridListElement = class GridListElement extends LitElement {
25583
26461
  }
25584
26462
  }
25585
26463
  }
26464
+ #groupByChanged;
26465
+ /** Grouping config for the active source, or `undefined` when ungrouped. */
26466
+ #groupingConfig() {
26467
+ const by = this.groupBy;
26468
+ if (by === null || by === void 0 || by === ``) return void 0;
26469
+ const collapsed = this._collapsedGroups;
26470
+ const src = this.#source;
26471
+ if (src instanceof DataGridListSource) return {
26472
+ groupKeyOf: (raw) => resolveGroupKey(by, src.rawItemAt(raw), raw, readProperty),
26473
+ itemAt: (raw) => src.rawItemAt(raw),
26474
+ labelOf: this.groupLabel,
26475
+ collapsed
26476
+ };
26477
+ const elements = src;
26478
+ return {
26479
+ groupKeyOf: (raw) => resolveGroupKey(by, elements.rawElementAt(raw), raw, readAttribute),
26480
+ itemAt: (raw) => elements.rawElementAt(raw),
26481
+ labelOf: this.groupLabel,
26482
+ collapsed
26483
+ };
26484
+ }
26485
+ /** Push the current grouping into the source and refresh the element-mode item cache. */
26486
+ #applyGroupingToSource() {
26487
+ this.#source.setGrouping(this.#groupingConfig());
26488
+ if (this.#source instanceof ElementGridListSource) this.#items = this.#source.effectiveElements();
26489
+ }
26490
+ /** The cursor can't stay on an item that a collapsed group just removed. */
26491
+ #dropUnreachableCursor() {
26492
+ const id = this.tickled.tickledId;
26493
+ if (id !== void 0 && this.#source.indexOfKey(id) < 0) this.tickled.focus(void 0);
26494
+ }
26495
+ /** Groups in display order (empty when ungrouped). */
26496
+ get groups() {
26497
+ return this.groupBy === null ? [] : this.#source.groups();
26498
+ }
26499
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
26500
+ setGroupCollapsed(groupKey, collapsed) {
26501
+ if (this._collapsedGroups.has(groupKey) === collapsed) return;
26502
+ const next = new Set(this._collapsedGroups);
26503
+ if (collapsed) next.add(groupKey);
26504
+ else next.delete(groupKey);
26505
+ this._collapsedGroups = next;
26506
+ this._dispatch(`list-group-toggle`, {
26507
+ groupKey,
26508
+ collapsed
26509
+ });
26510
+ }
26511
+ toggleGroup(groupKey) {
26512
+ this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
26513
+ }
26514
+ collapseAllGroups() {
26515
+ for (const g of this.groups) this.setGroupCollapsed(g.key, true);
26516
+ }
26517
+ expandAllGroups() {
26518
+ for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
26519
+ }
25586
26520
  #syncAriaMultiselectable() {
25587
26521
  this.setAttribute(`aria-multiselectable`, this.selection.canSelectMultiple() ? `true` : `false`);
25588
26522
  }
@@ -25622,6 +26556,10 @@ let GridListElement = class GridListElement extends LitElement {
25622
26556
  }
25623
26557
  this.#applyZoomAnchor();
25624
26558
  this.#activateTickledPan();
26559
+ if (this.#groupByChanged) {
26560
+ this.#groupByChanged = false;
26561
+ this._dispatch(`list-group-change`, { groupBy: this.groupBy });
26562
+ }
25625
26563
  }
25626
26564
  /** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
25627
26565
  #applyZoomAnchor() {
@@ -25630,41 +26568,33 @@ let GridListElement = class GridListElement extends LitElement {
25630
26568
  this.#zoomAnchor = void 0;
25631
26569
  const vp = this.#viewportRef.value;
25632
26570
  if (!vp) return;
25633
- const win = this.#currentWindow();
25634
- if (win.horizontal) {
25635
- if (this._viewportWidth <= 0) return;
25636
- const targetCenter = (index + lineFrac) * win.colStride;
25637
- const maxScroll = Math.max(0, win.totalWidth - this._viewportWidth);
25638
- const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportWidth / 2));
26571
+ const layout = this.#layout();
26572
+ const horizontal = layout.horizontal;
26573
+ const extent = horizontal ? this._viewportWidth : this._viewportHeight;
26574
+ if (extent <= 0) return;
26575
+ const targetCenter = layout.offsetOfAnchor(index, lineFrac);
26576
+ const maxScroll = Math.max(0, layout.totalExtent - extent);
26577
+ const next = Math.min(maxScroll, Math.max(0, targetCenter - extent / 2));
26578
+ if (horizontal) {
25639
26579
  if (Math.abs(vp.scrollLeft - next) > .5) {
25640
26580
  vp.scrollLeft = next;
25641
26581
  this._scrollLeft = next;
25642
26582
  }
25643
- return;
25644
- }
25645
- if (this._viewportHeight <= 0) return;
25646
- const targetCenter = ((win.cols > 0 ? Math.floor(index / win.cols) : 0) + lineFrac) * win.rowStride;
25647
- const maxScroll = Math.max(0, win.totalHeight - this._viewportHeight);
25648
- const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportHeight / 2));
25649
- if (Math.abs(vp.scrollTop - next) > .5) {
26583
+ } else if (Math.abs(vp.scrollTop - next) > .5) {
25650
26584
  vp.scrollTop = next;
25651
26585
  this._scrollTop = next;
25652
26586
  }
25653
26587
  }
25654
26588
  #captureCentreAnchor(prevOffset, prevExtent, prevItemSize, prevGap, prevCaptionStyle, prevCaptionHeight, prevOrientation) {
25655
- const count = this.#source?.count ?? 0;
25656
- if (count === 0) return;
26589
+ if ((this.#source?.count ?? 0) === 0) return;
25657
26590
  const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
25658
- const horizontal = prevOrientation === `row`;
25659
- const prev = computeGridWindow(count, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
25660
- const mainStride = horizontal ? prev.colStride : prev.rowStride;
25661
- const centerLine = (prevOffset + prevExtent / 2) / mainStride;
25662
- const lineFloor = Math.max(0, Math.floor(centerLine));
25663
- const index = horizontal ? Math.min(count - 1, lineFloor) : Math.min(count - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
25664
- this.#zoomAnchor = {
25665
- index,
25666
- lineFrac: centerLine - lineFloor
25667
- };
26591
+ const prev = this.#buildLayout({
26592
+ itemWidth: prevItemSize,
26593
+ rowHeight: prevRowHeight,
26594
+ gap: prevGap,
26595
+ orientation: prevOrientation
26596
+ });
26597
+ this.#zoomAnchor = prev.anchorAt(prevOffset + prevExtent / 2);
25668
26598
  }
25669
26599
  /**
25670
26600
  * Re-read the viewport after an ancestor's visibility/layout changed.
@@ -25878,37 +26808,75 @@ let GridListElement = class GridListElement extends LitElement {
25878
26808
  return this.#windowKeys(this.#currentWindow(true));
25879
26809
  }
25880
26810
  #windowKeys(win) {
25881
- return this.#source.keys().slice(win.startIndex, win.endIndex).filter((k) => !isSyntheticKey(k));
26811
+ const keys = this.#source.keys();
26812
+ const out = [];
26813
+ for (const [start, end] of win.ranges) for (let i = start; i < end; i++) if (!isSyntheticKey(keys[i])) out.push(keys[i]);
26814
+ return out;
26815
+ }
26816
+ /** Layout params for the current geometry, with optional overrides (used for zoom anchoring). */
26817
+ #layoutParams(over = {}) {
26818
+ return {
26819
+ count: this.#source.count,
26820
+ groups: this.groupBy === null ? void 0 : this.#source.groups(),
26821
+ itemWidth: this.itemSize,
26822
+ rowHeight: this.#rowHeight(),
26823
+ gap: this.gap,
26824
+ viewportWidth: this._viewportWidth,
26825
+ viewportHeight: this._viewportHeight,
26826
+ orientation: this.orientation,
26827
+ headerHeight: this.groupHeaderHeight,
26828
+ headerWidth: this.groupHeaderWidth,
26829
+ ...over
26830
+ };
26831
+ }
26832
+ #buildLayout(over = {}) {
26833
+ return buildLayout(this.#layoutParams(over));
26834
+ }
26835
+ #layoutCache;
26836
+ /** The current layout, rebuilt only when its inputs change (never per scroll). */
26837
+ #layout() {
26838
+ const params = this.#layoutParams();
26839
+ const cached = this.#layoutCache;
26840
+ if (cached) {
26841
+ const a = cached.params;
26842
+ let same = a.groups === params.groups;
26843
+ for (const k of Object.keys(params)) {
26844
+ if (!same) break;
26845
+ same = a[k] === params[k];
26846
+ }
26847
+ if (same) return cached.layout;
26848
+ }
26849
+ const layout = buildLayout(params);
26850
+ this.#layoutCache = {
26851
+ params,
26852
+ layout
26853
+ };
26854
+ return layout;
25882
26855
  }
25883
26856
  #currentWindow(noOverscan = false) {
25884
- const overscan = noOverscan ? 0 : 2;
25885
- return computeGridWindow(this.#source.count, this.itemSize, this.#rowHeight(), this.gap, this._viewportWidth, this._viewportHeight, this.orientation === `row` ? this._scrollLeft : this._scrollTop, this.orientation, overscan);
26857
+ return this.#layout().window(this.orientation === `row` ? this._scrollLeft : this._scrollTop, noOverscan ? 0 : 2);
25886
26858
  }
25887
26859
  /** Adjust the scroll offset so `key` is fully within the viewport. */
25888
26860
  #scrollItemIntoView(key) {
25889
26861
  const index = this.#source.indexOfKey(key);
25890
26862
  const vp = this.#viewportRef.value;
25891
26863
  if (index < 0 || !vp) return;
25892
- const win = this.#currentWindow();
25893
- if (win.horizontal) {
25894
- const left = index * win.colStride;
25895
- const right = left + this.itemSize;
26864
+ const [start, end] = this.#layout().extentOf(index);
26865
+ if (this.orientation === `row`) {
25896
26866
  const viewLeft = vp.scrollLeft;
25897
26867
  let next = viewLeft;
25898
- if (left < viewLeft) next = left;
25899
- else if (right > viewLeft + this._viewportWidth) next = right - this._viewportWidth;
26868
+ if (start < viewLeft) next = start;
26869
+ else if (end > viewLeft + this._viewportWidth) next = end - this._viewportWidth;
25900
26870
  if (next !== viewLeft) {
25901
26871
  vp.scrollLeft = next;
25902
26872
  this._scrollLeft = next;
25903
26873
  }
25904
26874
  return;
25905
26875
  }
25906
- const top = (win.cols > 0 ? Math.floor(index / win.cols) : 0) * win.rowStride;
25907
- const bottom = top + this.#rowHeight();
25908
26876
  const viewTop = vp.scrollTop;
25909
26877
  let next = viewTop;
25910
- if (top < viewTop) next = top;
25911
- else if (bottom > viewTop + this._viewportHeight) next = bottom - this._viewportHeight;
26878
+ if (start < viewTop) next = start;
26879
+ else if (end > viewTop + this._viewportHeight) next = end - this._viewportHeight;
25912
26880
  if (next !== viewTop) {
25913
26881
  vp.scrollTop = next;
25914
26882
  this._scrollTop = next;
@@ -25982,6 +26950,7 @@ let GridListElement = class GridListElement extends LitElement {
25982
26950
  >
25983
26951
  <div class="spacer" style=${spacerStyle}>
25984
26952
  ${this.#source instanceof DataGridListSource ? this.#renderDataCells(win) : this.#renderElementCells(win)}
26953
+ ${this.#renderHeaders(win)}
25985
26954
  </div>
25986
26955
  ${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
25987
26956
  </div>
@@ -25989,7 +26958,7 @@ let GridListElement = class GridListElement extends LitElement {
25989
26958
  `;
25990
26959
  }
25991
26960
  #renderDataCells(win) {
25992
- const slice = Array.from({ length: win.endIndex - win.startIndex }, (_, i) => win.startIndex + i);
26961
+ const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
25993
26962
  return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderDataCell(index, win));
25994
26963
  }
25995
26964
  #renderDataCell(index, win) {
@@ -26004,8 +26973,7 @@ let GridListElement = class GridListElement extends LitElement {
26004
26973
  const caption = style === `hidden` ? `` : this.#source.labelAt(index);
26005
26974
  const showCaption = caption !== ``;
26006
26975
  const cellHeight = win.rowStride - this.gap;
26007
- const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
26008
- const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
26976
+ const { x, y } = this.#layout().positionOf(index);
26009
26977
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
26010
26978
  const rawIcon = this.#source.iconAt(index);
26011
26979
  const icon = appearanceAbsent(rawIcon) ? void 0 : rawIcon;
@@ -26091,8 +27059,25 @@ let GridListElement = class GridListElement extends LitElement {
26091
27059
  current.startLoop?.();
26092
27060
  }
26093
27061
  #renderElementCells(win) {
26094
- const slice = this.#items.slice(win.startIndex, win.endIndex);
26095
- return repeat(slice, (_el, i) => this.#source.keyAt(win.startIndex + i), (el, i) => this.#renderCell(el, win.startIndex + i, win));
27062
+ const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
27063
+ return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderCell(this.#items[index], index, win));
27064
+ }
27065
+ #renderHeaders(win) {
27066
+ if (win.headers.length === 0) return nothing;
27067
+ const horizontal = win.horizontal;
27068
+ return repeat(win.headers, (h) => `__group__${h.key}`, (h) => html`
27069
+ <ixfx-list-group-header
27070
+ class="group-header"
27071
+ part="group-header"
27072
+ data-group-key=${h.key}
27073
+ label=${h.label}
27074
+ count=${h.count}
27075
+ orientation=${horizontal ? `vertical` : `horizontal`}
27076
+ ?collapsed=${h.collapsed}
27077
+ style=${horizontal ? `width:${h.size}px;height:100%;transform:translate(${h.offset}px,0)` : `width:100%;height:${h.size}px;transform:translate(0,${h.offset}px)`}
27078
+ @click=${() => this.toggleGroup(h.key)}
27079
+ ></ixfx-list-group-header>
27080
+ `);
26096
27081
  }
26097
27082
  #renderCell(el, index, win) {
26098
27083
  const key = this.#source.keyAt(index);
@@ -26107,8 +27092,7 @@ let GridListElement = class GridListElement extends LitElement {
26107
27092
  const caption = style === `hidden` ? `` : this.#captionFor(el);
26108
27093
  const showCaption = caption !== ``;
26109
27094
  const cellHeight = win.rowStride - this.gap;
26110
- const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
26111
- const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
27095
+ const { x, y } = this.#layout().positionOf(index);
26112
27096
  const thumbStyle = separate ? `height:${this.itemSize}px` : ``;
26113
27097
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
26114
27098
  return html`
@@ -26207,16 +27191,12 @@ let GridListElement = class GridListElement extends LitElement {
26207
27191
  #snapToNearestLine() {
26208
27192
  const vp = this.#viewportRef.value;
26209
27193
  if (!vp) return;
26210
- const win = this.#currentWindow();
26211
- const horizontal = win.horizontal;
26212
- const stride = horizontal ? win.colStride : win.rowStride;
26213
- if (stride <= 0) return;
27194
+ const layout = this.#layout();
27195
+ const horizontal = layout.horizontal;
26214
27196
  const offset = horizontal ? vp.scrollLeft : vp.scrollTop;
26215
- const extent = horizontal ? win.totalWidth : win.totalHeight;
26216
27197
  const viewportExtent = horizontal ? this._viewportWidth : this._viewportHeight;
26217
- const maxScroll = Math.max(0, extent - viewportExtent);
26218
- const index = Math.round(offset / stride);
26219
- const target = Math.min(maxScroll, Math.max(0, index * stride));
27198
+ const maxScroll = Math.max(0, layout.totalExtent - viewportExtent);
27199
+ const target = Math.min(maxScroll, Math.max(0, layout.snapTarget(offset)));
26220
27200
  if (Math.abs(target - offset) < .5) return;
26221
27201
  vp.scrollTo(horizontal ? {
26222
27202
  left: target,
@@ -26244,7 +27224,7 @@ let GridListElement = class GridListElement extends LitElement {
26244
27224
  if (mode === `manual`) return;
26245
27225
  if (this.selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
26246
27226
  if (this.#dragSelect.isDragging || this.#nativeDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
26247
- for (const node of e.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return;
27227
+ for (const node of e.composedPath()) if (node instanceof Element && (node.classList.contains(`cell`) || node.classList.contains(`group-header`))) return;
26248
27228
  this.selection.clear();
26249
27229
  }
26250
27230
  #startPostDragSuppressTimer() {
@@ -26345,6 +27325,22 @@ __decorate([property({
26345
27325
  })], GridListElement.prototype, "snapToItem", void 0);
26346
27326
  __decorate([property({ attribute: false })], GridListElement.prototype, "items", void 0);
26347
27327
  __decorate([property({ attribute: false })], GridListElement.prototype, "itemAdapter", void 0);
27328
+ __decorate([property({ attribute: `group-by` })], GridListElement.prototype, "groupBy", void 0);
27329
+ __decorate([property({ attribute: false })], GridListElement.prototype, "groupLabel", void 0);
27330
+ __decorate([property({
27331
+ type: Boolean,
27332
+ attribute: `cross-group-selection`,
27333
+ reflect: true
27334
+ })], GridListElement.prototype, "crossGroupSelection", void 0);
27335
+ __decorate([property({
27336
+ type: Number,
27337
+ attribute: `group-header-height`
27338
+ })], GridListElement.prototype, "groupHeaderHeight", void 0);
27339
+ __decorate([property({
27340
+ type: Number,
27341
+ attribute: `group-header-width`
27342
+ })], GridListElement.prototype, "groupHeaderWidth", void 0);
27343
+ __decorate([state()], GridListElement.prototype, "_collapsedGroups", void 0);
26348
27344
  __decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
26349
27345
  __decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
26350
27346
  __decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
@@ -53949,6 +54945,6 @@ function init() {
53949
54945
  console.log(`Init`);
53950
54946
  }
53951
54947
  //#endregion
53952
- export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, CheckedSelect, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, decomposeViewIsometry, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, rotationIsometry, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, wrapRoot };
54948
+ export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, CheckedSelect, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EMPTY_GROUP_LABEL, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListGroupHeaderElement, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, decomposeViewIsometry, defaultGroupLabel, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, partitionByGroup, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveGroupKey, resolveLeafPath, resolveTracks, rotationIsometry, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toGroupKey, toSnapshot, toggleInSet, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, wrapRoot };
53953
54949
 
53954
54950
  //# sourceMappingURL=index.js.map