@ixfx/components 0.11.6 → 0.11.8

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 (59) hide show
  1. package/bundle/index.d.ts +213 -2
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +1766 -165
  4. package/bundle/index.js.map +1 -1
  5. package/dist/checkbox.d.ts +3 -0
  6. package/dist/checkbox.d.ts.map +1 -1
  7. package/dist/checkbox.js +24 -1
  8. package/dist/checkbox.js.map +1 -1
  9. package/dist/checked-select-CglRukbP.d.ts +59 -0
  10. package/dist/checked-select-CglRukbP.d.ts.map +1 -0
  11. package/dist/checked-select-Dejl13qd.js +604 -0
  12. package/dist/checked-select-Dejl13qd.js.map +1 -0
  13. package/dist/checked-select.d.ts +2 -0
  14. package/dist/checked-select.js +3 -0
  15. package/dist/{detail-list-CmdCxddo.d.ts → detail-list-lE_5UWVl.d.ts} +36 -3
  16. package/dist/detail-list-lE_5UWVl.d.ts.map +1 -0
  17. package/dist/detail-list.d.ts +3 -2
  18. package/dist/detail-list.js +293 -6
  19. package/dist/detail-list.js.map +1 -1
  20. package/dist/{grid-list-D6AfWfwn.d.ts → grid-list-Bc-RkDoO.d.ts} +42 -1
  21. package/dist/grid-list-Bc-RkDoO.d.ts.map +1 -0
  22. package/dist/{grid-list-3fGhdaFA.js → grid-list-Bwaf61is.js} +659 -158
  23. package/dist/grid-list-Bwaf61is.js.map +1 -0
  24. package/dist/grid-list.d.ts +3 -2
  25. package/dist/grid-list.js +2 -1
  26. package/dist/grouping-CUKgmkMm.d.ts +63 -0
  27. package/dist/grouping-CUKgmkMm.d.ts.map +1 -0
  28. package/dist/grouping-RYPnHkrS.js +53 -0
  29. package/dist/grouping-RYPnHkrS.js.map +1 -0
  30. package/dist/{hex-editor-BiRfwTz1.js → hex-editor-DiYpgIKs.js} +2 -2
  31. package/dist/{hex-editor-BiRfwTz1.js.map → hex-editor-DiYpgIKs.js.map} +1 -1
  32. package/dist/hex.js +1 -1
  33. package/dist/index.d.ts +6 -3
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +9 -6
  36. package/dist/index.js.map +1 -1
  37. package/dist/list-group-header.d.ts +26 -0
  38. package/dist/list-group-header.d.ts.map +1 -0
  39. package/dist/list-group-header.js +122 -0
  40. package/dist/list-group-header.js.map +1 -0
  41. package/dist/{list-selection-controller-SA4piSBi.js → list-selection-controller-BeE70lbB.js} +42 -4
  42. package/dist/list-selection-controller-BeE70lbB.js.map +1 -0
  43. package/dist/plots.js +1 -1
  44. package/dist/vertical-list.js +1 -1
  45. package/dist/{xy-axis-COKw338Q.js → xy-axis-CyMd_Bn1.js} +2 -2
  46. package/dist/{xy-axis-COKw338Q.js.map → xy-axis-CyMd_Bn1.js.map} +1 -1
  47. package/docs-user/README.md +2 -0
  48. package/docs-user/checked-select.md +63 -0
  49. package/docs-user/detail-list.md +28 -0
  50. package/docs-user/grid-list.md +35 -3
  51. package/docs-user/index.json +12 -2
  52. package/docs-user/list-group-header.md +14 -0
  53. package/docs-user/llms.txt +2 -0
  54. package/llms.txt +2 -0
  55. package/package.json +9 -1
  56. package/dist/detail-list-CmdCxddo.d.ts.map +0 -1
  57. package/dist/grid-list-3fGhdaFA.js.map +0 -1
  58. package/dist/grid-list-D6AfWfwn.d.ts.map +0 -1
  59. package/dist/list-selection-controller-SA4piSBi.js.map +0 -1
package/bundle/index.js CHANGED
@@ -5,10 +5,10 @@ import { styleMap } from "lit/directives/style-map.js";
5
5
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
6
6
  import { Fzf } from "fzf";
7
7
  import { createCommandRegistry } from "@clinth/ui-commands";
8
+ import { repeat } from "lit/directives/repeat.js";
8
9
  import { Colour } from "@ixfx/visual";
9
10
  import { Trees } from "@ixfx/collections";
10
11
  import { clamp, clamper, rangeStream, round, scaler } from "@ixfx/numbers";
11
- import { repeat } from "lit/directives/repeat.js";
12
12
  import { classMap } from "lit/directives/class-map.js";
13
13
  import { createRef, ref } from "lit/directives/ref.js";
14
14
  import { Easings } from "@ixfx/modulation";
@@ -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;
@@ -8238,6 +8326,7 @@ SplitButton = __decorate([safeCustomElement(`ixfx-split-button`)], SplitButton);
8238
8326
  * Properties
8239
8327
  * checked – checkbox state (default: false)
8240
8328
  * disabled – disabled state (default: false)
8329
+ * readonly – display-only: no pointer/keyboard interaction, full opacity (default: false)
8241
8330
  * label – label text
8242
8331
  * name – form field name
8243
8332
  * value – form field value
