@ixfx/components 0.11.7 → 0.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/bundle/index.d.ts +165 -2
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +1160 -164
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/{ac-token--_vCHXWw.d.ts → ac-token-TCLAZeVg.d.ts} +2 -2
  7. package/dist/{ac-token--_vCHXWw.d.ts.map → ac-token-TCLAZeVg.d.ts.map} +1 -1
  8. package/dist/ac-token.d.ts +1 -1
  9. package/dist/colour-picker.d.ts +1 -1
  10. package/dist/crumbs.d.ts +1 -1
  11. package/dist/crumbs.js +1 -1
  12. package/dist/{data-grid-C6EjCe5O.d.ts → data-grid-Oiyt5qbA.d.ts} +4 -4
  13. package/dist/{data-grid-C6EjCe5O.d.ts.map → data-grid-Oiyt5qbA.d.ts.map} +1 -1
  14. package/dist/data-grid.d.ts +1 -1
  15. package/dist/data-grid.js +1 -1
  16. package/dist/{detail-list-CgkOHYOa.d.ts → detail-list-B0waVNeS.d.ts} +37 -6
  17. package/dist/detail-list-B0waVNeS.d.ts.map +1 -0
  18. package/dist/detail-list.d.ts +4 -3
  19. package/dist/detail-list.js +271 -6
  20. package/dist/detail-list.js.map +1 -1
  21. package/dist/{grid-list-3fGhdaFA.js → grid-list-Bwaf61is.js} +659 -158
  22. package/dist/grid-list-Bwaf61is.js.map +1 -0
  23. package/dist/{grid-list-D6AfWfwn.d.ts → grid-list-bzKuNTtu.d.ts} +43 -2
  24. package/dist/grid-list-bzKuNTtu.d.ts.map +1 -0
  25. package/dist/grid-list.d.ts +3 -2
  26. package/dist/grid-list.js +2 -1
  27. package/dist/{grouped-item-lister-DETte0Dh.d.ts → grouped-item-lister-Bhyo-dkH.d.ts} +2 -2
  28. package/dist/{grouped-item-lister-DETte0Dh.d.ts.map → grouped-item-lister-Bhyo-dkH.d.ts.map} +1 -1
  29. package/dist/grouped-item-lister.d.ts +1 -1
  30. package/dist/grouping-CUKgmkMm.d.ts +63 -0
  31. package/dist/grouping-CUKgmkMm.d.ts.map +1 -0
  32. package/dist/grouping-RYPnHkrS.js +53 -0
  33. package/dist/grouping-RYPnHkrS.js.map +1 -0
  34. package/dist/{icon-CDGprSdE.d.ts → icon-DR_3rSyZ.d.ts} +2 -2
  35. package/dist/icon-DR_3rSyZ.d.ts.map +1 -0
  36. package/dist/icons.d.ts +2 -2
  37. package/dist/incr-search.d.ts +1 -1
  38. package/dist/{index-tq6f8007.d.ts → index-BOUaDIW-.d.ts} +2 -2
  39. package/dist/{index-tq6f8007.d.ts.map → index-BOUaDIW-.d.ts.map} +1 -1
  40. package/dist/{index-CoqtMSd9.d.ts → index-BUB5SICW.d.ts} +2 -2
  41. package/dist/{index-CoqtMSd9.d.ts.map → index-BUB5SICW.d.ts.map} +1 -1
  42. package/dist/{index-CMsmZ6Or.d.ts → index-BvxHQUME.d.ts} +2 -2
  43. package/dist/{index-CMsmZ6Or.d.ts.map → index-BvxHQUME.d.ts.map} +1 -1
  44. package/dist/{index-UvnoZUAo.d.ts → index-CJ5fshD3.d.ts} +2 -2
  45. package/dist/{index-UvnoZUAo.d.ts.map → index-CJ5fshD3.d.ts.map} +1 -1
  46. package/dist/{index-DyTSt9OK.d.ts → index-CW2QL33o.d.ts} +3 -3
  47. package/dist/{index-DyTSt9OK.d.ts.map → index-CW2QL33o.d.ts.map} +1 -1
  48. package/dist/{index-CYndvTWH.d.ts → index-garxxqgV.d.ts} +2 -2
  49. package/dist/{index-CYndvTWH.d.ts.map → index-garxxqgV.d.ts.map} +1 -1
  50. package/dist/index.d.ts +20 -18
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +7 -5
  53. package/dist/index.js.map +1 -1
  54. package/dist/{labelled-input-base-DiTme1JO.d.ts → labelled-input-base-Dp_9KP0G.d.ts} +2 -2
  55. package/dist/{labelled-input-base-DiTme1JO.d.ts.map → labelled-input-base-Dp_9KP0G.d.ts.map} +1 -1
  56. package/dist/labelled-radial-input.d.ts +2 -2
  57. package/dist/labelled-radial-input.d.ts.map +1 -1
  58. package/dist/labelled-range-input.d.ts +1 -1
  59. package/dist/labelled-slider-input.d.ts +1 -1
  60. package/dist/list-group-header.d.ts +26 -0
  61. package/dist/list-group-header.d.ts.map +1 -0
  62. package/dist/list-group-header.js +122 -0
  63. package/dist/list-group-header.js.map +1 -0
  64. package/dist/{list-selection-controller-SA4piSBi.js → list-selection-controller-BeE70lbB.js} +42 -4
  65. package/dist/list-selection-controller-BeE70lbB.js.map +1 -0
  66. package/dist/{menu-CsBUyUuS.js → menu-DaAlsyhf.js} +2 -2
  67. package/dist/{menu-CsBUyUuS.js.map → menu-DaAlsyhf.js.map} +1 -1
  68. package/dist/{menu-item-BbgdcwBX.d.ts → menu-item-B0zqw2xp.d.ts} +14 -2
  69. package/dist/menu-item-B0zqw2xp.d.ts.map +1 -0
  70. package/dist/{menu-item-CfndBGO8.js → menu-item-CDPFgaSF.js} +32 -1
  71. package/dist/menu-item-CDPFgaSF.js.map +1 -0
  72. package/dist/menu.d.ts +2 -2
  73. package/dist/menu.js +2 -2
  74. package/dist/miller.d.ts +1 -1
  75. package/dist/panel.d.ts +1 -1
  76. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-C5SYbDrq.d.ts} +2 -2
  77. package/dist/{split-layout-JK3xWVX2.d.ts.map → split-layout-C5SYbDrq.d.ts.map} +1 -1
  78. package/dist/split-layout.d.ts +1 -1
  79. package/dist/{tab-list-pqfKaVAE.d.ts → tab-list-v8mKX7Fu.d.ts} +2 -2
  80. package/dist/{tab-list-pqfKaVAE.d.ts.map → tab-list-v8mKX7Fu.d.ts.map} +1 -1
  81. package/dist/tabs.d.ts +1 -1
  82. package/dist/{titlebar-Tqu-VvMU.d.ts → titlebar-B5zSTZnF.d.ts} +2 -2
  83. package/dist/{titlebar-Tqu-VvMU.d.ts.map → titlebar-B5zSTZnF.d.ts.map} +1 -1
  84. package/dist/titlebar.d.ts +1 -1
  85. package/dist/tree.d.ts +1 -1
  86. package/dist/{vertical-list-DTp-UIUE.d.ts → vertical-list-DheZSEmB.d.ts} +2 -2
  87. package/dist/{vertical-list-DTp-UIUE.d.ts.map → vertical-list-DheZSEmB.d.ts.map} +1 -1
  88. package/dist/vertical-list.d.ts +1 -1
  89. package/dist/vertical-list.js +1 -1
  90. package/docs-user/README.md +1 -0
  91. package/docs-user/detail-list.md +28 -0
  92. package/docs-user/grid-list.md +35 -3
  93. package/docs-user/index.json +7 -2
  94. package/docs-user/list-group-header.md +14 -0
  95. package/docs-user/llms.txt +1 -0
  96. package/llms.txt +1 -0
  97. package/package.json +5 -1
  98. package/dist/detail-list-CgkOHYOa.d.ts.map +0 -1
  99. package/dist/grid-list-3fGhdaFA.js.map +0 -1
  100. package/dist/grid-list-D6AfWfwn.d.ts.map +0 -1
  101. package/dist/icon-CDGprSdE.d.ts.map +0 -1
  102. package/dist/list-selection-controller-SA4piSBi.js.map +0 -1
  103. package/dist/menu-item-BbgdcwBX.d.ts.map +0 -1
  104. package/dist/menu-item-CfndBGO8.js.map +0 -1
