@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,19 +3,388 @@ import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrA
3
3
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
5
  import { c as createElementListSearchAdapter, d as createDataListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-D91M4Ozq.js";
6
+ import { i as resolveGroupKey, r as partitionByGroup } from "./grouping-RYPnHkrS.js";
6
7
  import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
7
- import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-SA4piSBi.js";
8
+ import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-BeE70lbB.js";
8
9
  import { i as listItemFadeKeyframes } from "./list-item-animation-BUoEKi5s.js";
9
10
  import "./interaction-BskmlA9t.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";
12
13
  import "./pan-text-CqtvjPyK.js";
14
+ import "./list-group-header.js";
13
15
  import { LitElement, css, html, nothing } from "lit";
14
16
  import { property, state } from "lit/decorators.js";
15
17
  import { createCommandRegistry } from "@clinth/ui-commands";
16
18
  import { repeat } from "lit/directives/repeat.js";
17
19
  import { createRef, ref } from "lit/directives/ref.js";
20
+ //#region src/grid-list/grid-list-layout.ts
21
+ /**
22
+ * `itemWidth` is the square's width; `rowHeight` is the full cell height
23
+ * (square, plus the caption band in `separate` / `under` styles).
24
+ * `scrollOffset` is `scrollTop` for `grid`/`column` and `scrollLeft` for `row`.
25
+ */
26
+ function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
27
+ const w = Math.max(1, itemWidth);
28
+ const h = Math.max(1, rowHeight);
29
+ const g = Math.max(0, gap);
30
+ const colStride = w + g;
31
+ const rowStride = h + g;
32
+ const offset = Math.max(0, scrollOffset);
33
+ if (orientation === `row`) {
34
+ const rows = count === 0 ? 0 : 1;
35
+ const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
36
+ if (count === 0 || viewportWidth <= 0) return {
37
+ cols: count,
38
+ rows,
39
+ totalHeight: 0,
40
+ totalWidth,
41
+ startIndex: 0,
42
+ endIndex: 0,
43
+ colStride,
44
+ rowStride,
45
+ horizontal: true
46
+ };
47
+ const firstVisible = Math.max(0, Math.floor(offset / colStride));
48
+ const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
49
+ return {
50
+ cols: count,
51
+ rows,
52
+ totalHeight: 0,
53
+ totalWidth,
54
+ startIndex: Math.max(0, firstVisible - overscan),
55
+ endIndex: Math.min(count, lastVisible + overscan),
56
+ colStride,
57
+ rowStride,
58
+ horizontal: true
59
+ };
60
+ }
61
+ const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
62
+ const rows = count === 0 ? 0 : Math.ceil(count / cols);
63
+ const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
64
+ if (count === 0 || viewportHeight <= 0) return {
65
+ cols,
66
+ rows,
67
+ totalHeight,
68
+ totalWidth: 0,
69
+ startIndex: 0,
70
+ endIndex: 0,
71
+ colStride,
72
+ rowStride,
73
+ horizontal: false
74
+ };
75
+ const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
76
+ const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
77
+ const rowStart = Math.max(0, firstVisibleRow - overscan);
78
+ const rowEnd = Math.min(rows, lastVisibleRow + overscan);
79
+ return {
80
+ cols,
81
+ rows,
82
+ totalHeight,
83
+ totalWidth: 0,
84
+ startIndex: rowStart * cols,
85
+ endIndex: Math.min(count, rowEnd * cols),
86
+ colStride,
87
+ rowStride,
88
+ horizontal: false
89
+ };
90
+ }
91
+ function buildSections(p, cols, stride, itemExtent) {
92
+ const g = Math.max(0, p.gap);
93
+ const horizontal = p.orientation === `row`;
94
+ const lineCount = (n) => horizontal ? n : Math.ceil(n / cols);
95
+ const extentFor = (n) => n === 0 ? 0 : (lineCount(n) - 1) * stride + itemExtent;
96
+ if (!p.groups) {
97
+ const end = extentFor(p.count);
98
+ return {
99
+ sections: [{
100
+ group: void 0,
101
+ first: 0,
102
+ count: p.count,
103
+ headerOffset: 0,
104
+ itemsOffset: 0,
105
+ end
106
+ }],
107
+ total: end,
108
+ count: p.count
109
+ };
110
+ }
111
+ const headerSize = horizontal ? p.headerWidth : p.headerHeight;
112
+ const sections = [];
113
+ let cursor = 0;
114
+ let first = 0;
115
+ for (const group of p.groups) {
116
+ const count = group.collapsed ? 0 : group.count;
117
+ const itemsOffset = cursor + headerSize + g;
118
+ const end = count === 0 ? cursor + headerSize : itemsOffset + extentFor(count);
119
+ sections.push({
120
+ group,
121
+ first,
122
+ count,
123
+ headerOffset: cursor,
124
+ itemsOffset,
125
+ end
126
+ });
127
+ first += count;
128
+ cursor = end + g;
129
+ }
130
+ return {
131
+ sections,
132
+ total: sections.length === 0 ? 0 : sections[sections.length - 1].end,
133
+ count: first
134
+ };
135
+ }
136
+ /** Index of the last section whose `headerOffset` is <= `offset` (0 when before all). */
137
+ function sectionAtOffset(sections, offset) {
138
+ let lo = 0;
139
+ let hi = sections.length - 1;
140
+ while (lo < hi) {
141
+ const mid = lo + hi + 1 >> 1;
142
+ if (sections[mid].headerOffset <= offset) lo = mid;
143
+ else hi = mid - 1;
144
+ }
145
+ return lo;
146
+ }
147
+ /** Index of the section holding effective item `index` (skips empty sections). */
148
+ function sectionOfIndex(sections, index) {
149
+ let lo = 0;
150
+ let hi = sections.length - 1;
151
+ while (lo < hi) {
152
+ const mid = lo + hi + 1 >> 1;
153
+ if (sections[mid].first <= index) lo = mid;
154
+ else hi = mid - 1;
155
+ }
156
+ let s = lo;
157
+ while (s < sections.length - 1 && sections[s].count === 0) s++;
158
+ return s;
159
+ }
160
+ function buildLayout(p) {
161
+ const horizontal = p.orientation === `row`;
162
+ const w = Math.max(1, p.itemWidth);
163
+ const h = Math.max(1, p.rowHeight);
164
+ const g = Math.max(0, p.gap);
165
+ const colStride = w + g;
166
+ const rowStride = h + g;
167
+ const cols = horizontal ? 1 : p.orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, p.viewportWidth) + g) / colStride));
168
+ const stride = horizontal ? colStride : rowStride;
169
+ const itemExtent = horizontal ? w : h;
170
+ const { sections, total, count } = buildSections(p, cols, stride, itemExtent);
171
+ const grouped = p.groups !== void 0;
172
+ const headerSize = horizontal ? p.headerWidth : p.headerHeight;
173
+ const viewportMain = horizontal ? p.viewportWidth : p.viewportHeight;
174
+ const cellHeight = rowStride - g;
175
+ const rowY = Math.max(0, (p.viewportHeight - cellHeight) / 2);
176
+ const positionOf = (index) => {
177
+ const si = sectionOfIndex(sections, index);
178
+ const s = sections[si];
179
+ const local = Math.max(0, index - s.first);
180
+ if (horizontal) return {
181
+ x: s.itemsOffset + local * colStride,
182
+ y: rowY,
183
+ col: 0,
184
+ line: local,
185
+ section: si
186
+ };
187
+ const col = cols <= 1 ? 0 : local % cols;
188
+ const line = cols <= 1 ? local : Math.floor(local / cols);
189
+ return {
190
+ x: col * colStride,
191
+ y: s.itemsOffset + line * rowStride,
192
+ col,
193
+ line,
194
+ section: si
195
+ };
196
+ };
197
+ const window = (offset, overscan = 2) => {
198
+ const off = Math.max(0, offset);
199
+ const headers = [];
200
+ const ranges = [];
201
+ const lo = off - overscan * stride;
202
+ const hi = off + viewportMain + overscan * stride;
203
+ let firstSection = 0;
204
+ if (sections.length > 1) {
205
+ let a = 0;
206
+ let b = sections.length - 1;
207
+ while (a < b) {
208
+ const mid = a + b >> 1;
209
+ if (sections[mid].end >= lo) b = mid;
210
+ else a = mid + 1;
211
+ }
212
+ firstSection = a;
213
+ }
214
+ if (viewportMain > 0) for (let si = firstSection; si < sections.length; si++) {
215
+ const s = sections[si];
216
+ if (s.headerOffset > hi) break;
217
+ if (s.group && s.headerOffset + headerSize >= lo) headers.push({
218
+ key: s.group.key,
219
+ label: s.group.label,
220
+ count: s.group.count,
221
+ collapsed: s.group.collapsed,
222
+ offset: s.headerOffset,
223
+ size: headerSize
224
+ });
225
+ if (s.count === 0) continue;
226
+ const rel = off - s.itemsOffset;
227
+ const viewport = rel < 0 ? viewportMain + rel : viewportMain;
228
+ if (viewport <= 0) continue;
229
+ const sw = computeGridWindow(s.count, w, h, g, horizontal ? viewport : p.viewportWidth, horizontal ? p.viewportHeight : viewport, Math.max(0, rel), p.orientation, overscan);
230
+ if (sw.endIndex > sw.startIndex) ranges.push([s.first + sw.startIndex, s.first + sw.endIndex]);
231
+ }
232
+ return {
233
+ cols: horizontal ? count : cols,
234
+ rows: 0,
235
+ totalHeight: horizontal ? 0 : total,
236
+ totalWidth: horizontal ? total : 0,
237
+ startIndex: ranges.length > 0 ? ranges[0][0] : 0,
238
+ endIndex: ranges.length > 0 ? ranges[ranges.length - 1][1] : 0,
239
+ colStride,
240
+ rowStride,
241
+ horizontal,
242
+ headers,
243
+ ranges
244
+ };
245
+ };
246
+ const extentOf = (index) => {
247
+ const { line, section } = positionOf(index);
248
+ const s = sections[section];
249
+ const start = s.itemsOffset + line * stride;
250
+ return [line === 0 && s.group ? s.headerOffset : start, start + itemExtent];
251
+ };
252
+ const neighbour = (index, dLine) => {
253
+ if (count === 0) return index;
254
+ const si = sectionOfIndex(sections, index);
255
+ const s = sections[si];
256
+ const local = index - s.first;
257
+ const col = local % cols;
258
+ if (dLine > 0) {
259
+ const t = local + cols;
260
+ if (t <= s.count - 1) return s.first + t;
261
+ const lastLine = Math.floor((s.count - 1) / cols);
262
+ if (Math.floor(local / cols) < lastLine) return s.first + s.count - 1;
263
+ 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);
264
+ return s.first + s.count - 1;
265
+ }
266
+ const t = local - cols;
267
+ if (t >= 0) return s.first + t;
268
+ for (let n = si - 1; n >= 0; n--) {
269
+ const prev = sections[n];
270
+ if (prev.count > 0) {
271
+ const line = Math.floor((prev.count - 1) / cols);
272
+ return prev.first + Math.min(line * cols + col, prev.count - 1);
273
+ }
274
+ }
275
+ return s.first;
276
+ };
277
+ const hitTest = (x0, x1, y0, y1) => {
278
+ const out = [];
279
+ if (count === 0) return out;
280
+ const main0 = horizontal ? x0 : y0;
281
+ const main1 = horizontal ? x1 : y1;
282
+ const start = sectionAtOffset(sections, main0 - stride);
283
+ for (let si = Math.max(0, start); si < sections.length; si++) {
284
+ const s = sections[si];
285
+ if (s.itemsOffset > main1) break;
286
+ if (s.count === 0 || s.end < main0) continue;
287
+ if (horizontal) {
288
+ const a = Math.max(0, Math.floor((x0 - s.itemsOffset) / colStride));
289
+ const b = Math.min(s.count - 1, Math.floor((x1 - s.itemsOffset) / colStride));
290
+ for (let i = a; i <= b; i++) out.push(s.first + i);
291
+ } else {
292
+ const colLo = Math.max(0, Math.floor(x0 / colStride));
293
+ const colHi = Math.min(cols - 1, Math.floor(x1 / colStride));
294
+ const rowLo = Math.max(0, Math.floor((y0 - s.itemsOffset) / rowStride));
295
+ const rowHi = Math.floor((y1 - s.itemsOffset) / rowStride);
296
+ for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
297
+ const local = row * cols + col;
298
+ if (local >= 0 && local < s.count) out.push(s.first + local);
299
+ }
300
+ }
301
+ }
302
+ return out;
303
+ };
304
+ const anchorAt = (mainOffset) => {
305
+ if (count === 0) return {
306
+ index: 0,
307
+ lineFrac: 0
308
+ };
309
+ let si = sectionAtOffset(sections, mainOffset);
310
+ while (si < sections.length - 1 && sections[si].count === 0) si++;
311
+ const s = sections[si];
312
+ if (s.count === 0) return {
313
+ index: 0,
314
+ lineFrac: 0
315
+ };
316
+ const centreLine = Math.max(0, mainOffset - s.itemsOffset) / stride;
317
+ const lineFloor = Math.max(0, Math.floor(centreLine));
318
+ const local = horizontal ? Math.min(s.count - 1, lineFloor) : Math.min(s.count - 1, lineFloor * cols + Math.max(0, Math.floor((cols - 1) / 2)));
319
+ return {
320
+ index: s.first + local,
321
+ lineFrac: centreLine - lineFloor
322
+ };
323
+ };
324
+ const offsetOfAnchor = (index, lineFrac) => {
325
+ const { line, section } = positionOf(index);
326
+ return sections[section].itemsOffset + (line + lineFrac) * stride;
327
+ };
328
+ const snapTarget = (offset) => {
329
+ const si = sectionAtOffset(sections, offset);
330
+ const s = sections[si];
331
+ let rel = Math.round((offset - s.itemsOffset) / stride);
332
+ if (grouped) {
333
+ const lines = s.count === 0 ? 0 : horizontal ? s.count : Math.ceil(s.count / cols);
334
+ rel = Math.max(0, Math.min(Math.max(0, lines - 1), rel));
335
+ }
336
+ const candidates = [s.itemsOffset + rel * stride];
337
+ if (grouped) {
338
+ candidates.push(s.headerOffset);
339
+ if (si + 1 < sections.length) candidates.push(sections[si + 1].headerOffset);
340
+ }
341
+ return candidates.reduce((best, c) => Math.abs(c - offset) < Math.abs(best - offset) ? c : best);
342
+ };
343
+ return {
344
+ horizontal,
345
+ cols,
346
+ colStride,
347
+ rowStride,
348
+ grouped,
349
+ count,
350
+ totalExtent: total,
351
+ window,
352
+ positionOf,
353
+ extentOf,
354
+ neighbour,
355
+ hitTest,
356
+ anchorAt,
357
+ offsetOfAnchor,
358
+ snapTarget
359
+ };
360
+ }
361
+ //#endregion
18
362
  //#region src/grid-list/grid-list-source.ts
