@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
@@ -3,8 +3,10 @@ import { r as IDataProvider } from "./data-provider-BAn_-oR1.js";
3
3
  import { F as ListDragPreviewSourceResolver, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent } from "./tree-component-DukLqesg.js";
4
4
  import { n as ListViewportChangeEvent } from "./list-viewport-types-BRZsLcvp.js";
5
5
  import { S as ListSearchController } from "./index-CMsmZ6Or.js";
6
+ import { a as ListGroupEventMap, o as ListGroupInfo, s as ListGroupLabel } from "./grouping-CUKgmkMm.js";
6
7
  import { i as ListInteractionMode, l as ListSelectionMode, r as ListElementEventMap } from "./list-selection-types-PjKdhF-a.js";
7
8
  import { n as CustomCheckboxOptions, t as AddItemOptions } from "./vertical-list-DTp-UIUE.js";
9
+ import "./list-group-header.js";
8
10
  import { LitElement, PropertyValues } from "lit";
9
11
  import { CommandId, CommandRegistry } from "@clinth/ui-commands";
10
12
  //#region src/detail-list/types.d.ts
@@ -20,6 +22,12 @@ type DetailSortItem = {
20
22
  type DetailSortComparator = (a: DetailSortItem, b: DetailSortItem) => number;
21
23
  /** Turns one object item into row content — a plain string or a DOM node. */
22
24
  type DetailFormatter = (item: unknown, index: number) => string | Node;
25
+ /**
26
+ * How a detail list derives an item's group key: a property name (object
27
+ * items) or `data-<name>` attribute, or a function of the item's data (`undefined`
28
+ * for declarative rows) and its `<li>` element.
29
+ */
30
+ type DetailGroupBy = string | ((data: unknown, el: Element) => string);
23
31
  //#endregion
24
32
  //#region src/detail-list/detail-list.d.ts
25
33
  /**
@@ -95,6 +103,21 @@ declare class DetailListElement extends LitElement {
95
103
  */
96
104
  dragPreviewRenderer: ListDragPreviewRenderer<Element> | undefined;
97
105
  dragPreviewSource: ListDragPreviewSourceResolver<Element> | undefined;
106
+ /**
107
+ * Group rows under collapsible headers that flow inline in the columns. A
108
+ * string names an item property (object items) or a `data-<name>` attribute;
109
+ * a function derives the key from `(data, element)`. `null` = ungrouped.
110
+ * Groups appear in first-encounter order (after sorting and filtering).
111
+ */
112
+ groupBy: DetailGroupBy | null;
113
+ /** Header label for a group. Defaults to the key (`(none)` for the empty key). */
114
+ groupLabel: ListGroupLabel | undefined;
115
+ /**
116
+ * Let selection span groups. When `false` (default) selecting in one group
117
+ * clears the others and Shift-range / select-all stay inside one group.
118
+ */
119
+ crossGroupSelection: boolean;
120
+ private _collapsedGroups;
98
121
  private _listEl;
99
122
  private readonly selection;
100
123
  private tickled;
@@ -106,6 +129,7 @@ declare class DetailListElement extends LitElement {
106
129
  connectedCallback(): void;
107
130
  disconnectedCallback(): void;
108
131
  firstUpdated(): void;
132
+ protected willUpdate(changed: PropertyValues): void;
109
133
  updated(changed: PropertyValues): void;
110
134
  /** The most recently selected item, or `undefined` if nothing is selected. */
111
135
  get selectedItem(): Element | undefined;
@@ -170,6 +194,13 @@ declare class DetailListElement extends LitElement {
170
194
  /** Move the cursor to the item with this key and scroll it into view. */
171
195
  focusKey(key: string): void;
172
196
  selectedFilter(kind: `selected` | `checked` | `either`): Generator<Element>;
197
+ /** Groups in display order (empty when ungrouped). */
198
+ get groups(): readonly ListGroupInfo[];
199
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
200
+ setGroupCollapsed(groupKey: string, collapsed: boolean): void;
201
+ toggleGroup(groupKey: string): void;
202
+ collapseAllGroups(): void;
203
+ expandAllGroups(): void;
173
204
  private _getVisibleItems;
174
205
  /**
175
206
  * Set the column width to fit the widest visible item.
@@ -199,6 +230,8 @@ declare class DetailListElement extends LitElement {
199
230
  private _dispatchCancelable;
200
231
  private _onPointerOver;
201
232
  private _onPointerLeave;
233
+ /** Restores hover transitions once the pointer takes over from the keyboard. */
234
+ private _endKeyboardNav;
202
235
  private _onKeyDown;
203
236
  private _onClick;
204
237
  private _onDblClick;
@@ -213,10 +246,10 @@ declare global {
213
246
  }
214
247
  interface HTMLElementEventMap extends DetailListElementEventMap {}
215
248
  }
216
- interface DetailListElementEventMap extends ListElementEventMap {
249
+ interface DetailListElementEventMap extends ListElementEventMap, ListGroupEventMap {
217
250
  'detail-list-viewport-change': ListViewportChangeEvent;
218
251
  'list-native-drag-start': ListNativeDragStartEvent<Element>;
219
252
  }
220
253
  //#endregion
221
- export { DetailSortItem as a, DetailSortComparator as i, DetailListElementEventMap as n, DetailSortMode as o, DetailFormatter as r, DetailListElement as t };
222
- //# sourceMappingURL=detail-list-CmdCxddo.d.ts.map
254
+ export { DetailSortComparator as a, DetailGroupBy as i, DetailListElementEventMap as n, DetailSortItem as o, DetailFormatter as r, DetailSortMode as s, DetailListElement as t };
255
+ //# sourceMappingURL=detail-list-lE_5UWVl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-list-lE_5UWVl.d.ts","names":[],"sources":["../src/detail-list/types.ts","../src/detail-list/detail-list.ts"],"mappings":";;;;;;;;;;;;;KACY;;KAGA;WACD,SAAS;WACT;WACA;;;KAIC,wBAAwB,GAAG,gBAAgB,GAAG;;KAG9C,mBAAmB,eAAe,2BAA2B;;;;;;KAc7D,2BAA2B,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;cCkI7C,0BAA0B;;EAErC,eAAe;EAGf,iBAAiB;EAGjB,MAAM;EAGN;EAGA,gBAAgB;EAGhB;EAGA,UAAU;EAGV;EAGA,WAAW;EAGX;;;;;EAOA;;;;;EAOA;;;;;;EAQA;EAGA;EAGA,mBAAmB,IAAI;;;;;;EAQvB;;EAIA,oBAAoB,gCAAgC;;;;;;EAQpD;;;;;;EAQA;;;;;EAOA,qBAAqB,wBAAwB;EAG7C,mBAAmB,8BAA8B;;;;;;;EASjD,SAAS;;EAIT,YAAY;;;;;EAOZ;UAEiB;UAGT;mBAiCS;UAqBT;UAyFA;;WAoGC,QAAM,qBAAA;;MAYX,YAAY;EAoBP;EAkEA;EA+BA;YASU,WAAW,SAAS;EAS9B,QAAQ,SAAS;;MA+DtB,gBAAgB;;MAKhB,iBAAiB,YAAY;;;;;;EASjC;EAIA,QAAQ,MAAM,kBAAkB,UAAU;;;;;EAwB1C,WAAW,MAAM,UAAU;;EAY3B,YAAY,OAAO,SAAS,WAAW;EAIvC;;;;;EAcA,SAAS,gBAAgB;;;;;EAiBzB,YAAY,MAAM;;;;;;;;;;EAalB;;;;;;EAsBA,SAAS,WAAW,WAAW,OAAO;EAItC,OAAO,MAAM;EAIb,WAAW,OAAO,SAAS;EAI3B,SAAS,MAAM;EAIf;EAIA;;MAQI,gBAAgB;;MAKhB;;EAMJ,WAAW,MAAM;;EAKjB,gBAAgB,MAAM;;EAKtB,mBAAmB,MAAM;;EAKzB,SAAS;EAgBP,eAAe,0CAA0C,UAAU;;MA8VjE,mBAAmB;;EAKvB,kBAAkB,kBAAkB;EAYpC,YAAY;EAIZ;EAKA;UAwRQ;;;;;;;;;EAoBF,kBAAkB;UAwBhB;;UAOA;UA2DA;UAiBA;;;;;;;UA2DA;;UAoBA;UAcA;UAYA;UAQA;UA6DA;UAMA;;UAKA;UAIA;UA+EA;UAiBA;UAMA;UAUA;SAUQ,sBAAM;EAyKb,wBAAM;;QAsBT;YACI;IACR,oBAAoB;;YAGZ,4BAA4B;;UAGvB,kCAAkC,qBAAqB;EACtE,+BAA+B;EAC/B,0BAA0B,yBAAyB"}
@@ -1,5 +1,6 @@
1
1
  import { I as ListDragSource, L as ListNativeDragStartDetail, M as ListDragPreview, N as ListDragPreviewContext, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent, j as ListDragEntry } from "./tree-component-DukLqesg.js";
2
2
  import { n as ListViewportChangeEvent, t as ListViewportChangeDetail } from "./list-viewport-types-BRZsLcvp.js";
3
+ import { i as ListGroupChangeEvent, l as ListGroupToggleDetail, n as ListGroupBy, o as ListGroupInfo, r as ListGroupChangeDetail, s as ListGroupLabel, u as ListGroupToggleEvent } from "./grouping-CUKgmkMm.js";
3
4
  import { n as CustomCheckboxOptions, t as AddItemOptions } from "./vertical-list-DTp-UIUE.js";
4
- import { a as DetailSortItem, i as DetailSortComparator, n as DetailListElementEventMap, o as DetailSortMode, r as DetailFormatter, t as DetailListElement } from "./detail-list-CmdCxddo.js";
5
- export { type AddItemOptions, type CustomCheckboxOptions, type DetailFormatter, DetailListElement, DetailListElementEventMap, type DetailSortComparator, type DetailSortItem, type DetailSortMode, type ListDragEntry, type ListDragPreview, type ListDragPreviewContext, type ListDragPreviewRenderer, type ListDragSource, type ListNativeDragStartDetail, type ListNativeDragStartEvent, type ListViewportChangeDetail, type ListViewportChangeEvent };
5
+ import { a as DetailSortComparator, i as DetailGroupBy, n as DetailListElementEventMap, o as DetailSortItem, r as DetailFormatter, s as DetailSortMode, t as DetailListElement } from "./detail-list-lE_5UWVl.js";
6
+ export { type AddItemOptions, type CustomCheckboxOptions, type DetailFormatter, type DetailGroupBy, DetailListElement, DetailListElementEventMap, type DetailSortComparator, type DetailSortItem, type DetailSortMode, type ListDragEntry, type ListDragPreview, type ListDragPreviewContext, type ListDragPreviewRenderer, type ListDragSource, type ListGroupBy, type ListGroupChangeDetail, type ListGroupChangeEvent, type ListGroupInfo, type ListGroupLabel, type ListGroupToggleDetail, type ListGroupToggleEvent, type ListNativeDragStartDetail, type ListNativeDragStartEvent, type ListViewportChangeDetail, type ListViewportChangeEvent };
@@ -4,17 +4,29 @@ 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`;
22
+ const NAV_KEYS = /* @__PURE__ */ new Set([
23
+ `ArrowDown`,
24
+ `ArrowUp`,
25
+ `ArrowLeft`,
26
+ `ArrowRight`,
27
+ `Home`,
28
+ `End`
29
+ ]);
18
30
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
19
31
  /**
20
32
  * Layout for the per-item badge wrapper `#ensureBadge` adds. Applied as inline
@@ -63,14 +75,26 @@ let DetailListElement = class DetailListElement extends LitElement {
63
75
  this.keyProperty = ``;
64
76
  this.itemWrap = `stop`;
65
77
  this.itemsDraggable = false;
78
+ this.groupBy = null;
79
+ this.crossGroupSelection = false;
80
+ this._collapsedGroups = /* @__PURE__ */ new Set();
66
81
  this.#itemData = /* @__PURE__ */ new WeakMap();
67
82
  this.#grid = [];
68
83
  this.#positions = /* @__PURE__ */ new Map();
84
+ this.#tops = /* @__PURE__ */ new Map();
69
85
  this.#geometryScheduled = false;
70
86
  this.#sortRaf = 0;
71
87
  this.#viewportScrollRaf = 0;
72
88
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
73
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;
74
98
  this.#providerBuilt = 0;
75
99
  this.#data = new DataController(this);
76
100
  this.selection = new ListSelectionController({
@@ -81,6 +105,12 @@ let DetailListElement = class DetailListElement extends LitElement {
81
105
  interactionMode: this.interactionMode
82
106
  }),
83
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
+ },
84
114
  onChange: (selected, previous) => {
85
115
  this._syncItemStates();
86
116
  this._dispatch(`list-select`, {
@@ -162,6 +192,7 @@ let DetailListElement = class DetailListElement extends LitElement {
162
192
  if (this._listEl) this._listEl.scrollLeft += dx;
163
193
  },
164
194
  shouldStart: (event) => {
195
+ if (this.#findGroupHeader(event.composedPath())) return false;
165
196
  this.#blankItemPressPending = this.itemsDraggable && this.#isBlankItemPress(event);
166
197
  return !this.itemsDraggable || this.#blankItemPressPending;
167
198
  }
@@ -185,6 +216,9 @@ let DetailListElement = class DetailListElement extends LitElement {
185
216
  this._onPointerLeave = (e) => {
186
217
  this.tickled.handlePointerLeave(e);
187
218
  };
219
+ this._endKeyboardNav = () => {
220
+ this.removeAttribute(`data-keyboard-nav`);
221
+ };
188
222
  this._onKeyDown = (e) => {
189
223
  if (this.search.isOpen) return;
190
224
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
@@ -194,6 +228,7 @@ let DetailListElement = class DetailListElement extends LitElement {
194
228
  }
195
229
  return;
196
230
  }
231
+ if (NAV_KEYS.has(e.key)) this.setAttribute(`data-keyboard-nav`, ``);
197
232
  switch (e.key) {
198
233
  case `ArrowDown`:
199
234
  e.preventDefault();
@@ -249,7 +284,13 @@ let DetailListElement = class DetailListElement extends LitElement {
249
284
  };
250
285
  this._onClick = (e) => {
251
286
  if (this.#dragSelect.isDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
252
- 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);
253
294
  if (!item) return;
254
295
  this.focus();
255
296
  this._dispatch(`list-item-click`, { item });
@@ -268,6 +309,8 @@ let DetailListElement = class DetailListElement extends LitElement {
268
309
  #itemData;
269
310
  #grid;
270
311
  #positions;
312
+ /** Viewport-relative `top` per item at the last geometry pass — lines rows up across columns. */
313
+ #tops;
271
314
  #geometryScheduled;
272
315
  #sortRaf;
273
316
  #viewportScrollRaf;
@@ -279,6 +322,17 @@ let DetailListElement = class DetailListElement extends LitElement {
279
322
  /** Items that have already played (or been skipped for) their enter animation. */
280
323
  #animatedItems;
281
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;
282
336
  #providerBuilt;
283
337
  #data;
284
338
  #nativeDragging;
@@ -382,6 +436,8 @@ let DetailListElement = class DetailListElement extends LitElement {
382
436
  this.addEventListener(`pointerover`, this._onPointerOver);
383
437
  this.addEventListener(`pointerleave`, this._onPointerLeave);
384
438
  this.addEventListener(`keydown`, this._onKeyDown);
439
+ this.addEventListener(`pointermove`, this._endKeyboardNav);
440
+ this.addEventListener(`pointerdown`, this._endKeyboardNav);
385
441
  this.addEventListener(`click`, this._onClick);
386
442
  this.addEventListener(`dblclick`, this._onDblClick);
387
443
  this.addEventListener(`wheel`, this._onWheel);
@@ -393,6 +449,7 @@ let DetailListElement = class DetailListElement extends LitElement {
393
449
  this.#processChildren();
394
450
  this.#animateChildMutations(records);
395
451
  this.#scheduleGeometry();
452
+ this.#scheduleGroups();
396
453
  });
397
454
  this.#mutationObserver.observe(this, {
398
455
  childList: true,
@@ -402,6 +459,7 @@ let DetailListElement = class DetailListElement extends LitElement {
402
459
  for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
403
460
  this.#searchRegistry ??= createCommandRegistry();
404
461
  this.#syncSearchCommands();
462
+ this.#scheduleGroups();
405
463
  }
406
464
  /**
407
465
  * Collapse-in items appended to the light DOM (however they got there). A bulk
@@ -428,6 +486,8 @@ let DetailListElement = class DetailListElement extends LitElement {
428
486
  this.removeEventListener(`pointerover`, this._onPointerOver);
429
487
  this.removeEventListener(`pointerleave`, this._onPointerLeave);
430
488
  this.removeEventListener(`keydown`, this._onKeyDown);
489
+ this.removeEventListener(`pointermove`, this._endKeyboardNav);
490
+ this.removeEventListener(`pointerdown`, this._endKeyboardNav);
431
491
  this.removeEventListener(`click`, this._onClick);
432
492
  this.removeEventListener(`dblclick`, this._onDblClick);
433
493
  this.removeEventListener(`wheel`, this._onWheel);
@@ -441,6 +501,8 @@ let DetailListElement = class DetailListElement extends LitElement {
441
501
  this.#resizeObserver = void 0;
442
502
  this.#mutationObserver?.disconnect();
443
503
  this.#mutationObserver = void 0;
504
+ this.#groupObserver?.disconnect();
505
+ this.#groupObserver = void 0;
444
506
  this.#unregisterSearchCommands?.();
445
507
  this.#unregisterSearchCommands = void 0;
446
508
  this.#searchRegistry = void 0;
@@ -455,8 +517,22 @@ let DetailListElement = class DetailListElement extends LitElement {
455
517
  this.#recomputeGeometry();
456
518
  this._syncNativeDraggable();
457
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
+ }
458
527
  updated(changed) {
459
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
+ }
460
536
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
461
537
  if (changed.has(`provider`)) {
462
538
  this.#builtFromProvider = [];
@@ -473,6 +549,7 @@ let DetailListElement = class DetailListElement extends LitElement {
473
549
  if (changed.has(`filterPredicate`)) {
474
550
  this.#applyFilterPredicate();
475
551
  this.#scheduleGeometry();
552
+ this.#scheduleGroups();
476
553
  }
477
554
  if (changed.has(`interactionMode`)) {
478
555
  this.selection.clearAnchor();
@@ -512,6 +589,7 @@ let DetailListElement = class DetailListElement extends LitElement {
512
589
  this.#ensureIcon(el);
513
590
  if (this.sort !== `none`) this.#applySort();
514
591
  if (this.interactionMode === `checked`) this._syncCheckboxes();
592
+ this.#scheduleGroups();
515
593
  }
516
594
  /**
517
595
  * Collapse + fade `item` out, then detach it; the rows after it slide up to
@@ -521,6 +599,7 @@ let DetailListElement = class DetailListElement extends LitElement {
521
599
  this.selection.deselect(item);
522
600
  return animateListItemOut(item).then(() => {
523
601
  item.remove();
602
+ this.#scheduleGroups();
524
603
  if (this.interactionMode === `checked`) this._syncCheckboxes();
525
604
  this.#recomputeGeometry();
526
605
  });
@@ -531,6 +610,8 @@ let DetailListElement = class DetailListElement extends LitElement {
531
610
  }
532
611
  clearItems() {
533
612
  this.replaceChildren();
613
+ this.#headerEls.clear();
614
+ this.#scheduleGroups();
534
615
  this.selection.reset();
535
616
  this.#builtFromProvider = [];
536
617
  this.#providerBuilt = 0;
@@ -548,6 +629,7 @@ let DetailListElement = class DetailListElement extends LitElement {
548
629
  }
549
630
  if (this.sort !== `none`) this.#applySort();
550
631
  if (this.interactionMode === `checked`) this._syncCheckboxes();
632
+ this.#scheduleGroups();
551
633
  }
552
634
  /**
553
635
  * Get the source object behind an `<li>` built from an `items` array or
@@ -667,6 +749,7 @@ let DetailListElement = class DetailListElement extends LitElement {
667
749
  if (this.interactionMode === `checked`) this._syncCheckboxes();
668
750
  this._syncItemStates();
669
751
  this.#scheduleGeometry();
752
+ this.#scheduleGroups();
670
753
  }
671
754
  #createItemEl(item, index) {
672
755
  const li = document.createElement(`li`);
@@ -718,6 +801,7 @@ let DetailListElement = class DetailListElement extends LitElement {
718
801
  if (this.interactionMode === `checked`) this._syncCheckboxes();
719
802
  this._syncItemStates();
720
803
  this.#scheduleGeometry();
804
+ this.#scheduleGroups();
721
805
  } else {
722
806
  this.#providerBuilt = 0;
723
807
  this.#rebuildFromObjects(items);
@@ -736,6 +820,16 @@ let DetailListElement = class DetailListElement extends LitElement {
736
820
  if (this.sort === `none`) return;
737
821
  const rows = [...this.querySelectorAll(ITEM_SELECTOR)];
738
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) {
739
833
  const dir = this.sort === `descending` ? -1 : 1;
740
834
  const meta = (el) => ({
741
835
  element: el,
@@ -765,9 +859,147 @@ let DetailListElement = class DetailListElement extends LitElement {
765
859
  i
766
860
  }));
767
861
  indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
768
- 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);
769
955
  this.#scheduleGeometry();
770
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
+ }
771
1003
  #scheduleGeometry() {
772
1004
  if (this.#geometryScheduled) return;
773
1005
  this.#geometryScheduled = true;
@@ -779,11 +1011,13 @@ let DetailListElement = class DetailListElement extends LitElement {
779
1011
  #recomputeGeometry() {
780
1012
  this.#grid = [];
781
1013
  this.#positions.clear();
1014
+ this.#tops.clear();
782
1015
  if (!this.isConnected) return;
783
1016
  const columns = [];
784
1017
  for (const el of this._getVisibleItems()) {
785
1018
  const rect = el.getBoundingClientRect();
786
1019
  if (rect.width === 0 && rect.height === 0) continue;
1020
+ this.#tops.set(el, rect.top);
787
1021
  let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
788
1022
  if (ci === -1) {
789
1023
  columns.push({
@@ -904,6 +1138,30 @@ let DetailListElement = class DetailListElement extends LitElement {
904
1138
  const next = this.#findNextCheckable(cur.col, cur.row, 0, -1);
905
1139
  if (next) this.#navigateTo(next);
906
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
+ }
907
1165
  #navigateRight() {
908
1166
  this.#recomputeGeometry();
909
1167
  const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
@@ -911,7 +1169,7 @@ let DetailListElement = class DetailListElement extends LitElement {
911
1169
  this.#navigateTo(this.#findFirstCheckable());
912
1170
  return;
913
1171
  }
914
- const next = this.#findNextCheckable(cur.col, cur.row, 1, 0);
1172
+ const next = this.#horizontalTarget(cur, 1);
915
1173
  if (next) this.#navigateTo(next);
916
1174
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
917
1175
  }
@@ -922,13 +1180,14 @@ let DetailListElement = class DetailListElement extends LitElement {
922
1180
  this.#navigateTo(this.#findFirstCheckable());
923
1181
  return;
924
1182
  }
925
- const next = this.#findNextCheckable(cur.col, cur.row, -1, 0);
1183
+ const next = this.#horizontalTarget(cur, -1);
926
1184
  if (next) this.#navigateTo(next);
927
1185
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
928
1186
  }
929
1187
  _getVisibleItems() {
930
1188
  return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
931
1189
  if (el.hidden) return false;
1190
+ if (el.hasAttribute(GROUP_COLLAPSED_ATTR)) return false;
932
1191
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
933
1192
  return true;
934
1193
  });
@@ -1217,6 +1476,20 @@ let DetailListElement = class DetailListElement extends LitElement {
1217
1476
  display: none !important;
1218
1477
  }
1219
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
+
1220
1493
  ::slotted(li) {
1221
1494
  box-sizing: border-box;
1222
1495
  display: flex;
@@ -1232,6 +1505,12 @@ let DetailListElement = class DetailListElement extends LitElement {
1232
1505
  outline: 1px solid transparent;
1233
1506
  }
1234
1507
 
1508
+ /* While navigating by keyboard, key-repeat outpaces the 0.1s fade so the
1509
+ cursor smears across rows. Snap instead. */
1510
+ :host([data-keyboard-nav]) ::slotted(li) {
1511
+ transition: none;
1512
+ }
1513
+
1235
1514
  /* In checked mode, only the checkbox column is clickable; the row body
1236
1515
  gets a default cursor so it doesn't suggest otherwise. */
1237
1516
  :host([interaction-mode="checked"]) ::slotted(li) {
@@ -1387,6 +1666,14 @@ __decorate([property({
1387
1666
  })], DetailListElement.prototype, "itemsDraggable", void 0);
1388
1667
  __decorate([property({ attribute: false })], DetailListElement.prototype, "dragPreviewRenderer", void 0);
1389
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);
1390
1677
  __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
1391
1678
  DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
1392
1679
  //#endregion