@@ -4,16 +4,20 @@ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js"
4
4
  import "./icon-1gpwtPx8.js";
5
5
  import { t as DataController } from "./data-controller-DOlmqNKb.js";
6
6
  import { c as createElementListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-D91M4Ozq.js";
7
+ import { a as toGroupKey, r as partitionByGroup } from "./grouping-RYPnHkrS.js";
7
8
  import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
8
- import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-SA4piSBi.js";
9
+ import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-BeE70lbB.js";
9
10
  import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
10
11
  import { t as scrollFadeStyles } from "./scroll-fade-BrNUIYnm.js";
11
12
  import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
13
+ import "./list-group-header.js";
12
14
  import { LitElement, css, html, nothing, render } from "lit";
13
- import { property, query } from "lit/decorators.js";
15
+ import { property, query, state } from "lit/decorators.js";
14
16
  import { createCommandRegistry } from "@clinth/ui-commands";
15
17
  //#region src/detail-list/detail-list.ts
16
18
  const ITEM_SELECTOR = `:scope > li`;
19
+ const GROUP_HEADER_ATTR = `data-ixfx-group-header`;
20
+ const GROUP_COLLAPSED_ATTR = `data-group-collapsed`;
17
21
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
18
22
  const NAV_KEYS = /* @__PURE__ */ new Set([
19
23
  `ArrowDown`,
@@ -71,14 +75,26 @@ let DetailListElement = class DetailListElement extends LitElement {
71
75
  this.keyProperty = ``;
72
76
  this.itemWrap = `stop`;
73
77
  this.itemsDraggable = false;
78
+ this.groupBy = null;
79
+ this.crossGroupSelection = false;
80
+ this._collapsedGroups = /* @__PURE__ */ new Set();
74
81
  this.#itemData = /* @__PURE__ */ new WeakMap();
75
82
  this.#grid = [];
76
83
  this.#positions = /* @__PURE__ */ new Map();
84
+ this.#tops = /* @__PURE__ */ new Map();
77
85
  this.#geometryScheduled = false;
78
86
  this.#sortRaf = 0;
79
87
  this.#viewportScrollRaf = 0;
80
88
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
81
89
  this.#builtFromProvider = [];
90
+ this.#groupIndex = /* @__PURE__ */ new Map();
91
+ this.#groupList = [];
92
+ this.#headerEls = /* @__PURE__ */ new Map();
93
+ this.#seq = /* @__PURE__ */ new WeakMap();
94
+ this.#seqNext = 0;
95
+ this.#wasGrouped = false;
96
+ this.#groupsScheduled = false;
97
+ this.#groupByChanged = false;
82
98
  this.#providerBuilt = 0;
83
99
  this.#data = new DataController(this);
84
100
  this.selection = new ListSelectionController({
@@ -89,6 +105,12 @@ let DetailListElement = class DetailListElement extends LitElement {
89
105
  interactionMode: this.interactionMode
90
106
  }),
91
107
  getKey: keyForElement,
108
+ getGroupKey: (el) => this.#groupIndex.get(el) ?? null,
109
+ crossGroupSelection: () => this.crossGroupSelection,
110
+ getFallbackGroupKey: () => {
111
+ const t = this.tickled.tickledId;
112
+ return t ? this.#groupIndex.get(t) ?? null : null;
113
+ },
92
114
  onChange: (selected, previous) => {
93
115
  this._syncItemStates();
94
116
  this._dispatch(`list-select`, {
@@ -170,6 +192,7 @@ let DetailListElement = class DetailListElement extends LitElement {
170
192
  if (this._listEl) this._listEl.scrollLeft += dx;
171
193
  },
172
194
  shouldStart: (event) => {
195
+ if (this.#findGroupHeader(event.composedPath())) return false;
173
196
  this.#blankItemPressPending = this.itemsDraggable && this.#isBlankItemPress(event);
174
197
  return !this.itemsDraggable || this.#blankItemPressPending;
175
198
  }
@@ -261,7 +284,13 @@ let DetailListElement = class DetailListElement extends LitElement {
261
284
  };
262
285
  this._onClick = (e) => {
263
286
  if (this.#dragSelect.isDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
264
- const item = this._findListItem(e.composedPath());
287
+ const path = e.composedPath();
288
+ const header = this.#findGroupHeader(path);
289
+ if (header) {
290
+ this.toggleGroup(header.dataset.groupKey ?? ``);
291
+ return;
292
+ }
293
+ const item = this._findListItem(path);
265
294
  if (!item) return;
266
295
  this.focus();
267
296
  this._dispatch(`list-item-click`, { item });
@@ -280,6 +309,8 @@ let DetailListElement = class DetailListElement extends LitElement {
280
309
  #itemData;
281
310
  #grid;
282
311
  #positions;
312
+ /** Viewport-relative `top` per item at the last geometry pass — lines rows up across columns. */
313
+ #tops;
283
314
  #geometryScheduled;
284
315
  #sortRaf;
285
316
  #viewportScrollRaf;
@@ -291,6 +322,17 @@ let DetailListElement = class DetailListElement extends LitElement {
291
322
  /** Items that have already played (or been skipped for) their enter animation. */
292
323
  #animatedItems;
293
324
  #builtFromProvider;
325
+ /** Element → group key, for the selection controller. Empty while ungrouped. */
326
+ #groupIndex;
327
+ #groupList;
328
+ #headerEls;
329
+ /** Insertion sequence per item, so regrouping starts from the original order. */
330
+ #seq;
331
+ #seqNext;
332
+ #wasGrouped;
333
+ #groupsScheduled;
334
+ #groupByChanged;
335
+ #groupObserver;
294
336
  #providerBuilt;
295
337
  #data;
296
338
  #nativeDragging;
@@ -407,6 +449,7 @@ let DetailListElement = class DetailListElement extends LitElement {
407
449
  this.#processChildren();
408
450
  this.#animateChildMutations(records);
409
451
  this.#scheduleGeometry();
452
+ this.#scheduleGroups();
410
453
  });
411
454
  this.#mutationObserver.observe(this, {
412
455
  childList: true,
@@ -416,6 +459,7 @@ let DetailListElement = class DetailListElement extends LitElement {
416
459
  for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
417
460
  this.#searchRegistry ??= createCommandRegistry();
418
461
  this.#syncSearchCommands();
462
+ this.#scheduleGroups();
419
463
  }
420
464
  /**
421
465
  * Collapse-in items appended to the light DOM (however they got there). A bulk
@@ -457,6 +501,8 @@ let DetailListElement = class DetailListElement extends LitElement {
457
501
  this.#resizeObserver = void 0;
458
502
  this.#mutationObserver?.disconnect();
459
503
  this.#mutationObserver = void 0;
504
+ this.#groupObserver?.disconnect();
505
+ this.#groupObserver = void 0;
460
506
  this.#unregisterSearchCommands?.();
461
507
  this.#unregisterSearchCommands = void 0;
462
508
  this.#searchRegistry = void 0;
@@ -471,8 +517,22 @@ let DetailListElement = class DetailListElement extends LitElement {
471
517
  this.#recomputeGeometry();
472
518
  this._syncNativeDraggable();
473
519
  }
520
+ willUpdate(changed) {
521
+ super.willUpdate(changed);
522
+ if (changed.has(`groupBy`) && this.hasUpdated) {
523
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
524
+ this.#groupByChanged = true;
525
+ }
526
+ }
474
527
  updated(changed) {
475
528
  super.updated(changed);
529
+ if (changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`)) {
530
+ this.#scheduleGroups();
531
+ if (this.#groupByChanged) {
532
+ this.#groupByChanged = false;
533
+ this._dispatch(`list-group-change`, { groupBy: this.groupBy });
534
+ }
535
+ }
476
536
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
477
537
  if (changed.has(`provider`)) {
478
538
  this.#builtFromProvider = [];
@@ -489,6 +549,7 @@ let DetailListElement = class DetailListElement extends LitElement {
489
549
  if (changed.has(`filterPredicate`)) {
490
550
  this.#applyFilterPredicate();
491
551
  this.#scheduleGeometry();
552
+ this.#scheduleGroups();
492
553
  }
493
554
  if (changed.has(`interactionMode`)) {
494
555
  this.selection.clearAnchor();
@@ -528,6 +589,7 @@ let DetailListElement = class DetailListElement extends LitElement {
528
589
  this.#ensureIcon(el);
529
590
  if (this.sort !== `none`) this.#applySort();
530
591
  if (this.interactionMode === `checked`) this._syncCheckboxes();
592
+ this.#scheduleGroups();
531
593
  }
532
594
  /**
533
595
  * Collapse + fade `item` out, then detach it; the rows after it slide up to
@@ -537,6 +599,7 @@ let DetailListElement = class DetailListElement extends LitElement {
537
599
  this.selection.deselect(item);
538
600
  return animateListItemOut(item).then(() => {
539
601
  item.remove();
602
+ this.#scheduleGroups();
540
603
  if (this.interactionMode === `checked`) this._syncCheckboxes();
541
604
  this.#recomputeGeometry();
542
605
  });
@@ -547,6 +610,8 @@ let DetailListElement = class DetailListElement extends LitElement {
547
610
  }
548
611
  clearItems() {
549
612
  this.replaceChildren();
613
+ this.#headerEls.clear();
614
+ this.#scheduleGroups();
550
615
  this.selection.reset();
551
616
  this.#builtFromProvider = [];
552
617
  this.#providerBuilt = 0;
@@ -564,6 +629,7 @@ let DetailListElement = class DetailListElement extends LitElement {
564
629
  }
565
630
  if (this.sort !== `none`) this.#applySort();
566
631
  if (this.interactionMode === `checked`) this._syncCheckboxes();
632
+ this.#scheduleGroups();
567
633
  }
568
634
  /**
569
635
  * Get the source object behind an `<li>` built from an `items` array or
@@ -683,6 +749,7 @@ let DetailListElement = class DetailListElement extends LitElement {
683
749
  if (this.interactionMode === `checked`) this._syncCheckboxes();
684
750
  this._syncItemStates();
685
751
  this.#scheduleGeometry();
752
+ this.#scheduleGroups();
686
753
  }
687
754
  #createItemEl(item, index) {
688
755
  const li = document.createElement(`li`);
@@ -734,6 +801,7 @@ let DetailListElement = class DetailListElement extends LitElement {
734
801
  if (this.interactionMode === `checked`) this._syncCheckboxes();
735
802
  this._syncItemStates();
736
803
  this.#scheduleGeometry();
804
+ this.#scheduleGroups();
737
805
  } else {
738
806
  this.#providerBuilt = 0;
739
807
  this.#rebuildFromObjects(items);
@@ -752,6 +820,16 @@ let DetailListElement = class DetailListElement extends LitElement {
752
820
  if (this.sort === `none`) return;
753
821
  const rows = [...this.querySelectorAll(ITEM_SELECTOR)];
754
822
  if (rows.length < 2) return;
823
+ if (this.#groupingActive()) {
824
+ this.#scheduleGroups();
825
+ return;
826
+ }
827
+ for (const el of this.#sortRows(rows)) this.appendChild(el);
828
+ this.#scheduleGeometry();
829
+ this.#scheduleGroups();
830
+ }
831
+ /** Stable sort of `rows` by the current `sort` / `sortBy` / `sortComparator`. */
832
+ #sortRows(rows) {
755
833
  const dir = this.sort === `descending` ? -1 : 1;
756
834
  const meta = (el) => ({
757
835
  element: el,
@@ -781,9 +859,147 @@ let DetailListElement = class DetailListElement extends LitElement {
781
859
  i
782
860
  }));
783
861
  indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
784
- for (const { el } of indexed) this.appendChild(el);
862
+ return indexed.map(({ el }) => el);
863
+ }
864
+ #groupingActive() {
865
+ return this.groupBy !== null && this.groupBy !== void 0 && this.groupBy !== ``;
866
+ }
867
+ /** Coalesce regrouping to one pass per microtask (streaming batches, search keystrokes). */
868
+ #scheduleGroups() {
869
+ if (this.#groupsScheduled || !this.#groupingActive() && !this.#wasGrouped) return;
870
+ this.#groupsScheduled = true;
871
+ queueMicrotask(() => {
872
+ this.#groupsScheduled = false;
873
+ this.#syncGroups();
874
+ });
875
+ }
876
+ #groupValue(el, name) {
877
+ return this.#propertyValue(this.getItemData(el), name) ?? el.getAttribute(`data-${name}`);
878
+ }
879
+ #groupKeyOf(el) {
880
+ const by = this.groupBy;
881
+ if (typeof by === `string`) return toGroupKey(this.#groupValue(el, by));
882
+ return toGroupKey(by(this.getItemData(el), el));
883
+ }
884
+ #isItemVisible(el) {
885
+ return !el.hidden && (!this.filterPredicate || this.filterPredicate(el));
886
+ }
887
+ #headerFor(key) {
888
+ let header = this.#headerEls.get(key);
889
+ if (!header) {
890
+ header = document.createElement(`ixfx-list-group-header`);
891
+ header.setAttribute(GROUP_HEADER_ATTR, ``);
892
+ header.dataset.groupKey = key;
893
+ this.#headerEls.set(key, header);
894
+ }
895
+ return header;
896
+ }
897
+ /**
898
+ * Bring the light DOM into grouped order: original order → sort → partition
899
+ * into contiguous groups, each preceded by a header row. Idempotent — it only
900
+ * touches the DOM when the order actually changes, so it never loops through
901
+ * the mutation observer. Ungrouping restores the original (sorted) order.
902
+ */
903
+ #syncGroups() {
904
+ const active = this.#groupingActive();
905
+ if (!active && !this.#wasGrouped) return;
906
+ const items = [...this.querySelectorAll(ITEM_SELECTOR)];
907
+ for (const el of items) if (!this.#seq.has(el)) this.#seq.set(el, this.#seqNext++);
908
+ let ordered = items.toSorted((a, b) => this.#seq.get(a) - this.#seq.get(b));
909
+ if (this.sort !== `none` && ordered.length > 1) ordered = this.#sortRows(ordered);
910
+ const nodes = [];
911
+ const info = [];
912
+ const index = /* @__PURE__ */ new Map();
913
+ const liveHeaders = /* @__PURE__ */ new Set();
914
+ if (active) {
915
+ const label = this.groupLabel;
916
+ const parts = partitionByGroup(ordered, (el) => this.#groupKeyOf(el), label ? (key, els) => label(key, els.map((el) => this.getItemData(el) ?? el)) : void 0);
917
+ for (const part of parts) {
918
+ const count = part.items.filter((el) => this.#isItemVisible(el)).length;
919
+ const collapsed = count > 0 && this._collapsedGroups.has(part.key);
920
+ if (count > 0) {
921
+ const header = this.#headerFor(part.key);
922
+ header.label = part.label;
923
+ header.count = count;
924
+ header.collapsed = collapsed;
925
+ liveHeaders.add(part.key);
926
+ nodes.push(header);
927
+ info.push({
928
+ key: part.key,
929
+ label: part.label,
930
+ count,
931
+ collapsed: this._collapsedGroups.has(part.key)
932
+ });
933
+ }
934
+ for (const el of part.items) {
935
+ index.set(el, part.key);
936
+ el.toggleAttribute(GROUP_COLLAPSED_ATTR, collapsed);
937
+ nodes.push(el);
938
+ }
939
+ }
940
+ } else {
941
+ for (const el of ordered) el.removeAttribute(GROUP_COLLAPSED_ATTR);
942
+ nodes.push(...ordered);
943
+ }
944
+ for (const [key, header] of this.#headerEls) if (!liveHeaders.has(key)) {
945
+ header.remove();
946
+ this.#headerEls.delete(key);
947
+ }
948
+ this.#groupIndex = index;
949
+ this.#groupList = info;
950
+ this.#wasGrouped = active;
951
+ const current = [...this.children].filter((c) => c.matches(`li`) || c.hasAttribute(GROUP_HEADER_ATTR));
952
+ if (current.length !== nodes.length || current.some((c, i) => c !== nodes[i])) this.append(...nodes);
953
+ if (this.tickled.tickledId?.hasAttribute(GROUP_COLLAPSED_ATTR)) this.tickled.focus(void 0);
954
+ this.#syncGroupObserver(active);
785
955
  this.#scheduleGeometry();
786
956
  }
957
+ /** Filter-search hides rows via `hidden` on the `<li>`s — regroup when that changes. */
958
+ #syncGroupObserver(active) {
959
+ if (!active) {
960
+ this.#groupObserver?.disconnect();
961
+ this.#groupObserver = void 0;
962
+ return;
963
+ }
964
+ if (this.#groupObserver || !this.isConnected) return;
965
+ this.#groupObserver = new MutationObserver(() => this.#scheduleGroups());
966
+ this.#groupObserver.observe(this, {
967
+ subtree: true,
968
+ attributes: true,
969
+ attributeFilter: [`hidden`]
970
+ });
971
+ }
972
+ /** Groups in display order (empty when ungrouped). */
973
+ get groups() {
974
+ return this.#groupList;
975
+ }
976
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
977
+ setGroupCollapsed(groupKey, collapsed) {
978
+ if (this._collapsedGroups.has(groupKey) === collapsed) return;
979
+ const next = new Set(this._collapsedGroups);
980
+ if (collapsed) next.add(groupKey);
981
+ else next.delete(groupKey);
982
+ this._collapsedGroups = next;
983
+ this._dispatch(`list-group-toggle`, {
984
+ groupKey,
985
+ collapsed
986
+ });
987
+ }
988
+ toggleGroup(groupKey) {
989
+ this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
990
+ }
991
+ collapseAllGroups() {
992
+ for (const g of this.#groupList) this.setGroupCollapsed(g.key, true);
993
+ }
994
+ expandAllGroups() {
995
+ for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
996
+ }
997
+ #findGroupHeader(path) {
998
+ for (const target of path) {
999
+ if (target === this) break;
1000
+ if (target instanceof HTMLElement && target.parentElement === this && target.hasAttribute(GROUP_HEADER_ATTR)) return target;
1001
+ }
1002
+ }
787
1003
  #scheduleGeometry() {
788
1004
  if (this.#geometryScheduled) return;
789
1005
  this.#geometryScheduled = true;
@@ -795,11 +1011,13 @@ let DetailListElement = class DetailListElement extends LitElement {
795
1011
  #recomputeGeometry() {
796
1012
  this.#grid = [];
797
1013
  this.#positions.clear();
1014
+ this.#tops.clear();
798
1015
  if (!this.isConnected) return;
799
1016
  const columns = [];
800
1017
  for (const el of this._getVisibleItems()) {
801
1018
  const rect = el.getBoundingClientRect();
802
1019
  if (rect.width === 0 && rect.height === 0) continue;
1020
+ this.#tops.set(el, rect.top);
803
1021
  let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
804
1022
  if (ci === -1) {
805
1023
  columns.push({
@@ -920,6 +1138,30 @@ let DetailListElement = class DetailListElement extends LitElement {
920
1138
  const next = this.#findNextCheckable(cur.col, cur.row, 0, -1);
921
1139
  if (next) this.#navigateTo(next);
922
1140
  }
1141
+ /**
1142
+ * Left/Right target. Ungrouped lists keep the row-index rule. Grouped lists
1143
+ * have headers at arbitrary heights, so an item's row index no longer equals
1144
+ * its visual row — pick the item in the neighbouring column whose top is
1145
+ * nearest the cursor's (ties go to the upper one).
1146
+ */
1147
+ #horizontalTarget(from, dir) {
1148
+ if (this.#groupList.length === 0) return this.#findNextCheckable(from.col, from.row, dir, 0);
1149
+ const column = this.#grid[from.col + dir];
1150
+ const current = this.tickled.tickledId;
1151
+ const top = current ? this.#tops.get(current) : void 0;
1152
+ if (!column || top === void 0) return void 0;
1153
+ let best;
1154
+ let bestDelta = Number.POSITIVE_INFINITY;
1155
+ for (const el of column) {
1156
+ if (!this._isCheckable(el)) continue;
1157
+ const delta = Math.abs((this.#tops.get(el) ?? Number.POSITIVE_INFINITY) - top);
1158
+ if (delta < bestDelta) {
1159
+ best = el;
1160
+ bestDelta = delta;
1161
+ }
1162
+ }
1163
+ return best;
1164
+ }
923
1165
  #navigateRight() {
924
1166
  this.#recomputeGeometry();
925
1167
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
@@ -927,7 +1169,7 @@ let DetailListElement = class DetailListElement extends LitElement {
927
1169
  this.#navigateTo(this.#findFirstCheckable());
928
1170
  return;
929
1171
  }
930
- const next = this.#findNextCheckable(cur.col, cur.row, 1, 0);
1172
+ const next = this.#horizontalTarget(cur, 1);
931
1173
  if (next) this.#navigateTo(next);
932
1174
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
933
1175
  }
@@ -938,13 +1180,14 @@ let DetailListElement = class DetailListElement extends LitElement {
938
1180
  this.#navigateTo(this.#findFirstCheckable());
939
1181
  return;
940
1182
  }
941
- const next = this.#findNextCheckable(cur.col, cur.row, -1, 0);
1183
+ const next = this.#horizontalTarget(cur, -1);
942
1184
  if (next) this.#navigateTo(next);
943
1185
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
944
1186
  }
945
1187
  _getVisibleItems() {
946
1188
  return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
947
1189
  if (el.hidden) return false;
1190
+ if (el.hasAttribute(GROUP_COLLAPSED_ATTR)) return false;
948
1191
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
949
1192
  return true;
950
1193
  });
@@ -1233,6 +1476,20 @@ let DetailListElement = class DetailListElement extends LitElement {
1233
1476
  display: none !important;
1234
1477
  }
1235
1478
 
1479
+ /* Rows of a collapsed group leave the flow; their header stays. */
1480
+ ::slotted([data-group-collapsed]) {
1481
+ display: none !important;
1482
+ }
1483
+
1484
+ /* Group headers are rows in the column flow, the same size as an item. */
1485
+ ::slotted(ixfx-list-group-header) {
1486
+ box-sizing: border-box;
1487
+ flex: none;
1488
+ width: var(--detail-list-column-width, 220px);
1489
+ height: var(--detail-list-item-height, 28px);
1490
+ --list-group-header-height: var(--detail-list-group-header-height, var(--detail-list-item-height, 28px));
1491
+ }
1492
+
1236
1493
  ::slotted(li) {
1237
1494
  box-sizing: border-box;
1238
1495
  display: flex;
@@ -1409,6 +1666,14 @@ __decorate([property({
1409
1666
  })], DetailListElement.prototype, "itemsDraggable", void 0);
1410
1667
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewRenderer", void 0);
1411
1668
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewSource", void 0);
1669
+ __decorate([property({ attribute: `group-by` })], DetailListElement.prototype, "groupBy", void 0);
1670
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "groupLabel", void 0);
1671
+ __decorate([property({
1672
+ type: Boolean,
1673
+ attribute: `cross-group-selection`,
1674
+ reflect: true
1675
+ })], DetailListElement.prototype, "crossGroupSelection", void 0);
1676
+ __decorate([state()], DetailListElement.prototype, "_collapsedGroups", void 0);
1412
1677
  __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
1413
1678
  DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
1414
1679
  //#endregion