363
+ /** Reorder `effective` raw indices into contiguous groups, dropping collapsed ones. */
364
+ function applyGrouping(effective, cfg, keyOfRaw) {
365
+ const parts = partitionByGroup(effective, (raw) => cfg.groupKeyOf(raw), cfg.labelOf ? (key, raws) => cfg.labelOf(key, raws.map((r) => cfg.itemAt(r))) : void 0);
366
+ const order = [];
367
+ const groups = [];
368
+ const groupByKey = /* @__PURE__ */ new Map();
369
+ for (const part of parts) {
370
+ const collapsed = cfg.collapsed.has(part.key);
371
+ groups.push({
372
+ key: part.key,
373
+ label: part.label,
374
+ count: part.items.length,
375
+ collapsed
376
+ });
377
+ for (const raw of part.items) {
378
+ groupByKey.set(keyOfRaw(raw), part.key);
379
+ if (!collapsed) order.push(raw);
380
+ }
381
+ }
382
+ return {
383
+ order,
384
+ groups,
385
+ groupByKey
386
+ };
387
+ }
19
388
  const ITEM_SELECTOR$1 = `:scope > *`;
20
389
  const SYNTHETIC_PREFIX = `gl-auto-`;
21
390
  /** Whether `key` is an auto-assigned key for a keyless light-DOM item. */