@@ -8269,12 +8358,14 @@ let Checkbox = class Checkbox extends LitElement {
8269
8358
  this.checked = false;
8270
8359
  this.disabled = false;
8271
8360
  this.indeterminate = false;
8361
+ this.readonly = false;
8272
8362
  this.label = ``;
8273
8363
  this.name = ``;
8274
8364
  this.value = ``;
8275
8365
  this.#handleChange = (e) => {
8276
8366
  const target = e.target;
8277
8367
  this.checked = target.checked;
8368
+ this.indeterminate = false;
8278
8369
  this.dispatchEvent(new CustomEvent(`change`, {
8279
8370
  detail: { checked: this.checked },
8280
8371
  bubbles: true,
@@ -8337,6 +8428,16 @@ let Checkbox = class Checkbox extends LitElement {
8337
8428
  opacity: var(--box-opacity-checked);
8338
8429
  }
8339
8430
 
8431
+ /* Display-only: inert, keeps full opacity, no hover reveal */
8432
+ :host([readonly]) {
8433
+ pointer-events: none;
8434
+ }
8435
+ :host([readonly]) label,
8436
+ :host([readonly]) label:hover {
8437
+ cursor: default;
8438
+ opacity: var(--box-opacity-checked);
8439
+ }
8440
+
8340
8441
  input {
8341
8442
  position: absolute;
8342
8443
  opacity: 0;
@@ -8448,6 +8549,10 @@ let Checkbox = class Checkbox extends LitElement {
8448
8549
  `
8449
8550
  ];
8450
8551
  }
8552
+ /** Focus the inner input, so the host can be focused like a native checkbox. */
8553
+ focus(options) {
8554
+ this.renderRoot.querySelector(`input`)?.focus(options);
8555
+ }
8451
8556
  #handleChange;
8452
8557
  render() {
8453
8558
  return html`
@@ -8457,6 +8562,8 @@ let Checkbox = class Checkbox extends LitElement {
8457
8562
  type="checkbox"
8458
8563
  .checked=${this.checked}
8459
8564
  .disabled=${this.disabled}
8565
+ tabindex=${this.readonly ? `-1` : nothing}
8566
+ aria-hidden=${this.readonly ? `true` : nothing}
8460
8567
  .indeterminate=${this.indeterminate}
8461
8568
  .name=${this.name}
8462
8569
  .value=${this.value}
@@ -8471,10 +8578,605 @@ let Checkbox = class Checkbox extends LitElement {
8471
8578
  __decorate([property({ type: Boolean })], Checkbox.prototype, "checked", void 0);
8472
8579
  __decorate([property({ type: Boolean })], Checkbox.prototype, "disabled", void 0);
8473
8580
  __decorate([property({ type: Boolean })], Checkbox.prototype, "indeterminate", void 0);
8581
+ __decorate([property({
8582
+ type: Boolean,
8583
+ reflect: true
8584
+ })], Checkbox.prototype, "readonly", void 0);
8474
8585
  __decorate([property({ type: String })], Checkbox.prototype, "label", void 0);
8475
8586
  __decorate([property({ type: String })], Checkbox.prototype, "name", void 0);
8476
8587
  __decorate([property({ type: String })], Checkbox.prototype, "value", void 0);
8477
8588
  Checkbox = __decorate([safeCustomElement(`ixfx-checkbox`)], Checkbox);
8589
+ //#endregion
8590
+ //#region src/checked-select/logic.ts
8591
+ function byLabel(a, b) {
8592
+ return a.label.localeCompare(b.label, void 0, { sensitivity: `base` });
8593
+ }
8594
+ /** Checked/indeterminate items first, then the rest; each group sorted by label. */
8595
+ function sortForOpen(items) {
8596
+ const on = items.filter((i) => i.checked !== `false`).toSorted(byLabel);
8597
+ const off = items.filter((i) => i.checked === `false`).toSorted(byLabel);
8598
+ return [...on, ...off];
8599
+ }
8600
+ /** User toggle: indeterminate and unchecked become checked, checked becomes unchecked. */
8601
+ function toggleState(state) {
8602
+ return state === `true` ? `false` : `true`;
8603
+ }
8604
+ /** Items shown in the resting state, alphabetically. */
8605
+ function visibleAtRest(items) {
8606
+ return items.filter((i) => i.checked !== `false`).toSorted(byLabel);
8607
+ }
8608
+ const VARIES_LABEL = `(varies)`;
8609
+ /** Resting text entries: checked labels, then a single `(varies)` if any are indeterminate. */
8610
+ function restTextParts(items) {
8611
+ const on = items.filter((i) => i.checked === `true`).toSorted(byLabel).map((i) => i.label);
8612
+ const varies = items.some((i) => i.checked === `indeterminate`) ? [VARIES_LABEL] : [];
8613
+ return [...on, ...varies];
8614
+ }
8615
+ /** Plain-text resting summary, comma separated. */
8616
+ function restText(items) {
8617
+ return restTextParts(items).join(`, `);
8618
+ }
8619
+ /** Returns a new list with the item matching `value` toggled. */
8620
+ function applyToggle(items, value) {
8621
+ return items.map((i) => i.value === value ? {
8622
+ ...i,
8623
+ checked: toggleState(i.checked)
8624
+ } : i);
8625
+ }
8626
+ /**
8627
+ * Index of the next label after `current` beginning with `char` (case-insensitive),
8628
+ * wrapping around. Returns -1 if nothing matches.
8629
+ */
8630
+ function nextMatchIndex(labels, current, char) {
8631
+ const c = char.toLocaleLowerCase();
8632
+ const count = labels.length;
8633
+ for (let step = 1; step <= count; step++) {
8634
+ const index = (current + step + count) % count;
8635
+ if (labels[index].toLocaleLowerCase().startsWith(c)) return index;
8636
+ }
8637
+ return -1;
8638
+ }
8639
+ /** Items in `draft` whose state differs from `original`. */
8640
+ function diffItems(original, draft) {
8641
+ const before = new Map(original.map((i) => [i.value, i.checked]));
8642
+ return draft.filter((i) => before.get(i.value) !== i.checked);
8643
+ }
8644
+ function clampSize(size, min, max) {
8645
+ return {
8646
+ width: Math.min(Math.max(size.width, min.width), Math.max(max.width, min.width)),
8647
+ height: Math.min(Math.max(size.height, min.height), Math.max(max.height, min.height))
8648
+ };
8649
+ }
8650
+ //#endregion
8651
+ //#region src/checked-select/checked-select.ts
8652
+ let instanceCounter = 0;
8653
+ let CheckedSelect = class CheckedSelect extends LitElement {
8654
+ constructor(..._args) {
8655
+ super(..._args);
8656
+ this.items = [];
8657
+ this.multiline = false;
8658
+ this.itemsInert = false;
8659
+ this.itemDisplay = `checkbox`;
8660
+ this.orientation = `horizontal`;
8661
+ this.placeholder = ``;
8662
+ this.disabled = false;
8663
+ this._open = false;
8664
+ this._draft = [];
8665
+ this._order = [];
8666
+ this.#anchorName = `--checked-select-anchor-${instanceCounter++}`;
8667
+ this.#tickled = new TickledController(this, { getItems: () => this._order.map((_, i) => ({ id: i })) });
8668
+ this.#onRestCheckboxChange = (e, value) => {
8669
+ e.stopPropagation();
8670
+ if (this._open) return;
8671
+ const next = applyToggle(this.items, value);
8672
+ this.#emitChange(next, diffItems(this.items, next));
8673
+ this.#focusRestCheckbox(value);
8674
+ };
8675
+ this.#onDocumentPointerDown = (e) => {
8676
+ const path = e.composedPath();
8677
+ const popup = this.#popup;
8678
+ const trigger = this.#trigger;
8679
+ if (popup && path.includes(popup) || trigger && path.includes(trigger)) return;
8680
+ this.cancel();
8681
+ };
8682
+ this.#onTriggerClick = (e) => {
8683
+ if (e.target.closest?.(`ixfx-checkbox`)) return;
8684
+ const scroller = e.composedPath()[0];
8685
+ if (scroller.classList?.contains(`rest-items`) && e instanceof MouseEvent && (e.offsetX > scroller.clientWidth || e.offsetY > scroller.clientHeight)) return;
8686
+ if (this._open) this.commit();
8687
+ else this.open();
8688
+ };
8689
+ this.#onTriggerKeyDown = (e) => {
8690
+ if (this._open || e.target !== e.currentTarget) return;
8691
+ if (e.key === `Enter` || e.key === ` ` || e.key === `ArrowDown`) {
8692
+ e.preventDefault();
8693
+ this.open();
8694
+ }
8695
+ };
8696
+ this.#onListKeyDown = (e) => {
8697
+ const rows = this.#rows();
8698
+ switch (e.key) {
8699
+ case `ArrowDown`:
8700
+ e.preventDefault();
8701
+ this.#tickled.handleKeyboardNavigation(`next`);
8702
+ this.#scrollToCursor();
8703
+ return;
8704
+ case `ArrowUp`:
8705
+ e.preventDefault();
8706
+ this.#tickled.handleKeyboardNavigation(`previous`);
8707
+ this.#scrollToCursor();
8708
+ return;
8709
+ case `Home`:
8710
+ e.preventDefault();
8711
+ this.#tickled.handleKeyboardNavigation(`first`);
8712
+ this.#scrollToCursor();
8713
+ return;
8714
+ case `End`:
8715
+ e.preventDefault();
8716
+ this.#tickled.handleKeyboardNavigation(`last`);
8717
+ this.#scrollToCursor();
8718
+ return;
8719
+ case ` `: {
8720
+ e.preventDefault();
8721
+ const row = rows[this.#tickled.tickledId ?? -1];
8722
+ if (row) this.#toggleRow(row.value);
8723
+ return;
8724
+ }
8725
+ case `Enter`:
8726
+ e.preventDefault();
8727
+ this.commit();
8728
+ return;
8729
+ case `Escape`:
8730
+ e.preventDefault();
8731
+ this.cancel();
8732
+ return;
8733
+ case `Tab`:
8734
+ this.cancel();
8735
+ return;
8736
+ }
8737
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
8738
+ const next = nextMatchIndex(rows.map((r) => r.label), this.#tickled.tickledId ?? -1, e.key);
8739
+ if (next >= 0) {
8740
+ this.#tickled.handleKeyboardNavigation(next);
8741
+ this.#scrollToCursor();
8742
+ }
8743
+ }
8744
+ };
8745
+ this.#onResizeDown = (e) => {
8746
+ const frame = this.shadowRoot?.querySelector(`.frame`);
8747
+ const zone = e.currentTarget;
8748
+ if (!frame) return;
8749
+ e.preventDefault();
8750
+ const start = frame.getBoundingClientRect();
8751
+ const originX = e.clientX;
8752
+ const originY = e.clientY;
8753
+ const style = getComputedStyle(this);
8754
+ const min = {
8755
+ width: Number.parseFloat(style.getPropertyValue(`--checked-select-min-width`)) || 128,
8756
+ height: Number.parseFloat(style.getPropertyValue(`--checked-select-min-height`)) || 80
8757
+ };
8758
+ const max = {
8759
+ width: window.innerWidth - start.left - 8,
8760
+ height: window.innerHeight - start.top - 8
8761
+ };
8762
+ zone.setPointerCapture?.(e.pointerId);
8763
+ zone.setAttribute(`data-active`, ``);
8764
+ const onMove = (m) => {
8765
+ this._size = clampSize({
8766
+ width: start.width + m.clientX - originX,
8767
+ height: start.height + m.clientY - originY
8768
+ }, min, max);
8769
+ };
8770
+ const onEnd = () => {
8771
+ zone.removeAttribute(`data-active`);
8772
+ zone.removeEventListener(`pointermove`, onMove);
8773
+ zone.removeEventListener(`pointerup`, onEnd);
8774
+ zone.removeEventListener(`pointercancel`, onEnd);
8775
+ this.#listbox?.focus();
8776
+ };
8777
+ zone.addEventListener(`pointermove`, onMove);
8778
+ zone.addEventListener(`pointerup`, onEnd);
8779
+ zone.addEventListener(`pointercancel`, onEnd);
8780
+ };
8781
+ }
8782
+ static {
8783
+ this.styles = [
8784
+ themeFallbacks,
8785
+ handleStyles,
8786
+ tickledStyles,
8787
+ tickledItemStyles,
8788
+ css`
8789
+ :host {
8790
+ --checked-select-gap: var(--space-m);
8791
+ --checked-select-padding: var(--space-s) var(--space-m);
8792
+ --checked-select-caret-width: 2rem;
8793
+ --checked-select-rest-max-height: 8rem;
8794
+ --checked-select-max-height: 16rem;
8795
+ --checked-select-min-width: 8rem;
8796
+ --checked-select-min-height: 5rem;
8797
+
8798
+ display: block;
8799
+ position: relative;
8800
+ }
8801
+
8802
+ :host([disabled]) {
8803
+ opacity: var(--prominence-disabled-opacity, 0.4);
8804
+ pointer-events: none;
8805
+ }
8806
+
8807
+ .rest {
8808
+ position: relative;
8809
+ box-sizing: border-box;
8810
+ cursor: pointer;
8811
+ background: var(--surface-4);
8812
+ color: var(--surface-4-text);
8813
+ border: var(--border);
8814
+ border-radius: var(--radius-m);
8815
+ user-select: none;
8816
+ }
8817
+
8818
+ .rest-items {
8819
+ display: flex;
8820
+ flex-wrap: nowrap;
8821
+ align-items: center;
8822
+ gap: var(--checked-select-gap);
8823
+ padding: var(--checked-select-padding);
8824
+ /* Room to scroll the last item clear of the caret */
8825
+ padding-inline-end: var(--checked-select-caret-width);
8826
+ min-height: 1.5em;
8827
+ box-sizing: border-box;
8828
+ overflow-x: auto;
8829
+ overflow-y: hidden;
8830
+ scrollbar-width: thin;
8831
+ }
8832
+ :host([multiline]) .rest-items {
8833
+ flex-wrap: wrap;
8834
+ overflow-x: hidden;
8835
+ overflow-y: auto;
8836
+ max-height: var(--checked-select-rest-max-height);
8837
+ /* Wrapping never runs under the caret, so keep its column clear (and the scrollbar beside it) */
8838
+ padding-inline-end: var(--space-m);
8839
+ margin-inline-end: var(--checked-select-caret-width);
8840
+ }
8841
+
8842
+ /* Vertical: one item per row, scrolling vertically beside the caret. Must follow the multiline rule. */
8843
+ :host([orientation="vertical"]) .rest-items {
8844
+ flex-direction: column;
8845
+ flex-wrap: nowrap;
8846
+ align-items: flex-start;
8847
+ overflow-x: hidden;
8848
+ overflow-y: auto;
8849
+ max-height: var(--checked-select-rest-max-height);
8850
+ padding-inline-end: var(--space-m);
8851
+ margin-inline-end: var(--checked-select-caret-width);
8852
+ }
8853
+
8854
+ /* Always-visible caret; items scroll underneath it */
8855
+ .caret {
8856
+ position: absolute;
8857
+ inset-block: 0;
8858
+ inset-inline-end: 0;
8859
+ width: var(--checked-select-caret-width);
8860
+ display: grid;
8861
+ place-items: center;
8862
+ background: inherit;
8863
+ border-radius: inherit;
8864
+ pointer-events: auto;
8865
+ }
8866
+ .caret svg {
8867
+ width: 1.1em;
8868
+ height: 1.1em;
8869
+ fill: none;
8870
+ stroke: currentColor;
8871
+ stroke-width: 2;
8872
+ stroke-linecap: round;
8873
+ stroke-linejoin: round;
8874
+ transition: rotate var(--transition, 0.1s ease-out);
8875
+ }
8876
+ .rest[aria-expanded="true"] .caret svg {
8877
+ rotate: 180deg;
8878
+ }
8879
+ /* While the popup is open its draft is authoritative */
8880
+ .rest[aria-expanded="true"] ixfx-checkbox {
8881
+ pointer-events: none;
8882
+ }
8883
+ .rest:hover {
8884
+ background: var(--surface-5);
8885
+ }
8886
+ .rest:focus-visible {
8887
+ outline: 2px solid var(--accent);
8888
+ outline-offset: 2px;
8889
+ }
8890
+ .rest ixfx-checkbox {
8891
+ flex-shrink: 0;
8892
+ }
8893
+ .rest-text {
8894
+ white-space: nowrap;
8895
+ }
8896
+ :host([multiline]) .rest-text {
8897
+ white-space: normal;
8898
+ }
8899
+ .placeholder {
8900
+ opacity: 0.6;
8901
+ }
8902
+
8903
+ .popup {
8904
+ /* Reset popover UA styles */
8905
+ inset: unset;
8906
+ margin: 0;
8907
+ padding: 0;
8908
+ border: none;
8909
+ background: transparent;
8910
+ overflow: visible;
8911
+
8912
+ /* CSS anchor positioning — position-anchor set inline */
8913
+ position: fixed;
8914
+ left: anchor(left);
8915
+ top: anchor(bottom);
8916
+ /* anchor-size() only works on the anchored element itself, so the width lives here */
8917
+ width: anchor-size(width);
8918
+ min-width: var(--checked-select-min-width);
8919
+ position-try-fallbacks: flip-block;
8920
+ margin-block: 4px;
8921
+ z-index: 100;
8922
+ }
8923
+
8924
+ .frame {
8925
+ position: relative;
8926
+ box-sizing: border-box;
8927
+ display: flex;
8928
+ flex-direction: column;
8929
+ width: 100%;
8930
+ max-height: var(--checked-select-max-height);
8931
+ background: var(--surface-3);
8932
+ color: var(--surface-3-text);
8933
+ border: var(--border);
8934
+ border-radius: var(--radius-m);
8935
+ }
8936
+
8937
+ .listbox {
8938
+ flex: 1;
8939
+ min-height: 0;
8940
+ overflow-y: auto;
8941
+ padding: var(--space-s);
8942
+ outline: none;
8943
+ }
8944
+ .listbox:focus-visible {
8945
+ outline: 2px solid var(--accent);
8946
+ outline-offset: -2px;
8947
+ }
8948
+
8949
+ .item {
8950
+ display: flex;
8951
+ width: 100%;
8952
+ box-sizing: border-box;
8953
+ }
8954
+
8955
+ /* Key-repeat outpaces the fade, smearing the cursor across rows. Snap while
8956
+ the keyboard drives the cursor; hover (mouse mode) keeps the transition. */
8957
+ .listbox[data-input-mode="keyboard"] .item {
8958
+ transition: none;
8959
+ }
8960
+
8961
+ .resize-zone {
8962
+ position: absolute;
8963
+ inset-block-end: 0;
8964
+ inset-inline-end: 0;
8965
+ width: 16px;
8966
+ height: 16px;
8967
+ display: grid;
8968
+ place-items: center;
8969
+ cursor: nwse-resize;
8970
+ touch-action: none;
8971
+ }
8972
+ .resize-zone .ixfx-handle {
8973
+ --ixfx-handle-spot-size: 10px;
8974
+ }
8975
+ `
8976
+ ];
8977
+ }
8978
+ #anchorName;
8979
+ #tickled;
8980
+ get isOpen() {
8981
+ return this._open;
8982
+ }
8983
+ get #trigger() {
8984
+ return this.shadowRoot?.querySelector(`.rest`) ?? null;
8985
+ }
8986
+ get #popup() {
8987
+ return this.shadowRoot?.querySelector(`.popup`) ?? null;
8988
+ }
8989
+ get #listbox() {
8990
+ return this.shadowRoot?.querySelector(`.listbox`) ?? null;
8991
+ }
8992
+ /** Draft items in their frozen open-time order. */
8993
+ #rows() {
8994
+ const byValue = new Map(this._draft.map((i) => [i.value, i]));
8995
+ return this._order.flatMap((v) => byValue.get(v) ?? []);
8996
+ }
8997
+ disconnectedCallback() {
8998
+ super.disconnectedCallback();
8999
+ document.removeEventListener(`pointerdown`, this.#onDocumentPointerDown, true);
9000
+ }
9001
+ willUpdate(changed) {
9002
+ super.willUpdate(changed);
9003
+ if (changed.has(`disabled`) && this.disabled && this._open) this.cancel();
9004
+ }
9005
+ async open() {
9006
+ if (this._open || this.disabled) return;
9007
+ this._draft = this.items;
9008
+ this._order = sortForOpen(this.items).map((i) => i.value);
9009
+ this._size = void 0;
9010
+ this._open = true;
9011
+ if (this._order.length > 0) this.#tickled.handleKeyboardNavigation(`first`);
9012
+ document.addEventListener(`pointerdown`, this.#onDocumentPointerDown, true);
9013
+ await this.updateComplete;
9014
+ this.#popup?.showPopover?.();
9015
+ this.#listbox?.focus();
9016
+ }
9017
+ commit() {
9018
+ if (!this._open) return;
9019
+ const changed = diffItems(this.items, this._draft);
9020
+ const next = this._draft;
9021
+ this.#close();
9022
+ if (changed.length === 0) return;
9023
+ this.#emitChange(next, changed);
9024
+ }
9025
+ #emitChange(next, changed) {
9026
+ this.items = next;
9027
+ this.dispatchEvent(new CustomEvent(`change`, {
9028
+ detail: {
9029
+ items: next,
9030
+ changed
9031
+ },
9032
+ bubbles: true,
9033
+ composed: true
9034
+ }));
9035
+ }
9036
+ /** A resting checkbox was used directly: toggle and commit immediately. */
9037
+ #onRestCheckboxChange;
9038
+ async #focusRestCheckbox(value) {
9039
+ await this.updateComplete;
9040
+ (this.shadowRoot?.querySelector(`.rest ixfx-checkbox[data-value="${CSS.escape(value)}"]`) ?? this.#trigger)?.focus();
9041
+ }
9042
+ cancel() {
9043
+ if (!this._open) return;
9044
+ this.#close();
9045
+ }
9046
+ #close() {
9047
+ document.removeEventListener(`pointerdown`, this.#onDocumentPointerDown, true);
9048
+ this.#popup?.hidePopover?.();
9049
+ this._open = false;
9050
+ this._draft = [];
9051
+ this.#tickled.clearTickled();
9052
+ this.#trigger?.focus();
9053
+ }
9054
+ #onDocumentPointerDown;
9055
+ #onTriggerClick;
9056
+ #onTriggerKeyDown;
9057
+ #toggleRow(value) {
9058
+ this._draft = applyToggle(this._draft, value);
9059
+ }
9060
+ #onRowClick(value, index) {
9061
+ this.#tickled.setTickled(index);
9062
+ this.#toggleRow(value);
9063
+ }
9064
+ #onListKeyDown;
9065
+ async #scrollToCursor() {
9066
+ await this.updateComplete;
9067
+ const index = this.#tickled.tickledId;
9068
+ this.shadowRoot?.querySelector(`[data-index="${index}"]`)?.scrollIntoView?.({ block: `nearest` });
9069
+ }
9070
+ #onResizeDown;
9071
+ #renderRest() {
9072
+ const shown = visibleAtRest(this.items);
9073
+ if (shown.length === 0) return html`<span class="placeholder">${this.placeholder}</span>`;
9074
+ if (this.itemDisplay === `text`) {
9075
+ if (this.orientation === `vertical`) return restTextParts(this.items).map((part) => html`<span class="rest-text">${part}</span>`);
9076
+ return html`<span class="rest-text">${restText(this.items)}</span>`;
9077
+ }
9078
+ return repeat(shown, (i) => i.value, (i) => html`
9079
+ <ixfx-checkbox
9080
+ ?readonly=${this.itemsInert}
9081
+ data-value=${i.value}
9082
+ label=${i.label}
9083
+ .checked=${i.checked === `true`}
9084
+ .indeterminate=${i.checked === `indeterminate`}
9085
+ @change=${(e) => this.#onRestCheckboxChange(e, i.value)}
9086
+ ></ixfx-checkbox>`);
9087
+ }
9088
+ #renderRow(row, index) {
9089
+ return html`
9090
+ <div
9091
+ class="item ${this.#tickled.isTickled(index) ? `tickled` : ``}"
9092
+ role="option"
9093
+ id="opt-${index}"
9094
+ data-index=${index}
9095
+ aria-checked=${row.checked === `indeterminate` ? `mixed` : row.checked}
9096
+ @pointerenter=${() => this.#tickled.handlePointerEnter(index)}
9097
+ @click=${() => this.#onRowClick(row.value, index)}
9098
+ >
9099
+ <ixfx-checkbox
9100
+ readonly
9101
+ label=${row.label}
9102
+ .checked=${row.checked === `true`}
9103
+ .indeterminate=${row.checked === `indeterminate`}
9104
+ ></ixfx-checkbox>
9105
+ </div>`;
9106
+ }
9107
+ render() {
9108
+ const rows = this._open ? this.#rows() : [];
9109
+ const cursor = this.#tickled.tickledId;
9110
+ const popupSize = this._size ? `width: ${this._size.width}px;` : ``;
9111
+ const frameStyle = this._size ? `height: ${this._size.height}px; max-height: none;` : ``;
9112
+ const summary = visibleAtRest(this.items).map((i) => i.label).join(`, `);
9113
+ return html`
9114
+ <div
9115
+ class="rest"
9116
+ role="combobox"
9117
+ tabindex=${this.disabled ? `-1` : `0`}
9118
+ aria-haspopup="listbox"
9119
+ aria-expanded=${this._open ? `true` : `false`}
9120
+ aria-disabled=${this.disabled ? `true` : nothing}
9121
+ title=${summary || nothing}
9122
+ style="anchor-name: ${this.#anchorName}"
9123
+ @click=${this.#onTriggerClick}
9124
+ @keydown=${this.#onTriggerKeyDown}
9125
+ >
9126
+ <div class="rest-items">${this.#renderRest()}</div>
9127
+ <div class="caret" aria-hidden="true">
9128
+ <svg viewBox="0 0 24 24"><path d="m6 9 6 6 6-6" /></svg>
9129
+ </div>
9130
+ </div>
9131
+ <div class="popup" popover="manual" style="position-anchor: ${this.#anchorName}; ${popupSize}">
9132
+ <div class="frame" style=${frameStyle}>
9133
+ <div
9134
+ class="listbox"
9135
+ role="listbox"
9136
+ aria-multiselectable="true"
9137
+ tabindex="-1"
9138
+ aria-activedescendant=${cursor === void 0 ? nothing : `opt-${cursor}`}
9139
+ data-input-mode=${this.#tickled.inputMode}
9140
+ @pointerleave=${(e) => this.#tickled.handlePointerLeave(e)}
9141
+ @keydown=${this.#onListKeyDown}
9142
+ >${rows.map((r, i) => this.#renderRow(r, i))}</div>
9143
+ <div class="resize-zone" @pointerdown=${this.#onResizeDown}>
9144
+ <span class="ixfx-handle" data-handle="dotted" data-shape="spot" part="handle"></span>
9145
+ </div>
9146
+ </div>
9147
+ </div>
9148
+ `;
9149
+ }
9150
+ };
9151
+ __decorate([property({ type: Array })], CheckedSelect.prototype, "items", void 0);
9152
+ __decorate([property({
9153
+ type: Boolean,
9154
+ reflect: true
9155
+ })], CheckedSelect.prototype, "multiline", void 0);
9156
+ __decorate([property({
9157
+ type: Boolean,
9158
+ attribute: `items-inert`,
9159
+ reflect: true
9160
+ })], CheckedSelect.prototype, "itemsInert", void 0);
9161
+ __decorate([property({
9162
+ type: String,
9163
+ attribute: `item-display`,
9164
+ reflect: true
9165
+ })], CheckedSelect.prototype, "itemDisplay", void 0);
9166
+ __decorate([property({
9167
+ type: String,
9168
+ reflect: true
9169
+ })], CheckedSelect.prototype, "orientation", void 0);
9170
+ __decorate([property({ type: String })], CheckedSelect.prototype, "placeholder", void 0);
9171
+ __decorate([property({
9172
+ type: Boolean,
9173
+ reflect: true
9174
+ })], CheckedSelect.prototype, "disabled", void 0);
9175
+ __decorate([state()], CheckedSelect.prototype, "_open", void 0);
9176
+ __decorate([state()], CheckedSelect.prototype, "_draft", void 0);
9177
+ __decorate([state()], CheckedSelect.prototype, "_order", void 0);
9178
+ __decorate([state()], CheckedSelect.prototype, "_size", void 0);
9179
+ CheckedSelect = __decorate([safeCustomElement(`ixfx-checked-select`)], CheckedSelect);
8478
9180
  const RANGE_LIMITS = {
8479
9181
  l: [0, 1],
8480
9182
  c: [0, .4],
@@ -16559,9 +17261,134 @@ __decorate([state()], DataTableElement.prototype, "_detailIndex", void 0);
16559
17261
  __decorate([state()], DataTableElement.prototype, "_stacked", void 0);
16560
17262
  DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableElement);
16561
17263
  //#endregion
17264
+ //#region src/list-group-header/list-group-header.ts
17265
+ let ListGroupHeaderElement = class ListGroupHeaderElement extends LitElement {
17266
+ constructor(..._args) {
17267
+ super(..._args);
17268
+ this.label = ``;
17269
+ this.count = 0;
17270
+ this.collapsed = false;
17271
+ this.orientation = `horizontal`;
17272
+ }
17273
+ static {
17274
+ this.styles = css`
17275
+ :host {
17276
+ --_height: var(--list-group-header-height, 28px);
17277
+ --_border: var(--list-group-header-border, var(--border));
17278
+
17279
+ display: flex;
17280
+ align-items: center;
17281
+ gap: var(--space-s);
17282
+ box-sizing: border-box;
17283
+ min-height: var(--_height);
17284
+ padding: 0 var(--space-s);
17285
+ background: transparent;
17286
+ color: var(--list-group-header-text, var(--surface-3-text));
17287
+ border-bottom: 1px solid var(--_border);
17288
+ font-weight: 600;
17289
+ cursor: pointer;
17290
+ user-select: none;
17291
+ }
17292
+
17293
+ .caret {
17294
+ display: inline-flex;
17295
+ transition: transform 0.15s;
17296
+ }
17297
+
17298
+ :host([collapsed]) .caret {
17299
+ transform: rotate(-90deg);
17300
+ }
17301
+
17302
+ .count {
17303
+ opacity: 0.6;
17304
+ font-weight: 400;
17305
+ font-size: 0.9em;
17306
+ }
17307
+
17308
+ .label {
17309
+ overflow: hidden;
17310
+ text-overflow: ellipsis;
17311
+ white-space: nowrap;
17312
+ min-width: 0;
17313
+ }
17314
+
17315
+ .divider {
17316
+ flex: 1;
17317
+ height: 1px;
17318
+ background: var(--_border);
17319
+ margin-left: var(--space-s);
17320
+ }
17321
+
17322
+ /* Single-row (grid-list \`row\` orientation) layout: a narrow column cell. */
17323
+ :host([orientation="vertical"]) {
17324
+ flex-direction: column;
17325
+ justify-content: flex-start;
17326
+ padding: var(--space-s) 0;
17327
+ border-bottom: 0;
17328
+ border-right: 1px solid var(--_border);
17329
+ min-height: 0;
17330
+ }
17331
+
17332
+ :host([orientation="vertical"]) .divider {
17333
+ width: 1px;
17334
+ height: auto;
17335
+ margin-left: 0;
17336
+ margin-top: var(--space-s);
17337
+ }
17338
+
17339
+ @media (prefers-reduced-motion: reduce) {
17340
+ .caret { transition: none; }
17341
+ }
17342
+ `;
17343
+ }
17344
+ connectedCallback() {
17345
+ super.connectedCallback();
17346
+ this.setAttribute(`role`, `button`);
17347
+ }
17348
+ willUpdate() {
17349
+ this.setAttribute(`aria-expanded`, this.collapsed ? `false` : `true`);
17350
+ this.setAttribute(`aria-label`, `${this.label}, ${this.count} ${this.count === 1 ? `item` : `items`}`);
17351
+ }
17352
+ render() {
17353
+ return html`
17354
+ <span class="caret"><ixfx-icon name="chevron-down" size="12"></ixfx-icon></span>
17355
+ <span class="label" part="label">${this.label}</span>
17356
+ <span class="count" part="count">(${this.count})</span>
17357
+ <span class="divider" part="divider"></span>
17358
+ `;
17359
+ }
17360
+ };
17361
+ __decorate([property({
17362
+ type: String,
17363
+ reflect: true
17364
+ })], ListGroupHeaderElement.prototype, "label", void 0);
17365
+ __decorate([property({
17366
+ type: Number,
17367
+ reflect: true
17368
+ })], ListGroupHeaderElement.prototype, "count", void 0);
17369
+ __decorate([property({
17370
+ type: Boolean,
17371
+ reflect: true
17372
+ })], ListGroupHeaderElement.prototype, "collapsed", void 0);
17373
+ __decorate([property({
17374
+ type: String,
17375
+ reflect: true
17376
+ })], ListGroupHeaderElement.prototype, "orientation", void 0);
17377
+ ListGroupHeaderElement = __decorate([safeCustomElement(`ixfx-list-group-header`)], ListGroupHeaderElement);
17378
+ //#endregion
16562
17379
  //#region src/detail-list/detail-list.ts
16563
17380
  const ITEM_SELECTOR$3 = `:scope > li`;
17381
+ const GROUP_HEADER_ATTR = `data-ixfx-group-header`;
17382
+ const GROUP_COLLAPSED_ATTR = `data-group-collapsed`;
16564
17383
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
17384
+ const NAV_KEYS = /* @__PURE__ */ new Set([
17385
+ `ArrowDown`,
17386
+ `ArrowUp`,
17387
+ `ArrowLeft`,
17388
+ `ArrowRight`,
17389
+ `Home`,
17390
+ `End`
17391
+ ]);
16565
17392
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
16566
17393
  /**
16567
17394
  * Layout for the per-item badge wrapper `#ensureBadge` adds. Applied as inline
@@ -16610,14 +17437,26 @@ let DetailListElement = class DetailListElement extends LitElement {
16610
17437
  this.keyProperty = ``;
16611
17438
  this.itemWrap = `stop`;
16612
17439
  this.itemsDraggable = false;
17440
+ this.groupBy = null;
17441
+ this.crossGroupSelection = false;
17442
+ this._collapsedGroups = /* @__PURE__ */ new Set();
16613
17443
  this.#itemData = /* @__PURE__ */ new WeakMap();
16614
17444
  this.#grid = [];
16615
17445
  this.#positions = /* @__PURE__ */ new Map();
17446
+ this.#tops = /* @__PURE__ */ new Map();
16616
17447
  this.#geometryScheduled = false;
16617
17448
  this.#sortRaf = 0;
16618
17449
  this.#viewportScrollRaf = 0;
16619
17450
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
16620
17451
  this.#builtFromProvider = [];
17452
+ this.#groupIndex = /* @__PURE__ */ new Map();
17453
+ this.#groupList = [];
17454
+ this.#headerEls = /* @__PURE__ */ new Map();
17455
+ this.#seq = /* @__PURE__ */ new WeakMap();
17456
+ this.#seqNext = 0;
17457
+ this.#wasGrouped = false;
17458
+ this.#groupsScheduled = false;
17459
+ this.#groupByChanged = false;
16621
17460
  this.#providerBuilt = 0;
16622
17461
  this.#data = new DataController(this);