@@ -42,6 +411,9 @@ var ElementGridListSource = class {
42
411
  #searchKeys;
43
412
  #effectiveIndices = [];
44
413
  #keysCache;
414
+ #grouping;
415
+ #groups = [];
416
+ #groupByKey = /* @__PURE__ */ new Map();
45
417
  constructor(host) {
46
418
  this.#host = host;
47
419
  this.refresh();
@@ -94,6 +466,16 @@ var ElementGridListSource = class {
94
466
  if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
95
467
  this.#recomputeEffective();
96
468
  }
469
+ setGrouping(config) {
470
+ this.#grouping = config;
471
+ this.#recomputeEffective();
472
+ }
473
+ groups() {
474
+ return this.#groups;
475
+ }
476
+ groupKeyOfKey(key) {
477
+ return this.#groupByKey.get(key) ?? null;
478
+ }
97
479
  refresh() {
98
480
  this.#items = Array.from(this.#host.querySelectorAll(ITEM_SELECTOR$1)).filter((el) => !el.hidden);
99
481
  for (const el of this.#items) {
@@ -136,9 +518,21 @@ var ElementGridListSource = class {
136
518
  }
137
519
  effective.push(i);
138
520
  }
139
- this.#effectiveIndices = effective;
521
+ this.#applyGroupingTo(effective);
140
522
  this.#keysCache = void 0;
141
523
  }
524
+ #applyGroupingTo(effective) {
525
+ if (this.#grouping === void 0) {
526
+ this.#effectiveIndices = effective;
527
+ this.#groups = [];
528
+ this.#groupByKey = /* @__PURE__ */ new Map();
529
+ return;
530
+ }
531
+ const grouped = applyGrouping(effective, this.#grouping, (raw) => this.#itemKey(this.#items[raw]));
532
+ this.#effectiveIndices = grouped.order;
533
+ this.#groups = grouped.groups;
534
+ this.#groupByKey = grouped.groupByKey;
535
+ }
142
536
  #itemKey(el) {
143
537
  const userKey = keyForElement(el);
144
538
  if (userKey !== void 0) return userKey;
@@ -161,6 +555,9 @@ var DataGridListSource = class {
161
555
  #searchKeys;
162
556
  #effectiveIndices = [];
163
557
  #keysCache;
558
+ #grouping;
559
+ #groups = [];
560
+ #groupByKey = /* @__PURE__ */ new Map();
164
561
  constructor(items, adapter) {
165
562
  this.#items = items;
166
563
  this.#adapter = adapter;
@@ -226,6 +623,16 @@ var DataGridListSource = class {
226
623
  if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
227
624
  this.#recomputeEffective();
228
625
  }
626
+ setGrouping(config) {
627
+ this.#grouping = config;
628
+ this.#recomputeEffective();
629
+ }
630
+ groups() {
631
+ return this.#groups;
632
+ }
633
+ groupKeyOfKey(key) {
634
+ return this.#groupByKey.get(key) ?? null;
635
+ }
229
636
  refresh() {
230
637
  this.#recomputeEffective();
231
638
  }
@@ -250,7 +657,16 @@ var DataGridListSource = class {
250
657
  if (this.#searchKeys !== void 0 && !this.#searchKeys.has(this.#adapter.key(this.#items[i], i))) continue;
251
658
  effective.push(i);
252
659
  }
253
- this.#effectiveIndices = effective;
660
+ if (this.#grouping === void 0) {
661
+ this.#effectiveIndices = effective;
662
+ this.#groups = [];
663
+ this.#groupByKey = /* @__PURE__ */ new Map();
664
+ } else {
665
+ const grouped = applyGrouping(effective, this.#grouping, (raw) => this.#adapter.key(this.#items[raw], raw));
666
+ this.#effectiveIndices = grouped.order;
667
+ this.#groups = grouped.groups;
668
+ this.#groupByKey = grouped.groupByKey;
669
+ }
254
670
  this.#keysCache = void 0;
255
671
  }
256
672
  };
@@ -266,76 +682,11 @@ const SNAP_DEBOUNCE_MS = 120;
266
682
  * to finish. Kept a touch above the default `--grid-list-item-move-duration`.
267
683
  */
268
684
  const REFLOW_WINDOW_MS = 320;
269
- function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
270
- const w = Math.max(1, itemWidth);
271
- const h = Math.max(1, rowHeight);
272
- const g = Math.max(0, gap);
273
- const colStride = w + g;
274
- const rowStride = h + g;
275
- const offset = Math.max(0, scrollOffset);
276
- if (orientation === `row`) {
277
- const rows = count === 0 ? 0 : 1;
278
- const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
279
- if (count === 0 || viewportWidth <= 0) return {
280
- cols: count,
281
- rows,
282
- totalHeight: 0,
283
- totalWidth,
284
- startIndex: 0,
285
- endIndex: 0,
286
- colStride,
287
- rowStride,
288
- horizontal: true
289
- };
290
- const firstVisible = Math.max(0, Math.floor(offset / colStride));
291
- const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
292
- return {
293
- cols: count,
294
- rows,
295
- totalHeight: 0,
296
- totalWidth,
297
- startIndex: Math.max(0, firstVisible - overscan),
298
- endIndex: Math.min(count, lastVisible + overscan),
299
- colStride,
300
- rowStride,
301
- horizontal: true
302
- };
303
- }
304
- const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
305
- const rows = count === 0 ? 0 : Math.ceil(count / cols);
306
- const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
307
- if (count === 0 || viewportHeight <= 0) return {
308
- cols,
309
- rows,
310
- totalHeight,
311
- totalWidth: 0,
312
- startIndex: 0,
313
- endIndex: 0,
314
- colStride,
315
- rowStride,
316
- horizontal: false
317
- };
318
- const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
319
- const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
320
- const rowStart = Math.max(0, firstVisibleRow - overscan);
321
- const rowEnd = Math.min(rows, lastVisibleRow + overscan);
322
- return {
323
- cols,
324
- rows,
325
- totalHeight,
326
- totalWidth: 0,
327
- startIndex: rowStart * cols,
328
- endIndex: Math.min(count, rowEnd * cols),
329
- colStride,
330
- rowStride,
331
- horizontal: false
332
- };
685
+ function readProperty(item, name) {
686
+ return typeof item === `object` && item !== null ? item[name] : void 0;
333
687
  }
334
- function columnOf(index, cols) {
335
- return cols <= 1 ? 0 : index % cols;
336
- }
337
- function rowOf(index, cols) {
338
- return cols <= 1 ? index : Math.floor(index / cols);
688
+ function readAttribute(el, name) {
689
+ return el.getAttribute(`data-${name}`);
339
690
  }
340
691
  /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
341
692
  function sameKeys(a, b) {
@@ -363,6 +714,11 @@ let GridListElement = class GridListElement extends LitElement {
363
714
  this.marqueeFromCellBlank = false;
364
715
  this.snapToItem = false;
365
716
  this.items = [];
717
+ this.groupBy = null;
718
+ this.crossGroupSelection = false;
719
+ this.groupHeaderHeight = 28;
720
+ this.groupHeaderWidth = 120;
721
+ this._collapsedGroups = /* @__PURE__ */ new Set();
366
722
  this._scrollTop = 0;
367
723
  this._scrollLeft = 0;
368
724
  this._viewportHeight = 0;
@@ -387,6 +743,12 @@ let GridListElement = class GridListElement extends LitElement {
387
743
  interactionMode: this.interactionMode
388
744
  }),
389
745
  getKey: (k) => k,
746
+ getGroupKey: (k) => this.groupBy === null ? null : this.#source.groupKeyOfKey(k),
747
+ crossGroupSelection: () => this.crossGroupSelection,
748
+ getFallbackGroupKey: () => {
749
+ const k = this.tickled.tickledId;
750
+ return k !== void 0 && this.groupBy !== null ? this.#source.groupKeyOfKey(k) : null;
751
+ },
390
752
  onChange: (selected, previous) => {
391
753
  this._syncItemStates();
392
754
  this.requestUpdate();
@@ -484,10 +846,11 @@ let GridListElement = class GridListElement extends LitElement {
484
846
  },
485
847
  shouldStart: (event) => {
486
848
  if (this.marqueeFromCellBlank && this.#isCellBlankPress(event)) return true;
487
- for (const node of event.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return false;
849
+ for (const node of event.composedPath()) if (node instanceof Element && (node.classList.contains(`cell`) || node.classList.contains(`group-header`))) return false;
488
850
  return true;
489
851
  }
490
852
  });
853
+ this.#groupByChanged = false;
491
854
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
492
855
  this.#onScroll = (e) => {
493
856
  const el = e.target;
@@ -537,15 +900,25 @@ let GridListElement = class GridListElement extends LitElement {
537
900
  const count = this.#source.count;
538
901
  if (count === 0) return;
539
902
  const win = this.#currentWindow();
540
- const move = (delta) => {
541
- const from = this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
542
- const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
903
+ const layout = this.#layout();
904
+ const current = () => this.tickled.tickledId ? this.#source.indexOfKey(this.tickled.tickledId) : -1;
905
+ const goto = (nextIndex) => {
543
906
  const nextKey = this.#source.keyAt(nextIndex);
544
907
  if (nextKey) {
545
908
  this.tickled.handleKeyboardNavigation(nextKey);
546
909
  this.#scrollItemIntoView(nextKey);
547
910
  }
548
911
  };
912
+ const move = (delta) => goto(Math.max(0, Math.min(count - 1, current() + delta)));
913
+ const moveLines = (dir, lines = 1) => {
914
+ let index = current();
915
+ if (index < 0) {
916
+ move(dir * lines * win.cols);
917
+ return;
918
+ }
919
+ for (let i = 0; i < lines; i++) index = layout.neighbour(index, dir);
920
+ goto(index);
921
+ };
549
922
  const perLine = this.orientation === `grid` ? win.cols : 1;
550
923
  switch (e.key) {
551
924
  case `ArrowLeft`:
@@ -555,10 +928,12 @@ let GridListElement = class GridListElement extends LitElement {
555
928
  move(1);
556
929
  break;
557
930
  case `ArrowUp`:
558
- move(-perLine);
931
+ if (this.orientation === `grid`) moveLines(-1);
932
+ else move(-perLine);
559
933
  break;
560
934
  case `ArrowDown`:
561
- move(perLine);
935
+ if (this.orientation === `grid`) moveLines(1);
936
+ else move(perLine);
562
937
  break;
563
938
  case `Home`:
564
939
  this.tickled.handleKeyboardNavigation(`first`);
@@ -573,7 +948,8 @@ let GridListElement = class GridListElement extends LitElement {
573
948
  const mainExtent = win.horizontal ? this._viewportWidth : this._viewportHeight;
574
949
  const mainStride = win.horizontal ? win.colStride : win.rowStride;
575
950
  const visibleLines = Math.max(1, Math.floor(mainExtent / mainStride));
576
- move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
951
+ if (this.orientation === `grid`) moveLines(e.key === `PageDown` ? 1 : -1, visibleLines);
952
+ else move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
577
953
  break;
578
954
  }
579
955
  case `Enter`: {
@@ -654,6 +1030,11 @@ let GridListElement = class GridListElement extends LitElement {
654
1030
  position: relative;
655
1031
  width: 100%;
656
1032
  }
1033
+ .group-header {
1034
+ position: absolute;
1035
+ inset: 0 auto auto 0;
1036
+ z-index: 1;
1037
+ }
657
1038
  .cell {
658
1039
  position: absolute;
659
1040
  inset: 0 auto auto 0;
@@ -947,6 +1328,9 @@ let GridListElement = class GridListElement extends LitElement {
947
1328
  specificity. Survivors slide to their new slot; a freshly inserted cell
948
1329
  also fades in. (A removed item's element is already gone, so there's
949
1330
  nothing left to fade — the gap simply closes as the others slide.) */
1331
+ :host([reflowing]) .group-header {
1332
+ transition: transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1);
1333
+ }
950
1334
  :host([reflowing]) .cell {
951
1335
  transition:
952
1336
  transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1),
@@ -957,6 +1341,7 @@ let GridListElement = class GridListElement extends LitElement {
957
1341
  animation: ixfx-list-item-in var(--grid-list-item-move-duration, 200ms) ease both;
958
1342
  }
959
1343
  @media (prefers-reduced-motion: reduce) {
1344
+ :host([reflowing]) .group-header { transition: none; }
960
1345
  :host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
961
1346
  .cell.entering { animation: none; }
962
1347
  }
@@ -1098,31 +1483,13 @@ let GridListElement = class GridListElement extends LitElement {
1098
1483
  * so virtualized off-screen items are covered without DOM measurement.
1099
1484
  */
1100
1485
  #marqueeHitTest(rect) {
1101
- const count = this.#source.count;
1102
- if (count === 0) return [];
1486
+ if (this.#source.count === 0) return [];
1103
1487
  const horizontal = this.orientation === `row`;
1104
- 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);
1105
- const out = [];
1106
- if (horizontal) {
1107
- const x0 = rect.x + this._scrollLeft;
1108
- const x1 = x0 + rect.width;
1109
- const lo = Math.max(0, Math.floor(x0 / win.colStride));
1110
- const hi = Math.min(count - 1, Math.floor(x1 / win.colStride));
1111
- for (let i = lo; i <= hi; i++) out.push(i);
1112
- } else {
1113
- const x0 = rect.x + this._scrollLeft;
1114
- const x1 = x0 + rect.width;
1115
- const y0 = rect.y + this._scrollTop;
1116
- const y1 = y0 + rect.height;
1117
- const colLo = Math.max(0, Math.floor(x0 / win.colStride));
1118
- const colHi = Math.min(win.cols - 1, Math.floor(x1 / win.colStride));
1119
- const rowLo = Math.max(0, Math.floor(y0 / win.rowStride));
1120
- const rowHi = Math.floor(y1 / win.rowStride);
1121
- for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
1122
- const index = row * win.cols + col;
1123
- if (index >= 0 && index < count) out.push(index);
1124
- }
1125
- }
1488
+ const x0 = rect.x + this._scrollLeft;
1489
+ const x1 = x0 + rect.width;
1490
+ const y0 = horizontal ? 0 : rect.y + this._scrollTop;
1491
+ const y1 = horizontal ? 0 : y0 + rect.height;
1492
+ const out = this.#layout().hitTest(x0, x1, y0, y1);
1126
1493
  const keys = [];
1127
1494
  for (const index of out) {
1128
1495
  const key = this.#source.keyAt(index);
@@ -1272,6 +1639,10 @@ let GridListElement = class GridListElement extends LitElement {
1272
1639
  if (previousLength === 0 && removedCount === 0) return;
1273
1640
  if (added.length > 0 && added.length === this.#items.length) return;
1274
1641
  for (const el of added) this.#entering.add(el);
1642
+ this.#openReflowWindow();
1643
+ }
1644
+ /** Open (or extend) the `[reflowing]` window so moved cells and headers slide. */
1645
+ #openReflowWindow() {
1275
1646
  this.toggleAttribute(`reflowing`, true);
1276
1647
  if (this.#reflowTimer) clearTimeout(this.#reflowTimer);
1277
1648
  this.#reflowTimer = window.setTimeout(() => {
@@ -1328,6 +1699,7 @@ let GridListElement = class GridListElement extends LitElement {
1328
1699
  this.#sourceElement = new ElementGridListSource(this);
1329
1700
  this.#source = this.#sourceElement;
1330
1701
  }
1702
+ this.#source.setGrouping(this.#groupingConfig());
1331
1703
  this.#applyFilters();
1332
1704
  if (this.#searchFilterKeys) this.#source.setFilters({ searchKeys: this.#searchFilterKeys });
1333
1705
  }
@@ -1369,6 +1741,17 @@ let GridListElement = class GridListElement extends LitElement {
1369
1741
  }
1370
1742
  this.search?.refresh();
1371
1743
  }
1744
+ const groupsChanged = changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`_collapsedGroups`);
1745
+ if (groupsChanged && this.hasUpdated) {
1746
+ if (changed.has(`groupBy`)) {
1747
+ if (this._collapsedGroups.size > 0) this._collapsedGroups = /* @__PURE__ */ new Set();
1748
+ this.#groupByChanged = true;
1749
+ }
1750
+ this.#applyGroupingToSource();
1751
+ this.#dropUnreachableCursor();
1752
+ this.#openReflowWindow();
1753
+ this.updateComplete.then(() => this.#emitViewportChange());
1754
+ } else if (groupsChanged) this.#applyGroupingToSource();
1372
1755
  if (changed.has(`itemSize`) || changed.has(`gap`)) {
1373
1756
  this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
1374
1757
  this.gap = Math.max(0, Math.round(this.gap));
@@ -1389,6 +1772,62 @@ let GridListElement = class GridListElement extends LitElement {
1389
1772
  }
1390
1773
  }
1391
1774
  }
1775
+ #groupByChanged;
1776
+ /** Grouping config for the active source, or `undefined` when ungrouped. */
1777
+ #groupingConfig() {
1778
+ const by = this.groupBy;
1779
+ if (by === null || by === void 0 || by === ``) return void 0;
1780
+ const collapsed = this._collapsedGroups;
1781
+ const src = this.#source;
1782
+ if (src instanceof DataGridListSource) return {
1783
+ groupKeyOf: (raw) => resolveGroupKey(by, src.rawItemAt(raw), raw, readProperty),
1784
+ itemAt: (raw) => src.rawItemAt(raw),
1785
+ labelOf: this.groupLabel,
1786
+ collapsed
1787
+ };
1788
+ const elements = src;
1789
+ return {
1790
+ groupKeyOf: (raw) => resolveGroupKey(by, elements.rawElementAt(raw), raw, readAttribute),
1791
+ itemAt: (raw) => elements.rawElementAt(raw),
1792
+ labelOf: this.groupLabel,
1793
+ collapsed
1794
+ };
1795
+ }
1796
+ /** Push the current grouping into the source and refresh the element-mode item cache. */
1797
+ #applyGroupingToSource() {
1798
+ this.#source.setGrouping(this.#groupingConfig());
1799
+ if (this.#source instanceof ElementGridListSource) this.#items = this.#source.effectiveElements();
1800
+ }
1801
+ /** The cursor can't stay on an item that a collapsed group just removed. */
1802
+ #dropUnreachableCursor() {
1803
+ const id = this.tickled.tickledId;
1804
+ if (id !== void 0 && this.#source.indexOfKey(id) < 0) this.tickled.focus(void 0);
1805
+ }
1806
+ /** Groups in display order (empty when ungrouped). */
1807
+ get groups() {
1808
+ return this.groupBy === null ? [] : this.#source.groups();
1809
+ }
1810
+ /** Collapse or expand one group. Fires `list-group-toggle` when the state changes. */
1811
+ setGroupCollapsed(groupKey, collapsed) {
1812
+ if (this._collapsedGroups.has(groupKey) === collapsed) return;
1813
+ const next = new Set(this._collapsedGroups);
1814
+ if (collapsed) next.add(groupKey);
1815
+ else next.delete(groupKey);
1816
+ this._collapsedGroups = next;
1817
+ this._dispatch(`list-group-toggle`, {
1818
+ groupKey,
1819
+ collapsed
1820
+ });
1821
+ }
1822
+ toggleGroup(groupKey) {
1823
+ this.setGroupCollapsed(groupKey, !this._collapsedGroups.has(groupKey));
1824
+ }
1825
+ collapseAllGroups() {
1826
+ for (const g of this.groups) this.setGroupCollapsed(g.key, true);
1827
+ }
1828
+ expandAllGroups() {
1829
+ for (const key of [...this._collapsedGroups]) this.setGroupCollapsed(key, false);
1830
+ }
1392
1831
  #syncAriaMultiselectable() {
1393
1832
  this.setAttribute(`aria-multiselectable`, this.selection.canSelectMultiple() ? `true` : `false`);
1394
1833
  }
@@ -1428,6 +1867,10 @@ let GridListElement = class GridListElement extends LitElement {
1428
1867
  }
1429
1868
  this.#applyZoomAnchor();
1430
1869
  this.#activateTickledPan();
1870
+ if (this.#groupByChanged) {
1871
+ this.#groupByChanged = false;
1872
+ this._dispatch(`list-group-change`, { groupBy: this.groupBy });
1873
+ }
1431
1874
  }
1432
1875
  /** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
1433
1876
  #applyZoomAnchor() {
@@ -1436,41 +1879,33 @@ let GridListElement = class GridListElement extends LitElement {
1436
1879
  this.#zoomAnchor = void 0;
1437
1880
  const vp = this.#viewportRef.value;
1438
1881
  if (!vp) return;
1439
- const win = this.#currentWindow();
1440
- if (win.horizontal) {
1441
- if (this._viewportWidth <= 0) return;
1442
- const targetCenter = (index + lineFrac) * win.colStride;
1443
- const maxScroll = Math.max(0, win.totalWidth - this._viewportWidth);
1444
- const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportWidth / 2));
1882
+ const layout = this.#layout();
1883
+ const horizontal = layout.horizontal;
1884
+ const extent = horizontal ? this._viewportWidth : this._viewportHeight;
1885
+ if (extent <= 0) return;
1886
+ const targetCenter = layout.offsetOfAnchor(index, lineFrac);
1887
+ const maxScroll = Math.max(0, layout.totalExtent - extent);
1888
+ const next = Math.min(maxScroll, Math.max(0, targetCenter - extent / 2));
1889
+ if (horizontal) {
1445
1890
  if (Math.abs(vp.scrollLeft - next) > .5) {
1446
1891
  vp.scrollLeft = next;
1447
1892
  this._scrollLeft = next;
1448
1893
  }
1449
- return;
1450
- }
1451
- if (this._viewportHeight <= 0) return;
1452
- const targetCenter = ((win.cols > 0 ? Math.floor(index / win.cols) : 0) + lineFrac) * win.rowStride;
1453
- const maxScroll = Math.max(0, win.totalHeight - this._viewportHeight);
1454
- const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportHeight / 2));
1455
- if (Math.abs(vp.scrollTop - next) > .5) {
1894
+ } else if (Math.abs(vp.scrollTop - next) > .5) {
1456
1895
  vp.scrollTop = next;
1457
1896
  this._scrollTop = next;
1458
1897
  }
1459
1898
  }
1460
1899
  #captureCentreAnchor(prevOffset, prevExtent, prevItemSize, prevGap, prevCaptionStyle, prevCaptionHeight, prevOrientation) {
1461
- const count = this.#source?.count ?? 0;
1462
- if (count === 0) return;
1900
+ if ((this.#source?.count ?? 0) === 0) return;
1463
1901
  const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
1464
- const horizontal = prevOrientation === `row`;
1465
- const prev = computeGridWindow(count, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
1466
- const mainStride = horizontal ? prev.colStride : prev.rowStride;
1467
- const centerLine = (prevOffset + prevExtent / 2) / mainStride;
1468
- const lineFloor = Math.max(0, Math.floor(centerLine));
1469
- const index = horizontal ? Math.min(count - 1, lineFloor) : Math.min(count - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
1470
- this.#zoomAnchor = {
1471
- index,
1472
- lineFrac: centerLine - lineFloor
1473
- };
1902
+ const prev = this.#buildLayout({
1903
+ itemWidth: prevItemSize,
1904
+ rowHeight: prevRowHeight,
1905
+ gap: prevGap,
1906
+ orientation: prevOrientation
1907
+ });
1908
+ this.#zoomAnchor = prev.anchorAt(prevOffset + prevExtent / 2);
1474
1909
  }
1475
1910
  /**
1476
1911
  * Re-read the viewport after an ancestor's visibility/layout changed.
@@ -1684,37 +2119,75 @@ let GridListElement = class GridListElement extends LitElement {
1684
2119
  return this.#windowKeys(this.#currentWindow(true));
1685
2120
  }
1686
2121
  #windowKeys(win) {
1687
- return this.#source.keys().slice(win.startIndex, win.endIndex).filter((k) => !isSyntheticKey(k));
2122
+ const keys = this.#source.keys();
2123
+ const out = [];
2124
+ for (const [start, end] of win.ranges) for (let i = start; i < end; i++) if (!isSyntheticKey(keys[i])) out.push(keys[i]);
2125
+ return out;
2126
+ }
2127
+ /** Layout params for the current geometry, with optional overrides (used for zoom anchoring). */
2128
+ #layoutParams(over = {}) {
2129
+ return {
2130
+ count: this.#source.count,
2131
+ groups: this.groupBy === null ? void 0 : this.#source.groups(),
2132
+ itemWidth: this.itemSize,
2133
+ rowHeight: this.#rowHeight(),
2134
+ gap: this.gap,
2135
+ viewportWidth: this._viewportWidth,
2136
+ viewportHeight: this._viewportHeight,
2137
+ orientation: this.orientation,
2138
+ headerHeight: this.groupHeaderHeight,
2139
+ headerWidth: this.groupHeaderWidth,
2140
+ ...over
2141
+ };
2142
+ }
2143
+ #buildLayout(over = {}) {
2144
+ return buildLayout(this.#layoutParams(over));
2145
+ }
2146
+ #layoutCache;
2147
+ /** The current layout, rebuilt only when its inputs change (never per scroll). */
2148
+ #layout() {
2149
+ const params = this.#layoutParams();
2150
+ const cached = this.#layoutCache;
2151
+ if (cached) {
2152
+ const a = cached.params;
2153
+ let same = a.groups === params.groups;
2154
+ for (const k of Object.keys(params)) {
2155
+ if (!same) break;
2156
+ same = a[k] === params[k];
2157
+ }
2158
+ if (same) return cached.layout;
2159
+ }
2160
+ const layout = buildLayout(params);
2161
+ this.#layoutCache = {
2162
+ params,
2163
+ layout
2164
+ };
2165
+ return layout;
1688
2166
  }
1689
2167
  #currentWindow(noOverscan = false) {
1690
- const overscan = noOverscan ? 0 : 2;
1691
- 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);
2168
+ return this.#layout().window(this.orientation === `row` ? this._scrollLeft : this._scrollTop, noOverscan ? 0 : 2);
1692
2169
  }
1693
2170
  /** Adjust the scroll offset so `key` is fully within the viewport. */
1694
2171
  #scrollItemIntoView(key) {
1695
2172
  const index = this.#source.indexOfKey(key);
1696
2173
  const vp = this.#viewportRef.value;
1697
2174
  if (index < 0 || !vp) return;
1698
- const win = this.#currentWindow();
1699
- if (win.horizontal) {
1700
- const left = index * win.colStride;
1701
- const right = left + this.itemSize;
2175
+ const [start, end] = this.#layout().extentOf(index);
2176
+ if (this.orientation === `row`) {
1702
2177
  const viewLeft = vp.scrollLeft;
1703
2178
  let next = viewLeft;
1704
- if (left < viewLeft) next = left;
1705
- else if (right > viewLeft + this._viewportWidth) next = right - this._viewportWidth;
2179
+ if (start < viewLeft) next = start;
2180
+ else if (end > viewLeft + this._viewportWidth) next = end - this._viewportWidth;
1706
2181
  if (next !== viewLeft) {
1707
2182
  vp.scrollLeft = next;
1708
2183
  this._scrollLeft = next;
1709
2184
  }
1710
2185
  return;
1711
2186
  }
1712
- const top = (win.cols > 0 ? Math.floor(index / win.cols) : 0) * win.rowStride;
1713
- const bottom = top + this.#rowHeight();
1714
2187
  const viewTop = vp.scrollTop;
1715
2188
  let next = viewTop;
1716
- if (top < viewTop) next = top;
1717
- else if (bottom > viewTop + this._viewportHeight) next = bottom - this._viewportHeight;
2189
+ if (start < viewTop) next = start;
2190
+ else if (end > viewTop + this._viewportHeight) next = end - this._viewportHeight;
1718
2191
  if (next !== viewTop) {
1719
2192
  vp.scrollTop = next;
1720
2193
  this._scrollTop = next;
@@ -1788,6 +2261,7 @@ let GridListElement = class GridListElement extends LitElement {
1788
2261
  >
1789
2262
  <div class="spacer" style=${spacerStyle}>
1790
2263
  ${this.#source instanceof DataGridListSource ? this.#renderDataCells(win) : this.#renderElementCells(win)}
2264
+ ${this.#renderHeaders(win)}
1791
2265
  </div>
1792
2266
  ${this.#marqueeRect ? html`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
1793
2267
  </div>
@@ -1795,7 +2269,7 @@ let GridListElement = class GridListElement extends LitElement {
1795
2269
  `;
1796
2270
  }
1797
2271
  #renderDataCells(win) {
1798
- const slice = Array.from({ length: win.endIndex - win.startIndex }, (_, i) => win.startIndex + i);
2272
+ const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
1799
2273
  return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderDataCell(index, win));
1800
2274
  }
1801
2275
  #renderDataCell(index, win) {
@@ -1810,8 +2284,7 @@ let GridListElement = class GridListElement extends LitElement {
1810
2284
  const caption = style === `hidden` ? `` : this.#source.labelAt(index);
1811
2285
  const showCaption = caption !== ``;
1812
2286
  const cellHeight = win.rowStride - this.gap;
1813
- const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
1814
- const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
2287
+ const { x, y } = this.#layout().positionOf(index);
1815
2288
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
1816
2289
  const rawIcon = this.#source.iconAt(index);
1817
2290
  const icon = appearanceAbsent(rawIcon) ? void 0 : rawIcon;
@@ -1897,8 +2370,25 @@ let GridListElement = class GridListElement extends LitElement {
1897
2370
  current.startLoop?.();
1898
2371
  }
1899
2372
  #renderElementCells(win) {
1900
- const slice = this.#items.slice(win.startIndex, win.endIndex);
1901
- return repeat(slice, (_el, i) => this.#source.keyAt(win.startIndex + i), (el, i) => this.#renderCell(el, win.startIndex + i, win));
2373
+ const slice = win.ranges.flatMap(([start, end]) => Array.from({ length: end - start }, (_, i) => start + i));
2374
+ return repeat(slice, (index) => this.#source.keyAt(index), (index) => this.#renderCell(this.#items[index], index, win));
2375
+ }
2376
+ #renderHeaders(win) {
2377
+ if (win.headers.length === 0) return nothing;
2378
+ const horizontal = win.horizontal;
2379
+ return repeat(win.headers, (h) => `__group__${h.key}`, (h) => html`
2380
+ <ixfx-list-group-header
2381
+ class="group-header"
2382
+ part="group-header"
2383
+ data-group-key=${h.key}
2384
+ label=${h.label}
2385
+ count=${h.count}
2386
+ orientation=${horizontal ? `vertical` : `horizontal`}
2387
+ ?collapsed=${h.collapsed}
2388
+ 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)`}
2389
+ @click=${() => this.toggleGroup(h.key)}
2390
+ ></ixfx-list-group-header>
2391
+ `);
1902
2392
  }
1903
2393
  #renderCell(el, index, win) {
1904
2394
  const key = this.#source.keyAt(index);
@@ -1913,8 +2403,7 @@ let GridListElement = class GridListElement extends LitElement {
1913
2403
  const caption = style === `hidden` ? `` : this.#captionFor(el);
1914
2404
  const showCaption = caption !== ``;
1915
2405
  const cellHeight = win.rowStride - this.gap;
1916
- const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
1917
- const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
2406
+ const { x, y } = this.#layout().positionOf(index);
1918
2407
  const thumbStyle = separate ? `height:${this.itemSize}px` : ``;
1919
2408
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
1920
2409
  return html`
@@ -2013,16 +2502,12 @@ let GridListElement = class GridListElement extends LitElement {
2013
2502
  #snapToNearestLine() {
2014
2503
  const vp = this.#viewportRef.value;
2015
2504
  if (!vp) return;
2016
- const win = this.#currentWindow();
2017
- const horizontal = win.horizontal;
2018
- const stride = horizontal ? win.colStride : win.rowStride;
2019
- if (stride <= 0) return;
2505
+ const layout = this.#layout();
2506
+ const horizontal = layout.horizontal;
2020
2507
  const offset = horizontal ? vp.scrollLeft : vp.scrollTop;
2021
- const extent = horizontal ? win.totalWidth : win.totalHeight;
2022
2508
  const viewportExtent = horizontal ? this._viewportWidth : this._viewportHeight;
2023
- const maxScroll = Math.max(0, extent - viewportExtent);
2024
- const index = Math.round(offset / stride);
2025
- const target = Math.min(maxScroll, Math.max(0, index * stride));
2509
+ const maxScroll = Math.max(0, layout.totalExtent - viewportExtent);
2510
+ const target = Math.min(maxScroll, Math.max(0, layout.snapTarget(offset)));
2026
2511
  if (Math.abs(target - offset) < .5) return;
2027
2512
  vp.scrollTo(horizontal ? {
2028
2513
  left: target,
@@ -2050,7 +2535,7 @@ let GridListElement = class GridListElement extends LitElement {
2050
2535
  if (mode === `manual`) return;
2051
2536
  if (this.selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
2052
2537
  if (this.#dragSelect.isDragging || this.#nativeDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
2053
- for (const node of e.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return;
2538
+ for (const node of e.composedPath()) if (node instanceof Element && (node.classList.contains(`cell`) || node.classList.contains(`group-header`))) return;
2054
2539
  this.selection.clear();
2055
2540
  }
2056
2541
  #startPostDragSuppressTimer() {
@@ -2151,6 +2636,22 @@ __decorate([property({
2151
2636
  })], GridListElement.prototype, "snapToItem", void 0);
2152
2637
  __decorate([property({ attribute: false })], GridListElement.prototype, "items", void 0);
2153
2638
  __decorate([property({ attribute: false })], GridListElement.prototype, "itemAdapter", void 0);
2639
+ __decorate([property({ attribute: `group-by` })], GridListElement.prototype, "groupBy", void 0);
2640
+ __decorate([property({ attribute: false })], GridListElement.prototype, "groupLabel", void 0);
2641
+ __decorate([property({
2642
+ type: Boolean,
2643
+ attribute: `cross-group-selection`,
2644
+ reflect: true
2645
+ })], GridListElement.prototype, "crossGroupSelection", void 0);
2646
+ __decorate([property({
2647
+ type: Number,
2648
+ attribute: `group-header-height`
2649
+ })], GridListElement.prototype, "groupHeaderHeight", void 0);
2650
+ __decorate([property({
2651
+ type: Number,
2652
+ attribute: `group-header-width`
2653
+ })], GridListElement.prototype, "groupHeaderWidth", void 0);
2654
+ __decorate([state()], GridListElement.prototype, "_collapsedGroups", void 0);
2154
2655
  __decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
2155
2656
  __decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
2156
2657
  __decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
@@ -2159,4 +2660,4 @@ GridListElement = __decorate([safeCustomElement(`ixfx-grid-list`)], GridListElem
2159
2660
  //#endregion
2160
2661
  export { GridListElement as t };
2161
2662
 
2162
- //# sourceMappingURL=grid-list-3fGhdaFA.js.map
2663
+ //# sourceMappingURL=grid-list-Bwaf61is.js.map