16623
17462
  this.selection = new ListSelectionController({
@@ -16628,6 +17467,12 @@ let DetailListElement = class DetailListElement extends LitElement {
16628
17467
  interactionMode: this.interactionMode
16629
17468
  }),
16630
17469
  getKey: keyForElement,
17470
+ getGroupKey: (el) => this.#groupIndex.get(el) ?? null,
17471
+ crossGroupSelection: () => this.crossGroupSelection,
17472
+ getFallbackGroupKey: () => {
17473
+ const t = this.tickled.tickledId;
17474
+ return t ? this.#groupIndex.get(t) ?? null : null;
17475
+ },
16631
17476
  onChange: (selected, previous) => {
16632
17477
  this._syncItemStates();
16633
17478
  this._dispatch(`list-select`, {
@@ -16709,6 +17554,7 @@ let DetailListElement = class DetailListElement extends LitElement {
16709
17554
  if (this._listEl) this._listEl.scrollLeft += dx;
16710
17555
  },
16711
17556
  shouldStart: (event) => {
17557
+ if (this.#findGroupHeader(event.composedPath())) return false;
16712
17558
  this.#blankItemPressPending = this.itemsDraggable && this.#isBlankItemPress(event);
16713
17559
  return !this.itemsDraggable || this.#blankItemPressPending;
16714
17560
  }
@@ -16732,6 +17578,9 @@ let DetailListElement = class DetailListElement extends LitElement {
16732
17578
  this._onPointerLeave = (e) => {
16733
17579
  this.tickled.handlePointerLeave(e);
16734
17580
  };
17581
+ this._endKeyboardNav = () => {
17582
+ this.removeAttribute(`data-keyboard-nav`);
17583
+ };
16735
17584
  this._onKeyDown = (e) => {
16736
17585
  if (this.search.isOpen) return;
16737
17586
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
@@ -16741,6 +17590,7 @@ let DetailListElement = class DetailListElement extends LitElement {
16741
17590
  }
16742
17591
  return;
16743
17592
  }
17593
+ if (NAV_KEYS.has(e.key)) this.setAttribute(`data-keyboard-nav`, ``);
16744
17594
  switch (e.key) {
16745
17595
  case `ArrowDown`:
16746
17596
  e.preventDefault();
@@ -16796,7 +17646,13 @@ let DetailListElement = class DetailListElement extends LitElement {
16796
17646
  };
16797
17647
  this._onClick = (e) => {
16798
17648
  if (this.#dragSelect.isDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
16799
- const item = this._findListItem(e.composedPath());
17649
+ const path = e.composedPath();
17650
+ const header = this.#findGroupHeader(path);
17651
+ if (header) {
17652
+ this.toggleGroup(header.dataset.groupKey ?? ``);
17653
+ return;
17654
+ }
17655
+ const item = this._findListItem(path);
16800
17656
  if (!item) return;
16801
17657
  this.focus();
16802
17658
  this._dispatch(`list-item-click`, { item });
@@ -16815,6 +17671,8 @@ let DetailListElement = class DetailListElement extends LitElement {
16815
17671
  #itemData;
16816
17672
  #grid;
16817
17673
  #positions;
17674
+ /** Viewport-relative `top` per item at the last geometry pass — lines rows up across columns. */
17675
+ #tops;
16818
17676
  #geometryScheduled;
16819
17677
  #sortRaf;
16820
17678
  #viewportScrollRaf;
@@ -16826,6 +17684,17 @@ let DetailListElement = class DetailListElement extends LitElement {
16826
17684
  /** Items that have already played (or been skipped for) their enter animation. */
16827
17685
  #animatedItems;
16828
17686
  #builtFromProvider;
17687
+ /** Element → group key, for the selection controller. Empty while ungrouped. */
17688
+ #groupIndex;
17689
+ #groupList;
17690
+ #headerEls;
17691
+ /** Insertion sequence per item, so regrouping starts from the original order. */
17692
+ #seq;
17693
+ #seqNext;
17694
+ #wasGrouped;
17695
+ #groupsScheduled;
17696
+ #groupByChanged;
17697
+ #groupObserver;
16829
17698
  #providerBuilt;
16830
17699
  #data;
16831
17700
  #nativeDragging;
@@ -16929,6 +17798,8 @@ let DetailListElement = class DetailListElement extends LitElement {
16929
17798
  this.addEventListener(`pointerover`, this._onPointerOver);
16930
17799
  this.addEventListener(`pointerleave`, this._onPointerLeave);
16931
17800
  this.addEventListener(`keydown`, this._onKeyDown);
17801
+ this.addEventListener(`pointermove`, this._endKeyboardNav);
17802
+ this.addEventListener(`pointerdown`, this._endKeyboardNav);
16932
17803
  this.addEventListener(`click`, this._onClick);
16933
17804
  this.addEventListener(`dblclick`, this._onDblClick);
16934
17805
  this.addEventListener(`wheel`, this._onWheel);
@@ -16940,6 +17811,7 @@ let DetailListElement = class DetailListElement extends LitElement {
16940
17811
  this.#processChildren();
16941
17812
  this.#animateChildMutations(records);
16942
17813
  this.#scheduleGeometry();
17814
+ this.#scheduleGroups();
16943
17815
  });
16944
17816
  this.#mutationObserver.observe(this, {
16945
17817
  childList: true,
@@ -16949,6 +17821,7 @@ let DetailListElement = class DetailListElement extends LitElement {
16949
17821
  for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#animatedItems.add(el);
16950
17822
  this.#searchRegistry ??= createCommandRegistry();
16951
17823
  this.#syncSearchCommands();
17824
+ this.#scheduleGroups();
16952
17825
  }
16953
17826
  /**
16954
17827
  * Collapse-in items appended to the light DOM (however they got there). A bulk
@@ -16975,6 +17848,8 @@ let DetailListElement = class DetailListElement extends LitElement {
16975
17848
  this.removeEventListener(`pointerover`, this._onPointerOver);
16976
17849
  this.removeEventListener(`pointerleave`, this._onPointerLeave);
16977
17850
  this.removeEventListener(`keydown`, this._onKeyDown);
17851
+ this.removeEventListener(`pointermove`, this._endKeyboardNav);
17852
+ this.removeEventListener(`pointerdown`, this._endKeyboardNav);
16978
17853
  this.removeEventListener(`click`, this._onClick);
16979
17854
  this.removeEventListener(`dblclick`, this._onDblClick);
16980
17855
  this.removeEventListener(`wheel`, this._onWheel);
@@ -16988,6 +17863,8 @@ let DetailListElement = class DetailListElement extends LitElement {
16988
17863
  this.#resizeObserver = void 0;
16989
17864
  this.#mutationObserver?.disconnect();
16990
17865
  this.#mutationObserver = void 0;
17866
+ this.#groupObserver?.disconnect();
17867
+ this.#groupObserver = void 0;
16991
17868
  this.#unregisterSearchCommands?.();
16992
17869
  this.#unregisterSearchCommands = void 0;
16993
17870
  this.#searchRegistry = void 0;
@@ -17002,8 +17879,22 @@ let DetailListElement = class DetailListElement extends LitElement {
17002
17879
  this.#recomputeGeometry();
17003
17880
  this._syncNativeDraggable();
17004
17881
  }
17882
+ willUpdate(changed) {
17883
+ super.willUpdate(changed);
17884
+ if (changed.has(`groupBy`) && this.hasUpdated) {
17885
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
17886
+ this.#groupByChanged = true;
17887
+ }
17888
+ }
17005
17889
  updated(changed) {
17006
17890
  super.updated(changed);
17891
+ if (changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`)) {
17892
+ this.#scheduleGroups();
17893
+ if (this.#groupByChanged) {
17894
+ this.#groupByChanged = false;
17895
+ this._dispatch(`list-group-change`, { groupBy: this.groupBy });
17896
+ }
17897
+ }
17007
17898
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
17008
17899
  if (changed.has(`provider`)) {
17009
17900
  this.#builtFromProvider = [];
@@ -17020,6 +17911,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17020
17911
  if (changed.has(`filterPredicate`)) {
17021
17912
  this.#applyFilterPredicate();
17022
17913
  this.#scheduleGeometry();
17914
+ this.#scheduleGroups();
17023
17915
  }
17024
17916
  if (changed.has(`interactionMode`)) {
17025
17917
  this.selection.clearAnchor();
@@ -17059,6 +17951,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17059
17951
  this.#ensureIcon(el);
17060
17952
  if (this.sort !== `none`) this.#applySort();
17061
17953
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17954
+ this.#scheduleGroups();
17062
17955
  }
17063
17956
  /**
17064
17957
  * Collapse + fade `item` out, then detach it; the rows after it slide up to
@@ -17068,6 +17961,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17068
17961
  this.selection.deselect(item);
17069
17962
  return animateListItemOut(item).then(() => {
17070
17963
  item.remove();
17964
+ this.#scheduleGroups();
17071
17965
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17072
17966
  this.#recomputeGeometry();
17073
17967
  });
@@ -17078,6 +17972,8 @@ let DetailListElement = class DetailListElement extends LitElement {
17078
17972
  }
17079
17973
  clearItems() {
17080
17974
  this.replaceChildren();
17975
+ this.#headerEls.clear();
17976
+ this.#scheduleGroups();
17081
17977
  this.selection.reset();
17082
17978
  this.#builtFromProvider = [];
17083
17979
  this.#providerBuilt = 0;
@@ -17095,6 +17991,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17095
17991
  }
17096
17992
  if (this.sort !== `none`) this.#applySort();
17097
17993
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17994
+ this.#scheduleGroups();
17098
17995
  }
17099
17996
  /**
17100
17997
  * Get the source object behind an `<li>` built from an `items` array or
@@ -17214,6 +18111,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17214
18111
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17215
18112
  this._syncItemStates();
17216
18113
  this.#scheduleGeometry();
18114
+ this.#scheduleGroups();
17217
18115
  }
17218
18116
  #createItemEl(item, index) {
17219
18117
  const li = document.createElement(`li`);
@@ -17265,6 +18163,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17265
18163
  if (this.interactionMode === `checked`) this._syncCheckboxes();
17266
18164
  this._syncItemStates();
17267
18165
  this.#scheduleGeometry();
18166
+ this.#scheduleGroups();
17268
18167
  } else {
17269
18168
  this.#providerBuilt = 0;
17270
18169
  this.#rebuildFromObjects(items);
@@ -17283,6 +18182,16 @@ let DetailListElement = class DetailListElement extends LitElement {
17283
18182
  if (this.sort === `none`) return;
17284
18183
  const rows = [...this.querySelectorAll(ITEM_SELECTOR$3)];
17285
18184
  if (rows.length < 2) return;
18185
+ if (this.#groupingActive()) {
18186
+ this.#scheduleGroups();
18187
+ return;
18188
+ }
18189
+ for (const el of this.#sortRows(rows)) this.appendChild(el);
18190
+ this.#scheduleGeometry();
18191
+ this.#scheduleGroups();
18192
+ }
18193
+ /** Stable sort of `rows` by the current `sort` / `sortBy` / `sortComparator`. */
18194
+ #sortRows(rows) {
17286
18195
  const dir = this.sort === `descending` ? -1 : 1;
17287
18196
  const meta = (el) => ({
17288
18197
  element: el,
@@ -17312,9 +18221,147 @@ let DetailListElement = class DetailListElement extends LitElement {
17312
18221
  i
17313
18222
  }));
17314
18223
  indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
17315
- for (const { el } of indexed) this.appendChild(el);
18224
+ return indexed.map(({ el }) => el);
18225
+ }
18226
+ #groupingActive() {
18227
+ return this.groupBy !== null && this.groupBy !== void 0 && this.groupBy !== ``;
18228
+ }
18229
+ /** Coalesce regrouping to one pass per microtask (streaming batches, search keystrokes). */
18230
+ #scheduleGroups() {
18231
+ if (this.#groupsScheduled || !this.#groupingActive() && !this.#wasGrouped) return;
18232
+ this.#groupsScheduled = true;
18233
+ queueMicrotask(() => {
18234
+ this.#groupsScheduled = false;
18235
+ this.#syncGroups();
18236
+ });
18237
+ }
18238
+ #groupValue(el, name) {
18239
+ return this.#propertyValue(this.getItemData(el), name) ?? el.getAttribute(`data-${name}`);
18240
+ }
18241
+ #groupKeyOf(el) {
18242
+ const by = this.groupBy;
18243
+ if (typeof by === `string`) return toGroupKey(this.#groupValue(el, by));
18244
+ return toGroupKey(by(this.getItemData(el), el));
18245
+ }
18246
+ #isItemVisible(el) {
18247
+ return !el.hidden && (!this.filterPredicate || this.filterPredicate(el));
18248
+ }
18249
+ #headerFor(key) {
18250
+ let header = this.#headerEls.get(key);
18251
+ if (!header) {
18252
+ header = document.createElement(`ixfx-list-group-header`);
18253
+ header.setAttribute(GROUP_HEADER_ATTR, ``);
18254
+ header.dataset.groupKey = key;
18255
+ this.#headerEls.set(key, header);
18256
+ }
18257
+ return header;
18258
+ }
18259
+ /**
18260
+ * Bring the light DOM into grouped order: original order → sort → partition
18261
+ * into contiguous groups, each preceded by a header row. Idempotent — it only
18262
+ * touches the DOM when the order actually changes, so it never loops through
18263
+ * the mutation observer. Ungrouping restores the original (sorted) order.
18264
+ */
18265
+ #syncGroups() {
18266
+ const active = this.#groupingActive();
18267
+ if (!active && !this.#wasGrouped) return;
18268
+ const items = [...this.querySelectorAll(ITEM_SELECTOR$3)];
18269
+ for (const el of items) if (!this.#seq.has(el)) this.#seq.set(el, this.#seqNext++);
18270
+ let ordered = items.toSorted((a, b) => this.#seq.get(a) - this.#seq.get(b));
18271
+ if (this.sort !== `none` && ordered.length > 1) ordered = this.#sortRows(ordered);
18272
+ const nodes = [];
18273
+ const info = [];
18274
+ const index = /* @__PURE__ */ new Map();
18275
+ const liveHeaders = /* @__PURE__ */ new Set();
18276
+ if (active) {
18277
+ const label = this.groupLabel;
18278
+ const parts = partitionByGroup(ordered, (el) => this.#groupKeyOf(el), label ? (key, els) => label(key, els.map((el) => this.getItemData(el) ?? el)) : void 0);
18279
+ for (const part of parts) {
18280
+ const count = part.items.filter((el) => this.#isItemVisible(el)).length;
18281
+ const collapsed = count > 0 && this._collapsedGroups.has(part.key);
18282
+ if (count > 0) {
18283
+ const header = this.#headerFor(part.key);
18284
+ header.label = part.label;
18285
+ header.count = count;
18286
+ header.collapsed = collapsed;
18287
+ liveHeaders.add(part.key);
18288
+ nodes.push(header);
18289
+ info.push({
18290
+ key: part.key,
18291
+ label: part.label,
18292
+ count,
18293
+ collapsed: this._collapsedGroups.has(part.key)
18294
+ });
18295
+ }
18296
+ for (const el of part.items) {
18297
+ index.set(el, part.key);
18298
+ el.toggleAttribute(GROUP_COLLAPSED_ATTR, collapsed);
18299
+ nodes.push(el);
18300
+ }
18301
+ }
18302
+ } else {
18303
+ for (const el of ordered) el.removeAttribute(GROUP_COLLAPSED_ATTR);
18304
+ nodes.push(...ordered);
18305
+ }
18306
+ for (const [key, header] of this.#headerEls) if (!liveHeaders.has(key)) {
18307
+ header.remove();
18308
+ this.#headerEls.delete(key);
18309
+ }
18310
+ this.#groupIndex = index;
18311
+ this.#groupList = info;
18312
+ this.#wasGrouped = active;
18313
+ const current = [...this.children].filter((c) => c.matches(`li`) || c.hasAttribute(GROUP_HEADER_ATTR));
18314
+ if (current.length !== nodes.length || current.some((c, i) => c !== nodes[i])) this.append(...nodes);
18315
+ if (this.tickled.tickledId?.hasAttribute(GROUP_COLLAPSED_ATTR)) this.tickled.focus(void 0);
18316
+ this.#syncGroupObserver(active);
17316
18317
  this.#scheduleGeometry();
17317
18318
  }
18319
+ /** Filter-search hides rows via `hidden` on the `<li>`s — regroup when that changes. */
18320
+ #syncGroupObserver(active) {
18321
+ if (!active) {
18322
+ this.#groupObserver?.disconnect();
18323
+ this.#groupObserver = void 0;
18324
+ return;
18325
+ }
18326
+ if (this.#groupObserver || !this.isConnected) return;
18327
+ this.#groupObserver = new MutationObserver(() => this.#scheduleGroups());
18328
+ this.#groupObserver.observe(this, {
18329
+ subtree: true,
18330
+ attributes: true,
18331
+ attributeFilter: [`hidden`]
18332
+ });
18333
+ }
18334
+ /** Groups in display order (empty when ungrouped). */
18335
+ get groups() {
18336
+ return this.#groupList;
18337
+ }
18338
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
18339
+ setGroupCollapsed(groupKey, collapsed) {
18340
+ if (this._collapsedGroups.has(groupKey) === collapsed) return;
18341
+ const next = new Set(this._collapsedGroups);
18342
+ if (collapsed) next.add(groupKey);
18343
+ else next.delete(groupKey);
18344
+ this._collapsedGroups = next;
18345
+ this._dispatch(`list-group-toggle`, {
18346
+ groupKey,
18347
+ collapsed
18348
+ });
18349
+ }
18350
+ toggleGroup(groupKey) {
18351
+ this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
18352
+ }
18353
+ collapseAllGroups() {
18354
+ for (const g of this.#groupList) this.setGroupCollapsed(g.key, true);
18355
+ }
18356
+ expandAllGroups() {
18357
+ for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
18358
+ }
18359
+ #findGroupHeader(path) {
18360
+ for (const target of path) {
18361
+ if (target === this) break;
18362
+ if (target instanceof HTMLElement && target.parentElement === this && target.hasAttribute(GROUP_HEADER_ATTR)) return target;
18363
+ }
18364
+ }
17318
18365
  #scheduleGeometry() {
17319
18366
  if (this.#geometryScheduled) return;
17320
18367
  this.#geometryScheduled = true;
@@ -17326,11 +18373,13 @@ let DetailListElement = class DetailListElement extends LitElement {
17326
18373
  #recomputeGeometry() {
17327
18374
  this.#grid = [];
17328
18375
  this.#positions.clear();
18376
+ this.#tops.clear();
17329
18377
  if (!this.isConnected) return;
17330
18378
  const columns = [];
17331
18379
  for (const el of this._getVisibleItems()) {
17332
18380
  const rect = el.getBoundingClientRect();
17333
18381
  if (rect.width === 0 && rect.height === 0) continue;
18382
+ this.#tops.set(el, rect.top);
17334
18383
  let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
17335
18384
  if (ci === -1) {
17336
18385
  columns.push({
@@ -17451,6 +18500,30 @@ let DetailListElement = class DetailListElement extends LitElement {
17451
18500
  const next = this.#findNextCheckable(cur.col, cur.row, 0, -1);
17452
18501
  if (next) this.#navigateTo(next);
17453
18502
  }
18503
+ /**
18504
+ * Left/Right target. Ungrouped lists keep the row-index rule. Grouped lists
18505
+ * have headers at arbitrary heights, so an item's row index no longer equals
18506
+ * its visual row — pick the item in the neighbouring column whose top is
18507
+ * nearest the cursor's (ties go to the upper one).
18508
+ */
18509
+ #horizontalTarget(from, dir) {
18510
+ if (this.#groupList.length === 0) return this.#findNextCheckable(from.col, from.row, dir, 0);
18511
+ const column = this.#grid[from.col + dir];
18512
+ const current = this.tickled.tickledId;
18513
+ const top = current ? this.#tops.get(current) : void 0;
18514
+ if (!column || top === void 0) return void 0;
18515
+ let best;
18516
+ let bestDelta = Number.POSITIVE_INFINITY;
18517
+ for (const el of column) {
18518
+ if (!this._isCheckable(el)) continue;
18519
+ const delta = Math.abs((this.#tops.get(el) ?? Number.POSITIVE_INFINITY) - top);
18520
+ if (delta < bestDelta) {
18521
+ best = el;
18522
+ bestDelta = delta;
18523
+ }
18524
+ }
18525
+ return best;
18526
+ }
17454
18527
  #navigateRight() {
17455
18528
  this.#recomputeGeometry();
17456
18529
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
@@ -17458,7 +18531,7 @@ let DetailListElement = class DetailListElement extends LitElement {
17458
18531
  this.#navigateTo(this.#findFirstCheckable());
17459
18532
  return;
17460
18533
  }
17461
- const next = this.#findNextCheckable(cur.col, cur.row, 1, 0);
18534
+ const next = this.#horizontalTarget(cur, 1);
17462
18535
  if (next) this.#navigateTo(next);
17463
18536
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
17464
18537
  }
@@ -17469,13 +18542,14 @@ let DetailListElement = class DetailListElement extends LitElement {
17469
18542
  this.#navigateTo(this.#findFirstCheckable());
17470
18543
  return;
17471
18544
  }
17472
- const next = this.#findNextCheckable(cur.col, cur.row, -1, 0);
18545
+ const next = this.#horizontalTarget(cur, -1);
17473
18546
  if (next) this.#navigateTo(next);
17474
18547
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
17475
18548
  }
17476
18549
  _getVisibleItems() {
17477
18550
  return Array.from(this.querySelectorAll(ITEM_SELECTOR$3)).filter((el) => {
17478
18551
  if (el.hidden) return false;
18552
+ if (el.hasAttribute(GROUP_COLLAPSED_ATTR)) return false;
17479
18553
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
17480
18554
  return true;
17481
18555
  });
@@ -17764,6 +18838,20 @@ let DetailListElement = class DetailListElement extends LitElement {
17764
18838
  display: none !important;
17765
18839
  }
17766
18840
 
18841
+ /* Rows of a collapsed group leave the flow; their header stays. */
18842
+ ::slotted([data-group-collapsed]) {
18843
+ display: none !important;
18844
+ }
18845
+
18846
+ /* Group headers are rows in the column flow, the same size as an item. */
18847
+ ::slotted(ixfx-list-group-header) {
18848
+ box-sizing: border-box;
18849
+ flex: none;
18850
+ width: var(--detail-list-column-width, 220px);
18851
+ height: var(--detail-list-item-height, 28px);
18852
+ --list-group-header-height: var(--detail-list-group-header-height, var(--detail-list-item-height, 28px));
18853
+ }
18854
+
17767
18855
  ::slotted(li) {
17768
18856
  box-sizing: border-box;
17769
18857
  display: flex;
@@ -17779,6 +18867,12 @@ let DetailListElement = class DetailListElement extends LitElement {
17779
18867
  outline: 1px solid transparent;
17780
18868
  }
17781
18869
 
18870
+ /* While navigating by keyboard, key-repeat outpaces the 0.1s fade so the
18871
+ cursor smears across rows. Snap instead. */
18872
+ :host([data-keyboard-nav]) ::slotted(li) {
18873
+ transition: none;
18874
+ }
18875
+
17782
18876
  /* In checked mode, only the checkbox column is clickable; the row body
17783
18877
  gets a default cursor so it doesn't suggest otherwise. */
17784
18878
  :host([interaction-mode="checked"]) ::slotted(li) {
@@ -17934,6 +19028,14 @@ __decorate([property({
17934
19028
  })], DetailListElement.prototype, "itemsDraggable", void 0);
17935
19029
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewRenderer", void 0);
17936
19030
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewSource", void 0);
19031
+ __decorate([property({ attribute: `group-by` })], DetailListElement.prototype, "groupBy", void 0);
19032
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "groupLabel", void 0);
19033
+ __decorate([property({
19034
+ type: Boolean,
19035
+ attribute: `cross-group-selection`,
19036
+ reflect: true
19037
+ })], DetailListElement.prototype, "crossGroupSelection", void 0);
19038
+ __decorate([state()], DetailListElement.prototype, "_collapsedGroups", void 0);
17937
19039
  __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
17938
19040
  DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
17939
19041
  //#endregion
@@ -23573,7 +24675,374 @@ let FormElement = class FormElement extends LitElement {
23573
24675
  };
23574
24676
  FormElement = __decorate([safeCustomElement(`ixfx-form`)], FormElement);
23575
24677
  //#endregion
24678
+ //#region src/grid-list/grid-list-layout.ts
24679
+ /**
24680
+ * `itemWidth` is the square's width; `rowHeight` is the full cell height
24681
+ * (square, plus the caption band in `separate` / `under` styles).
24682
+ * `scrollOffset` is `scrollTop` for `grid`/`column` and `scrollLeft` for `row`.
24683
+ */
24684
+ function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
24685
+ const w = Math.max(1, itemWidth);
24686
+ const h = Math.max(1, rowHeight);
24687
+ const g = Math.max(0, gap);
24688
+ const colStride = w + g;
24689
+ const rowStride = h + g;
24690
+ const offset = Math.max(0, scrollOffset);
24691
+ if (orientation === `row`) {
24692
+ const rows = count === 0 ? 0 : 1;
24693
+ const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
24694
+ if (count === 0 || viewportWidth <= 0) return {
24695
+ cols: count,
24696
+ rows,
24697
+ totalHeight: 0,
24698
+ totalWidth,
24699
+ startIndex: 0,
24700
+ endIndex: 0,
24701
+ colStride,
24702
+ rowStride,
24703
+ horizontal: true
24704
+ };
24705
+ const firstVisible = Math.max(0, Math.floor(offset / colStride));
24706
+ const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
24707
+ return {
24708
+ cols: count,
24709
+ rows,
24710
+ totalHeight: 0,
24711
+ totalWidth,
24712
+ startIndex: Math.max(0, firstVisible - overscan),
24713
+ endIndex: Math.min(count, lastVisible + overscan),
24714
+ colStride,
24715
+ rowStride,
24716
+ horizontal: true
24717
+ };
24718
+ }
24719
+ const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
24720
+ const rows = count === 0 ? 0 : Math.ceil(count / cols);
24721
+ const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
24722
+ if (count === 0 || viewportHeight <= 0) return {
24723
+ cols,
24724
+ rows,
24725
+ totalHeight,
24726
+ totalWidth: 0,
24727
+ startIndex: 0,
24728
+ endIndex: 0,
24729
+ colStride,
24730
+ rowStride,
24731
+ horizontal: false
24732
+ };
24733
+ const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
24734
+ const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
24735
+ const rowStart = Math.max(0, firstVisibleRow - overscan);
24736
+ const rowEnd = Math.min(rows, lastVisibleRow + overscan);
24737
+ return {
24738
+ cols,
24739
+ rows,
24740
+ totalHeight,
24741
+ totalWidth: 0,
24742
+ startIndex: rowStart * cols,
24743
+ endIndex: Math.min(count, rowEnd * cols),
24744
+ colStride,
24745
+ rowStride,
24746
+ horizontal: false
24747
+ };
24748
+ }
24749
+ function buildSections(p, cols, stride, itemExtent) {
24750
+ const g = Math.max(0, p.gap);
24751
+ const horizontal = p.orientation === `row`;
24752
+ const lineCount = (n) => horizontal ? n : Math.ceil(n / cols);
24753
+ const extentFor = (n) => n === 0 ? 0 : (lineCount(n) - 1) * stride + itemExtent;
24754
+ if (!p.groups) {
24755
+ const end = extentFor(p.count);
24756
+ return {
24757
+ sections: [{
24758
+ group: void 0,
24759
+ first: 0,
24760
+ count: p.count,
24761
+ headerOffset: 0,
24762
+ itemsOffset: 0,
24763
+ end
24764
+ }],
24765
+ total: end,
24766
+ count: p.count
24767
+ };
24768
+ }
24769
+ const headerSize = horizontal ? p.headerWidth : p.headerHeight;
24770
+ const sections = [];
24771
+ let cursor = 0;
24772
+ let first = 0;
24773
+ for (const group of p.groups) {
24774
+ const count = group.collapsed ? 0 : group.count;
24775
+ const itemsOffset = cursor + headerSize + g;
24776
+ const end = count === 0 ? cursor + headerSize : itemsOffset + extentFor(count);
24777
+ sections.push({
24778
+ group,
24779
+ first,
24780
+ count,
24781
+ headerOffset: cursor,
24782
+ itemsOffset,
24783
+ end
24784
+ });
24785
+ first += count;
24786
+ cursor = end + g;
24787
+ }
24788
+ return {
24789
+ sections,
24790
+ total: sections.length === 0 ? 0 : sections[sections.length - 1].end,
24791
+ count: first
24792
+ };
24793
+ }
24794
+ /** Index of the last section whose `headerOffset` is <= `offset` (0 when before all). */
24795
+ function sectionAtOffset(sections, offset) {
24796
+ let lo = 0;
24797
+ let hi = sections.length - 1;
24798
+ while (lo < hi) {
24799
+ const mid = lo + hi + 1 >> 1;
24800
+ if (sections[mid].headerOffset <= offset) lo = mid;
24801
+ else hi = mid - 1;
24802
+ }
24803
+ return lo;
24804
+ }
24805
+ /** Index of the section holding effective item `index` (skips empty sections). */
24806
+ function sectionOfIndex(sections, index) {
24807
+ let lo = 0;
24808
+ let hi = sections.length - 1;
24809
+ while (lo < hi) {
24810
+ const mid = lo + hi + 1 >> 1;
24811
+ if (sections[mid].first <= index) lo = mid;
24812
+ else hi = mid - 1;
24813
+ }
24814
+ let s = lo;
24815
+ while (s < sections.length - 1 && sections[s].count === 0) s++;
24816
+ return s;
24817
+ }
24818
+ function buildLayout(p) {
24819
+ const horizontal = p.orientation === `row`;
24820
+ const w = Math.max(1, p.itemWidth);
24821
+ const h = Math.max(1, p.rowHeight);
24822
+ const g = Math.max(0, p.gap);
24823
+ const colStride = w + g;
24824
+ const rowStride = h + g;
24825
+ const cols = horizontal ? 1 : p.orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, p.viewportWidth) + g) / colStride));
24826
+ const stride = horizontal ? colStride : rowStride;
24827
+ const itemExtent = horizontal ? w : h;
24828
+ const { sections, total, count } = buildSections(p, cols, stride, itemExtent);
24829
+ const grouped = p.groups !== void 0;
24830
+ const headerSize = horizontal ? p.headerWidth : p.headerHeight;
24831
+ const viewportMain = horizontal ? p.viewportWidth : p.viewportHeight;
24832
+ const cellHeight = rowStride - g;
24833
+ const rowY = Math.max(0, (p.viewportHeight - cellHeight) / 2);
24834
+ const positionOf = (index) => {
24835
+ const si = sectionOfIndex(sections, index);
24836
+ const s = sections[si];
24837
+ const local = Math.max(0, index - s.first);
24838
+ if (horizontal) return {
24839
+ x: s.itemsOffset + local * colStride,
24840
+ y: rowY,
24841
+ col: 0,
24842
+ line: local,
24843
+ section: si
24844
+ };
24845
+ const col = cols <= 1 ? 0 : local % cols;
24846
+ const line = cols <= 1 ? local : Math.floor(local / cols);
24847
+ return {
24848
+ x: col * colStride,
24849
+ y: s.itemsOffset + line * rowStride,
24850
+ col,
24851
+ line,
24852
+ section: si
24853
+ };
24854
+ };
24855
+ const window = (offset, overscan = 2) => {
24856
+ const off = Math.max(0, offset);
24857
+ const headers = [];
24858
+ const ranges = [];
24859
+ const lo = off - overscan * stride;
24860
+ const hi = off + viewportMain + overscan * stride;
24861
+ let firstSection = 0;
24862
+ if (sections.length > 1) {
24863
+ let a = 0;
24864
+ let b = sections.length - 1;
24865
+ while (a < b) {
24866
+ const mid = a + b >> 1;
24867
+ if (sections[mid].end >= lo) b = mid;
24868
+ else a = mid + 1;
24869
+ }
24870
+ firstSection = a;
24871
+ }
24872
+ if (viewportMain > 0) for (let si = firstSection; si < sections.length; si++) {
24873
+ const s = sections[si];
24874
+ if (s.headerOffset > hi) break;
24875
+ if (s.group && s.headerOffset + headerSize >= lo) headers.push({
24876
+ key: s.group.key,
24877
+ label: s.group.label,
24878
+ count: s.group.count,
24879
+ collapsed: s.group.collapsed,
24880
+ offset: s.headerOffset,
24881
+ size: headerSize
24882
+ });
24883
+ if (s.count === 0) continue;
24884
+ const rel = off - s.itemsOffset;
24885
+ const viewport = rel < 0 ? viewportMain + rel : viewportMain;
24886
+ if (viewport <= 0) continue;
24887
+ const sw = computeGridWindow(s.count, w, h, g, horizontal ? viewport : p.viewportWidth, horizontal ? p.viewportHeight : viewport, Math.max(0, rel), p.orientation, overscan);
24888
+ if (sw.endIndex > sw.startIndex) ranges.push([s.first + sw.startIndex, s.first + sw.endIndex]);
24889
+ }
24890
+ return {
24891
+ cols: horizontal ? count : cols,
24892
+ rows: 0,
24893
+ totalHeight: horizontal ? 0 : total,
24894
+ totalWidth: horizontal ? total : 0,
24895
+ startIndex: ranges.length > 0 ? ranges[0][0] : 0,
24896
+ endIndex: ranges.length > 0 ? ranges[ranges.length - 1][1] : 0,
24897
+ colStride,
24898
+ rowStride,
24899
+ horizontal,
24900
+ headers,
24901
+ ranges
24902
+ };
24903
+ };
24904
+ const extentOf = (index) => {
24905
+ const { line, section } = positionOf(index);
24906
+ const s = sections[section];
24907
+ const start = s.itemsOffset + line * stride;
24908
+ return [line === 0 && s.group ? s.headerOffset : start, start + itemExtent];
24909
+ };
24910
+ const neighbour = (index, dLine) => {
24911
+ if (count === 0) return index;
24912
+ const si = sectionOfIndex(sections, index);
24913
+ const s = sections[si];
24914
+ const local = index - s.first;
24915
+ const col = local % cols;
24916
+ if (dLine > 0) {
24917
+ const t = local + cols;
24918
+ if (t <= s.count - 1) return s.first + t;
24919
+ const lastLine = Math.floor((s.count - 1) / cols);
24920
+ if (Math.floor(local / cols) < lastLine) return s.first + s.count - 1;
24921
+ 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);
24922
+ return s.first + s.count - 1;
24923
+ }
24924
+ const t = local - cols;
24925
+ if (t >= 0) return s.first + t;
24926
+ for (let n = si - 1; n >= 0; n--) {
24927
+ const prev = sections[n];
24928
+ if (prev.count > 0) {
24929
+ const line = Math.floor((prev.count - 1) / cols);
24930
+ return prev.first + Math.min(line * cols + col, prev.count - 1);
24931
+ }
24932
+ }
24933
+ return s.first;
24934
+ };
24935
+ const hitTest = (x0, x1, y0, y1) => {
24936
+ const out = [];
24937
+ if (count === 0) return out;
24938
+ const main0 = horizontal ? x0 : y0;
24939
+ const main1 = horizontal ? x1 : y1;
24940
+ const start = sectionAtOffset(sections, main0 - stride);
24941
+ for (let si = Math.max(0, start); si < sections.length; si++) {
24942
+ const s = sections[si];
24943
+ if (s.itemsOffset > main1) break;
24944
+ if (s.count === 0 || s.end < main0) continue;
24945
+ if (horizontal) {
24946
+ const a = Math.max(0, Math.floor((x0 - s.itemsOffset) / colStride));
24947
+ const b = Math.min(s.count - 1, Math.floor((x1 - s.itemsOffset) / colStride));
24948
+ for (let i = a; i <= b; i++) out.push(s.first + i);
24949
+ } else {
24950
+ const colLo = Math.max(0, Math.floor(x0 / colStride));
24951
+ const colHi = Math.min(cols - 1, Math.floor(x1 / colStride));
24952
+ const rowLo = Math.max(0, Math.floor((y0 - s.itemsOffset) / rowStride));
24953
+ const rowHi = Math.floor((y1 - s.itemsOffset) / rowStride);
24954
+ for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
24955
+ const local = row * cols + col;
24956
+ if (local >= 0 && local < s.count) out.push(s.first + local);
24957
+ }
24958
+ }
24959
+ }
24960
+ return out;
24961
+ };
24962
+ const anchorAt = (mainOffset) => {
24963
+ if (count === 0) return {
24964
+ index: 0,
24965
+ lineFrac: 0
24966
+ };
24967
+ let si = sectionAtOffset(sections, mainOffset);
24968
+ while (si < sections.length - 1 && sections[si].count === 0) si++;
24969
+ const s = sections[si];
24970
+ if (s.count === 0) return {
24971
+ index: 0,
24972
+ lineFrac: 0
24973
+ };
24974
+ const centreLine = Math.max(0, mainOffset - s.itemsOffset) / stride;
24975
+ const lineFloor = Math.max(0, Math.floor(centreLine));
24976
+ const local = horizontal ? Math.min(s.count - 1, lineFloor) : Math.min(s.count - 1, lineFloor * cols + Math.max(0, Math.floor((cols - 1) / 2)));
24977
+ return {
24978
+ index: s.first + local,
24979
+ lineFrac: centreLine - lineFloor
24980
+ };
24981
+ };
24982
+ const offsetOfAnchor = (index, lineFrac) => {
24983
+ const { line, section } = positionOf(index);
24984
+ return sections[section].itemsOffset + (line + lineFrac) * stride;
24985
+ };
24986
+ const snapTarget = (offset) => {
24987
+ const si = sectionAtOffset(sections, offset);
24988
+ const s = sections[si];
24989
+ let rel = Math.round((offset - s.itemsOffset) / stride);
24990
+ if (grouped) {
24991
+ const lines = s.count === 0 ? 0 : horizontal ? s.count : Math.ceil(s.count / cols);
24992
+ rel = Math.max(0, Math.min(Math.max(0, lines - 1), rel));
24993
+ }
24994
+ const candidates = [s.itemsOffset + rel * stride];
24995
+ if (grouped) {
24996
+ candidates.push(s.headerOffset);
24997
+ if (si + 1 < sections.length) candidates.push(sections[si + 1].headerOffset);
24998
+ }
24999
+ return candidates.reduce((best, c) => Math.abs(c - offset) < Math.abs(best - offset) ? c : best);
25000
+ };
25001
+ return {
25002
+ horizontal,
25003
+ cols,
25004
+ colStride,
25005
+ rowStride,
25006
+ grouped,
25007
+ count,
25008
+ totalExtent: total,
25009
+ window,
25010
+ positionOf,
25011
+ extentOf,
25012
+ neighbour,
25013
+ hitTest,
25014
+ anchorAt,
25015
+ offsetOfAnchor,
25016
+ snapTarget
25017
+ };
25018
+ }
25019
+ //#endregion
23576
25020
  //#region src/grid-list/grid-list-source.ts
25021
+ /** Reorder `effective` raw indices into contiguous groups, dropping collapsed ones. */
25022
+ function applyGrouping(effective, cfg, keyOfRaw) {
25023
+ const parts = partitionByGroup(effective, (raw) => cfg.groupKeyOf(raw), cfg.labelOf ? (key, raws) => cfg.labelOf(key, raws.map((r) => cfg.itemAt(r))) : void 0);
25024
+ const order = [];
25025
+ const groups = [];
25026
+ const groupByKey = /* @__PURE__ */ new Map();
25027
+ for (const part of parts) {
25028
+ const collapsed = cfg.collapsed.has(part.key);
25029
+ groups.push({
25030
+ key: part.key,
25031
+ label: part.label,
25032
+ count: part.items.length,
25033
+ collapsed
25034
+ });
25035
+ for (const raw of part.items) {
25036
+ groupByKey.set(keyOfRaw(raw), part.key);
25037
+ if (!collapsed) order.push(raw);
25038
+ }
25039
+ }
25040
+ return {
25041
+ order,
25042
+ groups,
25043
+ groupByKey
25044
+ };
25045
+ }
23577
25046
  const ITEM_SELECTOR$2 = `:scope > *`;
23578
25047
  const SYNTHETIC_PREFIX = `gl-auto-`;
23579
25048
  /** Whether `key` is an auto-assigned key for a keyless light-DOM item. */
@@ -23600,6 +25069,9 @@ var ElementGridListSource = class {
23600
25069
  #searchKeys;
23601
25070
  #effectiveIndices = [];
23602
25071
  #keysCache;
25072
+ #grouping;
25073
+ #groups = [];
25074
+ #groupByKey = /* @__PURE__ */ new Map();
23603
25075
  constructor(host) {
23604
25076
  this.#host = host;
23605
25077
  this.refresh();
@@ -23652,6 +25124,16 @@ var ElementGridListSource = class {
23652
25124
  if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
23653
25125
  this.#recomputeEffective();
23654
25126
  }
25127
+ setGrouping(config) {
25128
+ this.#grouping = config;
25129
+ this.#recomputeEffective();
25130
+ }
25131
+ groups() {
25132
+ return this.#groups;
25133
+ }
25134
+ groupKeyOfKey(key) {
25135
+ return this.#groupByKey.get(key) ?? null;
25136
+ }
23655
25137
  refresh() {
23656
25138
  this.#items = Array.from(this.#host.querySelectorAll(ITEM_SELECTOR$2)).filter((el) => !el.hidden);
23657
25139
  for (const el of this.#items) {
@@ -23694,9 +25176,21 @@ var ElementGridListSource = class {
23694
25176
  }
23695
25177
  effective.push(i);
23696
25178
  }
23697
- this.#effectiveIndices = effective;
25179
+ this.#applyGroupingTo(effective);
23698
25180
  this.#keysCache = void 0;
23699
25181
  }
25182
+ #applyGroupingTo(effective) {
25183
+ if (this.#grouping === void 0) {
25184
+ this.#effectiveIndices = effective;
25185
+ this.#groups = [];
25186
+ this.#groupByKey = /* @__PURE__ */ new Map();
25187
+ return;
25188
+ }
25189
+ const grouped = applyGrouping(effective, this.#grouping, (raw) => this.#itemKey(this.#items[raw]));
25190
+ this.#effectiveIndices = grouped.order;
25191
+ this.#groups = grouped.groups;
25192
+ this.#groupByKey = grouped.groupByKey;
25193
+ }
23700
25194
  #itemKey(el) {
23701
25195
  const userKey = keyForElement(el);
23702
25196
  if (userKey !== void 0) return userKey;
@@ -23719,6 +25213,9 @@ var DataGridListSource = class {
23719
25213
  #searchKeys;
23720
25214
  #effectiveIndices = [];
23721
25215
  #keysCache;
25216
+ #grouping;
25217
+ #groups = [];
25218
+ #groupByKey = /* @__PURE__ */ new Map();
23722
25219
  constructor(items, adapter) {
23723
25220
  this.#items = items;
23724
25221
  this.#adapter = adapter;
@@ -23784,6 +25281,16 @@ var DataGridListSource = class {
23784
25281
  if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
23785
25282
  this.#recomputeEffective();
23786
25283
  }
25284
+ setGrouping(config) {
25285
+ this.#grouping = config;
25286
+ this.#recomputeEffective();
25287
+ }
25288
+ groups() {
25289
+ return this.#groups;
25290
+ }
25291
+ groupKeyOfKey(key) {
25292
+ return this.#groupByKey.get(key) ?? null;
25293
+ }
23787
25294
  refresh() {
23788
25295
  this.#recomputeEffective();
23789
25296
  }
@@ -23808,7 +25315,16 @@ var DataGridListSource = class {
23808
25315
  if (this.#searchKeys !== void 0 && !this.#searchKeys.has(this.#adapter.key(this.#items[i], i))) continue;
23809
25316
  effective.push(i);
23810
25317
  }
23811
- this.#effectiveIndices = effective;
25318
+ if (this.#grouping === void 0) {
25319
+ this.#effectiveIndices = effective;
25320
+ this.#groups = [];
25321
+ this.#groupByKey = /* @__PURE__ */ new Map();
25322
+ } else {
25323
+ const grouped = applyGrouping(effective, this.#grouping, (raw) => this.#adapter.key(this.#items[raw], raw));
25324
+ this.#effectiveIndices = grouped.order;
25325
+ this.#groups = grouped.groups;
25326
+ this.#groupByKey = grouped.groupByKey;
25327
+ }
23812
25328
  this.#keysCache = void 0;
23813
25329
  }
23814
25330
  };
@@ -23824,76 +25340,11 @@ const SNAP_DEBOUNCE_MS = 120;
23824
25340
  * to finish. Kept a touch above the default `--grid-list-item-move-duration`.
23825
25341
  */
23826
25342
  const REFLOW_WINDOW_MS = 320;
23827
- function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
23828
- const w = Math.max(1, itemWidth);
23829
- const h = Math.max(1, rowHeight);
23830
- const g = Math.max(0, gap);
23831
- const colStride = w + g;
23832
- const rowStride = h + g;
23833
- const offset = Math.max(0, scrollOffset);
23834
- if (orientation === `row`) {
23835
- const rows = count === 0 ? 0 : 1;
23836
- const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
23837
- if (count === 0 || viewportWidth <= 0) return {
23838
- cols: count,
23839
- rows,
23840
- totalHeight: 0,
23841
- totalWidth,
23842
- startIndex: 0,
23843
- endIndex: 0,
23844
- colStride,
23845
- rowStride,
23846
- horizontal: true
23847
- };
23848
- const firstVisible = Math.max(0, Math.floor(offset / colStride));
23849
- const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
23850
- return {
23851
- cols: count,
23852
- rows,
23853
- totalHeight: 0,
23854
- totalWidth,
23855
- startIndex: Math.max(0, firstVisible - overscan),
23856
- endIndex: Math.min(count, lastVisible + overscan),
23857
- colStride,
23858
- rowStride,
23859
- horizontal: true
23860
- };
23861
- }
23862
- const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
23863
- const rows = count === 0 ? 0 : Math.ceil(count / cols);
23864
- const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
23865
- if (count === 0 || viewportHeight <= 0) return {
23866
- cols,
23867
- rows,
23868
- totalHeight,
23869
- totalWidth: 0,
23870
- startIndex: 0,
23871
- endIndex: 0,
23872
- colStride,
23873
- rowStride,
23874
- horizontal: false
23875
- };
23876
- const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
23877
- const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
23878
- const rowStart = Math.max(0, firstVisibleRow - overscan);
23879
- const rowEnd = Math.min(rows, lastVisibleRow + overscan);
23880
- return {
23881
- cols,
23882
- rows,
23883
- totalHeight,
23884
- totalWidth: 0,
23885
- startIndex: rowStart * cols,
23886
- endIndex: Math.min(count, rowEnd * cols),
23887
- colStride,
23888
- rowStride,
23889
- horizontal: false
23890
- };
25343
+ function readProperty(item, name) {
25344
+ return typeof item === `object` && item !== null ? item[name] : void 0;
23891
25345
  }
23892
- function columnOf(index, cols) {
23893
- return cols <= 1 ? 0 : index % cols;
23894
- }
23895
- function rowOf(index, cols) {
23896
- return cols <= 1 ? index : Math.floor(index / cols);
25346
+ function readAttribute(el, name) {
25347
+ return el.getAttribute(`data-${name}`);
23897
25348
  }
23898
25349
  /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
23899
25350
  function sameKeys$1(a, b) {
@@ -23921,6 +25372,11 @@ let GridListElement = class GridListElement extends LitElement {
23921
25372
  this.marqueeFromCellBlank = false;
23922
25373
  this.snapToItem = false;
23923
25374
  this.items = [];
25375
+ this.groupBy = null;
25376
+ this.crossGroupSelection = false;
25377
+ this.groupHeaderHeight = 28;
25378
+ this.groupHeaderWidth = 120;
25379
+ this._collapsedGroups = /* @__PURE__ */ new Set();
23924
25380
  this._scrollTop = 0;
23925
25381
  this._scrollLeft = 0;
23926
25382
  this._viewportHeight = 0;
@@ -23945,6 +25401,12 @@ let GridListElement = class GridListElement extends LitElement {
23945
25401
  interactionMode: this.interactionMode
23946
25402
  }),
23947
25403
  getKey: (k) => k,
25404
+ getGroupKey: (k) => this.groupBy === null ? null : this.#source.groupKeyOfKey(k),
25405
+ crossGroupSelection: () => this.crossGroupSelection,
25406
+ getFallbackGroupKey: () => {
25407
+ const k = this.tickled.tickledId;
25408
+ return k !== void 0 && this.groupBy !== null ? this.#source.groupKeyOfKey(k) : null;
25409
+ },
23948
25410
  onChange: (selected, previous) => {
23949
25411
  this._syncItemStates();
23950
25412
  this.requestUpdate();
@@ -24042,10 +25504,11 @@ let GridListElement = class GridListElement extends LitElement {
24042
25504
  },
24043
25505
  shouldStart: (event) => {
24044
25506
  if (this.marqueeFromCellBlank && this.#isCellBlankPress(event)) return true;
24045
- for (const node of event.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return false;
25507
+ for (const node of event.composedPath()) if (node instanceof Element && (node.classList.contains(`cell`) || node.classList.contains(`group-header`))) return false;
24046
25508
  return true;
24047
25509
  }
24048
25510
  });
25511
+ this.#groupByChanged = false;
24049
25512
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
24050
25513
  this.#onScroll = (e) => {
24051
25514
  const el = e.target;
@@ -24095,15 +25558,25 @@ let GridListElement = class GridListElement extends LitElement {
24095
25558
  const count = this.#source.count;
24096
25559
  if (count === 0) return;
24097
25560
  const win = this.#currentWindow();
24098
- const move = (delta) => {
24099
- const from = this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
24100
- const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
25561
+ const layout = this.#layout();
25562
+ const current = () => this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
25563
+ const goto = (nextIndex) => {
24101
25564
  const nextKey = this.#source.keyAt(nextIndex);
24102
25565
  if (nextKey) {
24103
25566
  this.tickled.handleKeyboardNavigation(nextKey);
24104
25567
  this.#scrollItemIntoView(nextKey);
24105
25568
  }
24106
25569
  };
25570
+ const move = (delta) => goto(Math.max(0, Math.min(count - 1, current() + delta)));
25571
+ const moveLines = (dir, lines = 1) => {
25572
+ let index = current();
25573
+ if (index < 0) {
25574
+ move(dir * lines * win.cols);
25575
+ return;
25576
+ }
25577
+ for (let i = 0; i < lines; i++) index = layout.neighbour(index, dir);
25578
+ goto(index);
25579
+ };
24107
25580
  const perLine = this.orientation === `grid` ? win.cols : 1;
24108
25581
  switch (e.key) {
24109
25582
  case `ArrowLeft`:
@@ -24113,10 +25586,12 @@ let GridListElement = class GridListElement extends LitElement {
24113
25586
  move(1);
24114
25587
  break;
24115
25588
  case `ArrowUp`:
24116
- move(-perLine);
25589
+ if (this.orientation === `grid`) moveLines(-1);
25590
+ else move(-perLine);
24117
25591
  break;
24118
25592
  case `ArrowDown`:
24119
- move(perLine);
25593
+ if (this.orientation === `grid`) moveLines(1);
25594
+ else move(perLine);
24120
25595
  break;
24121
25596
  case `Home`:
24122
25597
  this.tickled.handleKeyboardNavigation(`first`);
@@ -24131,7 +25606,8 @@ let GridListElement = class GridListElement extends LitElement {
24131
25606
  const mainExtent = win.horizontal ? this._viewportWidth : this._viewportHeight;
24132
25607
  const mainStride = win.horizontal ? win.colStride : win.rowStride;
24133
25608
  const visibleLines = Math.max(1, Math.floor(mainExtent / mainStride));
24134
- move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
25609
+ if (this.orientation === `grid`) moveLines(e.key === `PageDown` ? 1 : -1, visibleLines);
25610
+ else move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
24135
25611
  break;
24136
25612
  }
24137
25613
  case `Enter`: {
@@ -24212,6 +25688,11 @@ let GridListElement = class GridListElement extends LitElement {
24212
25688
  position: relative;
24213
25689
  width: 100%;
24214
25690
  }
25691
+ .group-header {
25692
+ position: absolute;
25693
+ inset: 0 auto auto 0;
25694
+ z-index: 1;
25695
+ }
24215
25696
  .cell {
24216
25697
  position: absolute;
24217
25698
  inset: 0 auto auto 0;
@@ -24505,6 +25986,9 @@ let GridListElement = class GridListElement extends LitElement {
24505
25986
  specificity. Survivors slide to their new slot; a freshly inserted cell
24506
25987
  also fades in. (A removed item's element is already gone, so there's
24507
25988
  nothing left to fade — the gap simply closes as the others slide.) */
25989
+ :host([reflowing]) .group-header {
25990
+ transition: transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1);
25991
+ }
24508
25992
  :host([reflowing]) .cell {
24509
25993
  transition:
24510
25994
  transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1),
@@ -24515,6 +25999,7 @@ let GridListElement = class GridListElement extends LitElement {
24515
25999
  animation: ixfx-list-item-in var(--grid-list-item-move-duration, 200ms) ease both;
24516
26000
  }
24517
26001
  @media (prefers-reduced-motion: reduce) {
26002
+ :host([reflowing]) .group-header { transition: none; }
24518
26003
  :host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
24519
26004
  .cell.entering { animation: none; }
24520
26005
  }
@@ -24656,31 +26141,13 @@ let GridListElement = class GridListElement extends LitElement {
24656
26141
  * so virtualized off-screen items are covered without DOM measurement.
24657
26142
  */
24658
26143
  #marqueeHitTest(rect) {
24659
- const count = this.#source.count;
24660
- if (count === 0) return [];
26144
+ if (this.#source.count === 0) return [];
24661
26145
  const horizontal = this.orientation === `row`;
24662
- 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);
24663
- const out = [];
24664
- if (horizontal) {
24665
- const x0 = rect.x + this._scrollLeft;
24666
- const x1 = x0 + rect.width;
24667
- const lo = Math.max(0, Math.floor(x0 / win.colStride));
24668
- const hi = Math.min(count - 1, Math.floor(x1 / win.colStride));
24669
- for (let i = lo; i <= hi; i++) out.push(i);
24670
- } else {
24671
- const x0 = rect.x + this._scrollLeft;
24672
- const x1 = x0 + rect.width;
24673
- const y0 = rect.y + this._scrollTop;
24674
- const y1 = y0 + rect.height;
24675
- const colLo = Math.max(0, Math.floor(x0 / win.colStride));
24676
- const colHi = Math.min(win.cols - 1, Math.floor(x1 / win.colStride));
24677
- const rowLo = Math.max(0, Math.floor(y0 / win.rowStride));
24678
- const rowHi = Math.floor(y1 / win.rowStride);
24679
- for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
24680
- const index = row * win.cols + col;
24681
- if (index >= 0 && index < count) out.push(index);
24682
- }
24683
- }
26146
+ const x0 = rect.x + this._scrollLeft;
26147
+ const x1 = x0 + rect.width;
26148
+ const y0 = horizontal ? 0 : rect.y + this._scrollTop;
26149
+ const y1 = horizontal ? 0 : y0 + rect.height;
26150
+ const out = this.#layout().hitTest(x0, x1, y0, y1);
24684
26151
  const keys = [];
24685
26152
  for (const index of out) {
24686
26153
  const key = this.#source.keyAt(index);
@@ -24830,6 +26297,10 @@ let GridListElement = class GridListElement extends LitElement {
24830
26297
  if (previousLength === 0 && removedCount === 0) return;
24831
26298
  if (added.length > 0 && added.length === this.#items.length) return;
24832
26299
  for (const el of added) this.#entering.add(el);
26300
+ this.#openReflowWindow();
26301
+ }
26302
+ /** Open (or extend) the `[reflowing]` window so moved cells and headers slide. */
26303
+ #openReflowWindow() {
24833
26304
  this.toggleAttribute(`reflowing`, true);
24834
26305
  if (this.#reflowTimer) clearTimeout(this.#reflowTimer);
24835
26306
  this.#reflowTimer = window.setTimeout(() => {
@@ -24886,6 +26357,7 @@ let GridListElement = class GridListElement extends LitElement {
24886
26357
  this.#sourceElement = new ElementGridListSource(this);
24887
26358
  this.#source = this.#sourceElement;
24888
26359
  }
26360
+ this.#source.setGrouping(this.#groupingConfig());
24889
26361
  this.#applyFilters();
24890
26362
  if (this.#searchFilterKeys) this.#source.setFilters({ searchKeys: this.#searchFilterKeys });
24891
26363
  }
@@ -24927,6 +26399,17 @@ let GridListElement = class GridListElement extends LitElement {
24927
26399
  }
24928
26400
  this.search?.refresh();
24929
26401
  }
26402
+ const groupsChanged = changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`);
26403
+ if (groupsChanged && this.hasUpdated) {
26404
+ if (changed.has(`groupBy`)) {
26405
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
26406
+ this.#groupByChanged = true;
26407
+ }
26408
+ this.#applyGroupingToSource();
26409
+ this.#dropUnreachableCursor();
26410
+ this.#openReflowWindow();
26411
+ this.updateComplete.then(() => this.#emitViewportChange());
26412
+ } else if (groupsChanged) this.#applyGroupingToSource();
24930
26413
  if (changed.has(`itemSize`) || changed.has(`gap`)) {
24931
26414
  this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
24932
26415
  this.gap = Math.max(0, Math.round(this.gap));
@@ -24947,6 +26430,62 @@ let GridListElement = class GridListElement extends LitElement {
24947
26430
  }
24948
26431
  }
24949
26432
  }
26433
+ #groupByChanged;
26434
+ /** Grouping config for the active source, or `undefined` when ungrouped. */
26435
+ #groupingConfig() {
26436
+ const by = this.groupBy;
26437
+ if (by === null || by === void 0 || by === ``) return void 0;
26438
+ const collapsed = this._collapsedGroups;
26439
+ const src = this.#source;
26440
+ if (src instanceof DataGridListSource) return {
26441
+ groupKeyOf: (raw) => resolveGroupKey(by, src.rawItemAt(raw), raw, readProperty),
26442
+ itemAt: (raw) => src.rawItemAt(raw),
26443
+ labelOf: this.groupLabel,
26444
+ collapsed
26445
+ };
26446
+ const elements = src;
26447
+ return {
26448
+ groupKeyOf: (raw) => resolveGroupKey(by, elements.rawElementAt(raw), raw, readAttribute),
26449
+ itemAt: (raw) => elements.rawElementAt(raw),
26450
+ labelOf: this.groupLabel,
26451
+ collapsed
26452
+ };
26453
+ }
26454
+ /** Push the current grouping into the source and refresh the element-mode item cache. */
26455
+ #applyGroupingToSource() {
26456
+ this.#source.setGrouping(this.#groupingConfig());
26457
+ if (this.#source instanceof ElementGridListSource) this.#items = this.#source.effectiveElements();
26458
+ }
26459
+ /** The cursor can't stay on an item that a collapsed group just removed. */
26460
+ #dropUnreachableCursor() {
26461
+ const id = this.tickled.tickledId;
26462
+ if (id !== void 0 && this.#source.indexOfKey(id) < 0) this.tickled.focus(void 0);
26463
+ }
26464
+ /** Groups in display order (empty when ungrouped). */
26465
+ get groups() {
26466
+ return this.groupBy === null ? [] : this.#source.groups();
26467
+ }
26468
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
26469
+ setGroupCollapsed(groupKey, collapsed) {
26470
+ if (this._collapsedGroups.has(groupKey) === collapsed) return;
26471
+ const next = new Set(this._collapsedGroups);
26472
+ if (collapsed) next.add(groupKey);
26473
+ else next.delete(groupKey);
26474
+ this._collapsedGroups = next;
26475
+ this._dispatch(`list-group-toggle`, {
26476
+ groupKey,
26477
+ collapsed
26478
+ });
26479
+ }
26480
+ toggleGroup(groupKey) {
26481
+ this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
26482
+ }
26483
+ collapseAllGroups() {
26484
+ for (const g of this.groups) this.setGroupCollapsed(g.key, true);
26485
+ }
26486
+ expandAllGroups() {
26487
+ for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
26488
+ }
24950
26489
  #syncAriaMultiselectable() {
24951
26490
  this.setAttribute(`aria-multiselectable`, this.selection.canSelectMultiple() ? `true` : `false`);
24952
26491
  }
@@ -24986,6 +26525,10 @@ let GridListElement = class GridListElement extends LitElement {
24986
26525
  }
24987
26526
  this.#applyZoomAnchor();
24988
26527
  this.#activateTickledPan();
26528
+ if (this.#groupByChanged) {
26529
+ this.#groupByChanged = false;
26530
+ this._dispatch(`list-group-change`, { groupBy: this.groupBy });
26531
+ }
24989
26532
  }
24990
26533
  /** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
24991
26534
  #applyZoomAnchor() {
@@ -24994,41 +26537,33 @@ let GridListElement = class GridListElement extends LitElement {
24994
26537
  this.#zoomAnchor = void 0;
24995
26538
  const vp = this.#viewportRef.value;
24996
26539
  if (!vp) return;
24997
- const win = this.#currentWindow();
24998
- if (win.horizontal) {
24999
- if (this._viewportWidth <= 0) return;
25000
- const targetCenter = (index + lineFrac) * win.colStride;
25001
- const maxScroll = Math.max(0, win.totalWidth - this._viewportWidth);
25002
- const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportWidth / 2));
26540
+ const layout = this.#layout();
26541
+ const horizontal = layout.horizontal;
26542
+ const extent = horizontal ? this._viewportWidth : this._viewportHeight;
26543
+ if (extent <= 0) return;
26544
+ const targetCenter = layout.offsetOfAnchor(index, lineFrac);
26545
+ const maxScroll = Math.max(0, layout.totalExtent - extent);
26546
+ const next = Math.min(maxScroll, Math.max(0, targetCenter - extent / 2));
26547
+ if (horizontal) {
25003
26548
  if (Math.abs(vp.scrollLeft - next) > .5) {
25004
26549
  vp.scrollLeft = next;
25005
26550
  this._scrollLeft = next;
25006
26551
  }
25007
- return;
25008
- }
25009
- if (this._viewportHeight <= 0) return;
25010
- const targetCenter = ((win.cols > 0 ? Math.floor(index / win.cols) : 0) + lineFrac) * win.rowStride;
25011
- const maxScroll = Math.max(0, win.totalHeight - this._viewportHeight);
25012
- const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportHeight / 2));
25013
- if (Math.abs(vp.scrollTop - next) > .5) {
26552
+ } else if (Math.abs(vp.scrollTop - next) > .5) {
25014
26553
  vp.scrollTop = next;
25015
26554
  this._scrollTop = next;
25016
26555
  }
25017
26556
  }
25018
26557
  #captureCentreAnchor(prevOffset, prevExtent, prevItemSize, prevGap, prevCaptionStyle, prevCaptionHeight, prevOrientation) {
25019
- const count = this.#source?.count ?? 0;
25020
- if (count === 0) return;
26558
+ if ((this.#source?.count ?? 0) === 0) return;
25021
26559
  const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
25022
- const horizontal = prevOrientation === `row`;
25023
- const prev = computeGridWindow(count, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
25024
- const mainStride = horizontal ? prev.colStride : prev.rowStride;
25025
- const centerLine = (prevOffset + prevExtent / 2) / mainStride;
25026
- const lineFloor = Math.max(0, Math.floor(centerLine));
25027
- const index = horizontal ? Math.min(count - 1, lineFloor) : Math.min(count - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
25028
- this.#zoomAnchor = {
25029
- index,
25030
- lineFrac: centerLine - lineFloor
25031
- };
26560
+ const prev = this.#buildLayout({
26561
+ itemWidth: prevItemSize,
26562
+ rowHeight: prevRowHeight,
26563
+ gap: prevGap,
26564
+ orientation: prevOrientation
26565
+ });
26566
+ this.#zoomAnchor = prev.anchorAt(prevOffset + prevExtent / 2);
25032
26567
  }
25033
26568
  /**
25034
26569
  * Re-read the viewport after an ancestor's visibility/layout changed.
@@ -25242,37 +26777,75 @@ let GridListElement = class GridListElement extends LitElement {
25242
26777
  return this.#windowKeys(this.#currentWindow(true));
25243
26778
  }
25244
26779
  #windowKeys(win) {
25245
- return this.#source.keys().slice(win.startIndex, win.endIndex).filter((k) => !isSyntheticKey(k));
26780
+ const keys = this.#source.keys();
26781
+ const out = [];
26782
+ for (const [start, end] of win.ranges) for (let i = start; i < end; i++) if (!isSyntheticKey(keys[i])) out.push(keys[i]);
26783
+ return out;
26784
+ }
26785
+ /** Layout params for the current geometry, with optional overrides (used for zoom anchoring). */
26786
+ #layoutParams(over = {}) {
26787
+ return {
26788
+ count: this.#source.count,
26789
+ groups: this.groupBy === null ? void 0 : this.#source.groups(),
26790
+ itemWidth: this.itemSize,
26791
+ rowHeight: this.#rowHeight(),
26792
+ gap: this.gap,
26793
+ viewportWidth: this._viewportWidth,
26794
+ viewportHeight: this._viewportHeight,
26795
+ orientation: this.orientation,
26796
+ headerHeight: this.groupHeaderHeight,
26797
+ headerWidth: this.groupHeaderWidth,
26798
+ ...over
26799
+ };
26800
+ }
26801
+ #buildLayout(over = {}) {
26802
+ return buildLayout(this.#layoutParams(over));
26803
+ }
26804
+ #layoutCache;
26805
+ /** The current layout, rebuilt only when its inputs change (never per scroll). */
26806
+ #layout() {
26807
+ const params = this.#layoutParams();
26808
+ const cached = this.#layoutCache;
26809
+ if (cached) {
26810
+ const a = cached.params;
26811
+ let same = a.groups === params.groups;
26812
+ for (const k of Object.keys(params)) {
26813
+ if (!same) break;
26814
+ same = a[k] === params[k];
26815
+ }
26816
+ if (same) return cached.layout;
26817
+ }
26818
+ const layout = buildLayout(params);
26819
+ this.#layoutCache = {
26820
+ params,
26821
+ layout
26822
+ };
26823
+ return layout;
25246
26824
  }
25247
26825
  #currentWindow(noOverscan = false) {
25248
- const overscan = noOverscan ? 0 : 2;
25249
- 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);
26826
+ return this.#layout().window(this.orientation === `row` ? this._scrollLeft : this._scrollTop, noOverscan ? 0 : 2);
25250
26827
  }
25251
26828
  /** Adjust the scroll offset so `key` is fully within the viewport. */
25252
26829
  #scrollItemIntoView(key) {
25253
26830
  const index = this.#source.indexOfKey(key);
25254
26831
  const vp = this.#viewportRef.value;
25255
26832
  if (index < 0 || !vp) return;
25256
- const win = this.#currentWindow();
25257
- if (win.horizontal) {
25258
- const left = index * win.colStride;
25259
- const right = left + this.itemSize;
26833
+ const [start, end] = this.#layout().extentOf(index);
26834
+ if (this.orientation === `row`) {
25260
26835
  const viewLeft = vp.scrollLeft;
25261
26836
  let next = viewLeft;
25262
- if (left < viewLeft) next = left;
25263
- else if (right > viewLeft + this._viewportWidth) next = right - this._viewportWidth;
26837
+ if (start < viewLeft) next = start;
26838
+ else if (end > viewLeft + this._viewportWidth) next = end - this._viewportWidth;
25264
26839
  if (next !== viewLeft) {
25265
26840
  vp.scrollLeft = next;
25266
26841
  this._scrollLeft = next;
25267
26842
  }
25268
26843
  return;
25269
26844
  }
25270
- const top = (win.cols > 0 ? Math.floor(index / win.cols) : 0) * win.rowStride;
25271
- const bottom = top + this.#rowHeight();
25272
26845
  const viewTop = vp.scrollTop;
25273
26846
  let next = viewTop;
25274
- if (top < viewTop) next = top;
25275
- else if (bottom > viewTop + this._viewportHeight) next = bottom - this._viewportHeight;
26847
+ if (start < viewTop) next = start;
26848
+ else if (end > viewTop + this._viewportHeight) next = end - this._viewportHeight;
25276
26849
  if (next !== viewTop) {
25277
26850
  vp.scrollTop = next;
25278
26851
  this._scrollTop = next;
@@ -25346,6 +26919,7 @@ let GridListElement = class GridListElement extends LitElement {
25346
26919
  >
25347
26920
  <div class="spacer" style=${spacerStyle}>
25348
26921
  ${this.#source instanceof DataGridListSource ? this.#renderDataCells(win) : this.#renderElementCells(win)}
26922
+ ${this.#renderHeaders(win)}
25349
26923
  </div>
25350
26924
  ${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
25351
26925
  </div>
@@ -25353,7 +26927,7 @@ let GridListElement = class GridListElement extends LitElement {
25353
26927
  `;
25354
26928
  }
25355
26929
  #renderDataCells(win) {
25356
- const slice = Array.from({ length: win.endIndex - win.startIndex }, (_, i) => win.startIndex + i);
26930
+ const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
25357
26931
  return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderDataCell(index, win));
25358
26932
  }
25359
26933
  #renderDataCell(index, win) {
@@ -25368,8 +26942,7 @@ let GridListElement = class GridListElement extends LitElement {
25368
26942
  const caption = style === `hidden` ? `` : this.#source.labelAt(index);
25369
26943
  const showCaption = caption !== ``;
25370
26944
  const cellHeight = win.rowStride - this.gap;
25371
- const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
25372
- const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
26945
+ const { x, y } = this.#layout().positionOf(index);
25373
26946
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
25374
26947
  const rawIcon = this.#source.iconAt(index);
25375
26948
  const icon = appearanceAbsent(rawIcon) ? void 0 : rawIcon;
@@ -25455,8 +27028,25 @@ let GridListElement = class GridListElement extends LitElement {
25455
27028
  current.startLoop?.();
25456
27029
  }
25457
27030
  #renderElementCells(win) {
25458
- const slice = this.#items.slice(win.startIndex, win.endIndex);
25459
- return repeat(slice, (_el, i) => this.#source.keyAt(win.startIndex + i), (el, i) => this.#renderCell(el, win.startIndex + i, win));
27031
+ const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
27032
+ return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderCell(this.#items[index], index, win));
27033
+ }
27034
+ #renderHeaders(win) {
27035
+ if (win.headers.length === 0) return nothing;
27036
+ const horizontal = win.horizontal;
27037
+ return repeat(win.headers, (h) => `__group__${h.key}`, (h) => html`
27038
+ <ixfx-list-group-header
27039
+ class="group-header"
27040
+ part="group-header"
27041
+ data-group-key=${h.key}
27042
+ label=${h.label}
27043
+ count=${h.count}
27044
+ orientation=${horizontal ? `vertical` : `horizontal`}
27045
+ ?collapsed=${h.collapsed}
27046
+ 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)`}
27047
+ @click=${() => this.toggleGroup(h.key)}
27048
+ ></ixfx-list-group-header>
27049
+ `);
25460
27050
  }
25461
27051
  #renderCell(el, index, win) {
25462
27052
  const key = this.#source.keyAt(index);
@@ -25471,8 +27061,7 @@ let GridListElement = class GridListElement extends LitElement {
25471
27061
  const caption = style === `hidden` ? `` : this.#captionFor(el);
25472
27062
  const showCaption = caption !== ``;
25473
27063
  const cellHeight = win.rowStride - this.gap;
25474
- const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
25475
- const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
27064
+ const { x, y } = this.#layout().positionOf(index);
25476
27065
  const thumbStyle = separate ? `height:${this.itemSize}px` : ``;
25477
27066
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
25478
27067
  return html`
@@ -25571,16 +27160,12 @@ let GridListElement = class GridListElement extends LitElement {
25571
27160
  #snapToNearestLine() {
25572
27161
  const vp = this.#viewportRef.value;
25573
27162
  if (!vp) return;
25574
- const win = this.#currentWindow();
25575
- const horizontal = win.horizontal;
25576
- const stride = horizontal ? win.colStride : win.rowStride;
25577
- if (stride <= 0) return;
27163
+ const layout = this.#layout();
27164
+ const horizontal = layout.horizontal;
25578
27165
  const offset = horizontal ? vp.scrollLeft : vp.scrollTop;
25579
- const extent = horizontal ? win.totalWidth : win.totalHeight;
25580
27166
  const viewportExtent = horizontal ? this._viewportWidth : this._viewportHeight;
25581
- const maxScroll = Math.max(0, extent - viewportExtent);
25582
- const index = Math.round(offset / stride);
25583
- const target = Math.min(maxScroll, Math.max(0, index * stride));
27167
+ const maxScroll = Math.max(0, layout.totalExtent - viewportExtent);
27168
+ const target = Math.min(maxScroll, Math.max(0, layout.snapTarget(offset)));
25584
27169
  if (Math.abs(target - offset) < .5) return;
25585
27170
  vp.scrollTo(horizontal ? {
25586
27171
  left: target,
@@ -25608,7 +27193,7 @@ let GridListElement = class GridListElement extends LitElement {
25608
27193
  if (mode === `manual`) return;
25609
27194
  if (this.selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
25610
27195
  if (this.#dragSelect.isDragging || this.#nativeDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
25611
- for (const node of e.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return;
27196
+ for (const node of e.composedPath()) if (node instanceof Element && (node.classList.contains(`cell`) || node.classList.contains(`group-header`))) return;
25612
27197
  this.selection.clear();
25613
27198
  }
25614
27199
  #startPostDragSuppressTimer() {
@@ -25709,6 +27294,22 @@ __decorate([property({
25709
27294
  })], GridListElement.prototype, "snapToItem", void 0);
25710
27295
  __decorate([property({ attribute: false })], GridListElement.prototype, "items", void 0);
25711
27296
  __decorate([property({ attribute: false })], GridListElement.prototype, "itemAdapter", void 0);
27297
+ __decorate([property({ attribute: `group-by` })], GridListElement.prototype, "groupBy", void 0);
27298
+ __decorate([property({ attribute: false })], GridListElement.prototype, "groupLabel", void 0);
27299
+ __decorate([property({
27300
+ type: Boolean,
27301
+ attribute: `cross-group-selection`,
27302
+ reflect: true
27303
+ })], GridListElement.prototype, "crossGroupSelection", void 0);
27304
+ __decorate([property({
27305
+ type: Number,
27306
+ attribute: `group-header-height`
27307
+ })], GridListElement.prototype, "groupHeaderHeight", void 0);
27308
+ __decorate([property({
27309
+ type: Number,
27310
+ attribute: `group-header-width`
27311
+ })], GridListElement.prototype, "groupHeaderWidth", void 0);
27312
+ __decorate([state()], GridListElement.prototype, "_collapsedGroups", void 0);
25712
27313
  __decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
25713
27314
  __decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
25714
27315
  __decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
@@ -53313,6 +54914,6 @@ function init() {
53313
54914
  console.log(`Init`);
53314
54915
  }
53315
54916
  //#endregion
53316
- export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, 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 };
54917
+ 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 };
53317
54918
 
53318
54919
  //# sourceMappingURL=index.js.map