@ixfx/components 0.15.4 → 0.16.0

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 (101) hide show
  1. package/bundle/index.d.ts +1223 -48
  2. package/bundle/index.js +875 -700
  3. package/dist/ac-text.d.ts +1 -1
  4. package/dist/{ac-token-Cq7lsYf-.d.ts → ac-token-qEs3FsQv.d.ts} +1 -1
  5. package/dist/ac-token.d.ts +1 -1
  6. package/dist/colour-picker.d.ts +1 -1
  7. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  8. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  9. package/dist/crumbs.d.ts +2 -115
  10. package/dist/crumbs.js +1 -1179
  11. package/dist/{data-grid-CbiubdG2.d.ts → data-grid-CIG2DVip.d.ts} +58 -9
  12. package/dist/{data-grid-Dx3l3dxw.js → data-grid-rMkLX2qo.js} +630 -215
  13. package/dist/data-grid.d.ts +3 -3
  14. package/dist/data-grid.js +1 -1
  15. package/dist/{detail-list-ClkMV0SN.d.ts → detail-list-6iMTNP4N.d.ts} +35 -5
  16. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  17. package/dist/detail-list.d.ts +4 -4
  18. package/dist/detail-list.js +1 -1
  19. package/dist/{flat-list-0xTZdXzh.d.ts → flat-list-JPgQSmLW.d.ts} +28 -5
  20. package/dist/flat-list.d.ts +2 -2
  21. package/dist/flat-list.js +123 -4
  22. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  23. package/dist/{grid-list-BnWJUIBC.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  24. package/dist/grid-list.d.ts +3 -3
  25. package/dist/grid-list.js +1 -1
  26. package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +1 -1
  27. package/dist/grouped-item-lister.d.ts +1 -1
  28. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +1 -1
  29. package/dist/icons.d.ts +2 -2
  30. package/dist/incr-search.d.ts +1 -1
  31. package/dist/index-9ttcdwlL.d.ts +71 -0
  32. package/dist/{index-BAuqR9El.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  33. package/dist/index-CYRc6Dah.d.ts +475 -0
  34. package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +1 -1
  35. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +1 -1
  36. package/dist/{index-Bz3loIAf.d.ts → index-DN2BWctP.d.ts} +1 -1
  37. package/dist/{index-BPTuCRGb.d.ts → index-Dr80Zdl6.d.ts} +2 -2
  38. package/dist/{index-cnaRPQPp.d.ts → index-DxyyVqzA.d.ts} +2 -2
  39. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +1 -1
  40. package/dist/index.d.ts +380 -33
  41. package/dist/index.js +2247 -866
  42. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +1 -1
  43. package/dist/labelled-radial-input.d.ts +2 -2
  44. package/dist/labelled-range-input.d.ts +1 -1
  45. package/dist/labelled-slider-input.d.ts +1 -1
  46. package/dist/list-group-header.d.ts +1 -1
  47. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  48. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  49. package/dist/{menu-item-B0zqw2xp.d.ts → menu-item-bF6narIz.d.ts} +1 -1
  50. package/dist/menu.d.ts +2 -2
  51. package/dist/miller-BFRfn0ve.js +2237 -0
  52. package/dist/miller.d.ts +4 -437
  53. package/dist/miller.js +1 -1815
  54. package/dist/panel.d.ts +1 -1
  55. package/dist/pending-BGidHppL.js +91 -0
  56. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  57. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-CophkNeU.d.ts} +42 -4
  58. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  59. package/dist/split-layout.d.ts +1 -1
  60. package/dist/split-layout.js +1 -1
  61. package/dist/sunburst-tree.d.ts +2 -0
  62. package/dist/sunburst-tree.js +2 -0
  63. package/dist/sunbursttree-list-B_VxOHlo.js +1556 -0
  64. package/dist/sunbursttree-list-D6j5BxVQ.d.ts +303 -0
  65. package/dist/{tab-list-wC0xNz1b.d.ts → tab-list-Df371rqw.d.ts} +1 -1
  66. package/dist/tabs.d.ts +1 -1
  67. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +1 -1
  68. package/dist/titlebar.d.ts +1 -1
  69. package/dist/tree-CftHwtEb.js +146 -0
  70. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  71. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  72. package/dist/tree.d.ts +3 -3
  73. package/dist/tree.js +1 -1
  74. package/dist/types-LU1u0CB8.d.ts +87 -0
  75. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  76. package/dist/{vertical-list-B8FoU-pU.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  77. package/dist/vertical-list.d.ts +3 -3
  78. package/dist/vertical-list.js +1 -1789
  79. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  80. package/docs-user/README.md +3 -0
  81. package/docs-user/crumbs.md +27 -0
  82. package/docs-user/data-grid.md +77 -5
  83. package/docs-user/detail-list.md +54 -0
  84. package/docs-user/dock.md +54 -12
  85. package/docs-user/flat-list.md +27 -0
  86. package/docs-user/grid-list.md +64 -11
  87. package/docs-user/hyperbolic-tree.md +31 -4
  88. package/docs-user/icicle-tree.md +36 -0
  89. package/docs-user/index.json +17 -2
  90. package/docs-user/llms.txt +3 -0
  91. package/docs-user/miller.md +52 -21
  92. package/docs-user/multicol-list.md +50 -8
  93. package/docs-user/split-layout.md +45 -17
  94. package/docs-user/sunburst-tree.md +223 -0
  95. package/docs-user/tree.md +46 -7
  96. package/docs-user/treemap.md +32 -0
  97. package/docs-user/twosplit-layout.md +22 -0
  98. package/docs-user/user-catalog.md +1 -1
  99. package/docs-user/vertical-list.md +55 -7
  100. package/llms.txt +3 -0
  101. package/package.json +5 -1
@@ -6,7 +6,9 @@ import "./icon-1gpwtPx8.js";
6
6
  import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES } from "./incr-search-JIhwB49y.js";
7
7
  import { t as handleStyles } from "./handle-DAXs7kbJ.js";
8
8
  import { a as listItemFadeKeyframes, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
9
+ import { a as capKeys, l as keysToArray, n as EMPTY_KEYS, o as flagsFromRecord, r as MAX_PENDING_KEYS } from "./pending-BGidHppL.js";
9
10
  import "./interaction-E-uMGHkJ.js";
11
+ import { n as anchorFromRow, o as scrollToOffset, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
10
12
  import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
11
13
  import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
12
14
  import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
@@ -16,6 +18,191 @@ import { LitElement, css, html, nothing } from "lit";
16
18
  import { property, query, state } from "lit/decorators.js";
17
19
  import { createCommandRegistry } from "@clinth/ui-commands";
18
20
  import { repeat } from "lit/directives/repeat.js";
21
+ //#region src/data-grid/data-grid-view-state.ts
22
+ const EMPTY_COLUMNS = /* @__PURE__ */ new Map();
23
+ const EMPTY_ORDER = /* @__PURE__ */ new Map();
24
+ /**
25
+ * Selected keys whose row is gone go pending (deselected silently); pending
26
+ * keys whose row now exists are selected unless `canRestore` is false (e.g.
27
+ * `selectionMode="none"`), in which case they stay pending. With `single`, at
28
+ * most one key stays selected (a restored key wins) and nothing more stays
29
+ * pending.
30
+ */
31
+ function reconcileSelectedKeys(selected, pending, has, single, canRestore = true) {
32
+ if (selected.size === 0 && pending.size === 0) return {
33
+ selected,
34
+ pending,
35
+ restored: false,
36
+ changed: false
37
+ };
38
+ let next;
39
+ let nextPending;
40
+ for (const key of selected) {
41
+ if (has(key)) continue;
42
+ next ??= new Set(selected);
43
+ next.delete(key);
44
+ nextPending ??= new Set(pending);
45
+ nextPending.add(key);
46
+ }
47
+ const present = canRestore ? [...pending].filter(has) : [];
48
+ const resolved = resolvePendingKeys(next ?? selected, nextPending ?? pending, present, single);
49
+ return {
50
+ ...resolved,
51
+ changed: resolved.changed || next !== void 0
52
+ };
53
+ }
54
+ /**
55
+ * Select the `present` pending keys (already known to exist). Used on its own
56
+ * for streamed appends, where only the new slice can match — so the cost is
57
+ * proportional to the slice, not the row count.
58
+ */
59
+ function resolvePendingKeys(selected, pending, present, single) {
60
+ if (present.length === 0) return {
61
+ selected,
62
+ pending: capKeys(pending),
63
+ restored: false,
64
+ changed: false
65
+ };
66
+ const next = new Set(selected);
67
+ const nextPending = new Set(pending);
68
+ for (const key of present) {
69
+ next.add(key);
70
+ nextPending.delete(key);
71
+ }
72
+ if (single) return {
73
+ selected: /* @__PURE__ */ new Set([present.at(-1)]),
74
+ pending: EMPTY_KEYS,
75
+ restored: true,
76
+ changed: true
77
+ };
78
+ return {
79
+ selected: next,
80
+ pending: capKeys(nextPending),
81
+ restored: true,
82
+ changed: true
83
+ };
84
+ }
85
+ /** Pending keys that appear in `sliceKeys` — iterates the slice, not the pending set. */
86
+ function matchSlice(pending, sliceKeys) {
87
+ if (pending.size === 0) return [];
88
+ return sliceKeys.filter((k) => pending.has(k));
89
+ }
90
+ /** Collapsed group keys as an `expanded` record (`false` = collapsed). */
91
+ function collapsedToRecord(collapsed, include = () => true) {
92
+ const out = {};
93
+ for (const key of collapsed) if (include(key)) out[key] = false;
94
+ return out;
95
+ }
96
+ /** `expanded` record → collapsed group keys. Groups default open, so only `false` matters. */
97
+ function collapsedFromRecord(record) {
98
+ const keys = [...flagsFromRecord(record)].filter(([, open]) => !open).map(([k]) => k);
99
+ return new Set(capKeys(new Set(keys)));
100
+ }
101
+ /** Parse a saved `columns` record into pending layout and a saved order. Invalid values are dropped. */
102
+ function columnStateFromRecord(record) {
103
+ const pending = /* @__PURE__ */ new Map();
104
+ const order = /* @__PURE__ */ new Map();
105
+ for (const [key, s] of Object.entries(record)) {
106
+ if (s === null || typeof s !== `object`) continue;
107
+ const width = typeof s.width === `number` && Number.isFinite(s.width) && s.width > 0 ? s.width : void 0;
108
+ const visible = typeof s.visible === `boolean` ? s.visible : void 0;
109
+ if (width !== void 0 || visible !== void 0) pending.set(key, {
110
+ ...width === void 0 ? {} : { width },
111
+ ...visible === void 0 ? {} : { visible }
112
+ });
113
+ if (typeof s.order === `number` && Number.isFinite(s.order)) order.set(key, s.order);
114
+ }
115
+ return {
116
+ pending,
117
+ order
118
+ };
119
+ }
120
+ /**
121
+ * - columns in `prev` but not in `columns` → their width / visibility go pending
122
+ * - pending layout for columns now defined → applied (on a copy of the column)
123
+ * - a saved `order` reorders the columns it names within the slots they
124
+ * occupy, leaving other columns where they are; it is dropped once every
125
+ * column it names is defined (so the host can reorder freely afterwards)
126
+ *
127
+ * Returns the input array when nothing changed, so `columns` identity (a sort
128
+ * input) only changes when it must.
129
+ */
130
+ function reconcileColumns(prev, columns, pending, order) {
131
+ const keys = new Set(columns.map((c) => c.key));
132
+ let nextPending;
133
+ for (const col of prev) {
134
+ if (keys.has(col.key) || pending.has(col.key)) continue;
135
+ nextPending ??= new Map(pending);
136
+ nextPending.set(col.key, layoutOf(col));
137
+ }
138
+ let next;
139
+ columns.forEach((col, i) => {
140
+ const saved = (nextPending ?? pending).get(col.key);
141
+ if (!saved) return;
142
+ nextPending ??= new Map(pending);
143
+ nextPending.delete(col.key);
144
+ if ((saved.width === void 0 || saved.width === col.width) && (saved.visible === void 0 || saved.visible === (col.visible !== false))) return;
145
+ next ??= [...columns];
146
+ next[i] = {
147
+ ...col,
148
+ ...saved
149
+ };
150
+ });
151
+ let nextOrder = order;
152
+ if (order.size > 0) {
153
+ const working = next ?? columns;
154
+ const slots = working.flatMap((c, i) => order.has(c.key) ? [i] : []);
155
+ const ranked = slots.map((i) => working[i]).sort((a, b) => order.get(a.key) - order.get(b.key));
156
+ if (ranked.some((c, j) => c !== working[slots[j]])) {
157
+ next ??= [...columns];
158
+ slots.forEach((slot, j) => {
159
+ next[slot] = ranked[j];
160
+ });
161
+ }
162
+ if ([...order.keys()].every((k) => keys.has(k))) nextOrder = EMPTY_ORDER;
163
+ }
164
+ return {
165
+ columns: next ?? columns,
166
+ pending: nextPending ? capMap(nextPending) : pending,
167
+ order: nextOrder,
168
+ changed: next !== void 0 || nextPending !== void 0 || nextOrder !== order
169
+ };
170
+ }
171
+ /** Defined columns (and, with `pending`, saved layout for undefined ones) as a JSON record. */
172
+ function captureColumns(columns, pending, order, includePending) {
173
+ const out = {};
174
+ if (includePending) {
175
+ for (const [key, layout] of pending) out[key] = withOrder(layout, order.get(key));
176
+ for (const [key, rank] of order) if (!(key in out) && !columns.some((c) => c.key === key)) out[key] = { order: rank };
177
+ }
178
+ columns.forEach((col, i) => {
179
+ out[col.key] = withOrder(layoutOf(col), order.size > 0 ? order.get(col.key) : i);
180
+ });
181
+ return out;
182
+ }
183
+ /** Whether `value` is a usable sort state (`null` = unsorted). */
184
+ function isSortState(value) {
185
+ if (value === null) return true;
186
+ if (typeof value !== `object`) return false;
187
+ const s = value;
188
+ return typeof s.key === `string` && (s.direction === `asc` || s.direction === `desc`);
189
+ }
190
+ function layoutOf(col) {
191
+ return {
192
+ ...col.width === void 0 ? {} : { width: col.width },
193
+ visible: col.visible !== false
194
+ };
195
+ }
196
+ function withOrder(layout, order) {
197
+ return order === void 0 ? { ...layout } : {
198
+ ...layout,
199
+ order
200
+ };
201
+ }
202
+ function capMap(map, max = MAX_PENDING_KEYS) {
203
+ return map.size <= max ? map : new Map([...map].slice(map.size - max));
204
+ }
205
+ //#endregion
19
206
  //#region src/data-grid/shift-wheel.ts
20
207
  /**
21
208
  * Shift + vertical mouse wheel → horizontal scroll.
@@ -72,12 +259,20 @@ const EMPTY_CELL_LOOK = {
72
259
  iconColor: void 0,
73
260
  badge: void 0
74
261
  };
262
+ /**
263
+ * Default `getRowKey`: `row.key`, then `row.id`, else the row's sorted index.
264
+ * Index keys aren't stable, so view state never records them.
265
+ */
266
+ const defaultGetRowKey = (r, i) => r.key ?? r.id ?? String(i);
75
267
  /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
76
268
  function sameKeys(a, b) {
77
269
  if (a === void 0 || a.length !== b.length) return false;
78
270
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
79
271
  return true;
80
272
  }
273
+ function sameSet(a, b) {
274
+ return a.size === b.size && [...a].every((k) => b.has(k));
275
+ }
81
276
  /** Element-wise reference equality, treating `undefined` (never computed) as different. */
82
277
  function sameRefs(a, b) {
83
278
  return a !== void 0 && a.length === b.length && a.every((v, i) => v === b[i]);
@@ -91,7 +286,7 @@ let DataGridElement = class DataGridElement extends LitElement {
91
286
  this.interactionMode = `standard`;
92
287
  this.sortState = null;
93
288
  this.rowHeight = 28;
94
- this.getRowKey = (r, i) => r.key ?? r.id ?? String(i);
289
+ this.getRowKey = defaultGetRowKey;
95
290
  this.editable = false;
96
291
  this.editability = `user`;
97
292
  this.cellOverflow = `ellipsis`;
@@ -185,6 +380,15 @@ let DataGridElement = class DataGridElement extends LitElement {
185
380
  this.#viewportChangeRaf = 0;
186
381
  this.#resortRaf = 0;
187
382
  this._drag = null;
383
+ this.#viewState = new ViewStateController(this, {
384
+ kind: `data-grid`,
385
+ capture: () => this.getViewState()
386
+ });
387
+ this.#pendingSelected = EMPTY_KEYS;
388
+ this.#pendingColumns = EMPTY_COLUMNS;
389
+ this.#columnOrder = EMPTY_ORDER;
390
+ this.#reconciledColumns = [];
391
+ this.#groupKeys = /* @__PURE__ */ new Set();
188
392
  this._onFocusOut = (e) => {
189
393
  if (this._suppressBlurCommit) return;
190
394
  if (this.editability === `immediate`) return;
@@ -204,180 +408,18 @@ let DataGridElement = class DataGridElement extends LitElement {
204
408
  this._scrollLeft = el.scrollLeft;
205
409
  const after = this.#windowRows(2);
206
410
  if (before.start !== after.start || before.end !== after.end) this.requestUpdate();
411
+ if (!this.#viewState.pendingScroll) this.#viewState.notify();
207
412
  cancelAnimationFrame(this.#viewportChangeRaf);
208
413
  this.#viewportChangeRaf = 0;
209
414
  this.#emitViewportChangeIfChanged();
210
415
  });
211
416
  };
212
417
  this._onKeyDown = (e) => {
213
- if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === `a`) {
214
- if (this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode` || this.interactionMode === `standard`) {
215
- e.preventDefault();
216
- const prev = new Set(this._selectedKeys);
217
- if (this.groupBy && !this.crossGroupSelection && this._anchorKey) {
218
- const g = this._getGroupKey(this._getRowForKey(this._anchorKey) ?? { key: this._anchorKey });
219
- this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row` && this._getGroupKey(f.row) === g).map((f) => f.key));
220
- } else this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
221
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
222
- detail: {
223
- selectedKeys: new Set(this._selectedKeys),
224
- previous: prev
225
- },
226
- bubbles: true,
227
- composed: true
228
- }));
229
- }
230
- return;
231
- }
232
- if (this._editing && (e.key === `ArrowDown` || e.key === `ArrowUp`)) {
233
- if ((this.shadowRoot?.activeElement)?.tagName === `SELECT`) return;
234
- e.preventDefault();
235
- const cur = this._editing;
236
- const dir = e.key === `ArrowDown` ? 1 : -1;
237
- const colKey = cur.colKey;
238
- this._suppressBlurCommit = true;
239
- this._commitEdit();
240
- const rowKeys = this._flat.filter((f) => f.type === `row`).map((f) => f.key);
241
- const idx = rowKeys.indexOf(cur.rowKey);
242
- let nextIdx = idx + dir;
243
- while (nextIdx >= 0 && nextIdx < rowKeys.length) {
244
- const nk = rowKeys[nextIdx];
245
- const row = this.#rowEntry(nk)?.row;
246
- const col = this._visibleColumns.find((c) => c.key === colKey);
247
- if (row && col && this._isEditable(row, col)) {
248
- this._startEdit(nk, colKey);
249
- this._tickled.handleKeyboardNavigation(nextIdx > idx ? `next` : `previous`);
250
- this._scrollRowIntoView(nk);
251
- break;
252
- }
253
- nextIdx += dir;
254
- }
255
- if (nextIdx < 0 || nextIdx >= rowKeys.length) {
256
- this._tickled.handleKeyboardNavigation(dir > 0 ? `next` : `previous`);
257
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
258
- }
259
- requestAnimationFrame(() => {
260
- this._suppressBlurCommit = false;
261
- });
262
- return;
263
- }
264
- if (e.key === `ArrowDown` || e.key === `ArrowUp` || e.key === `Home` || e.key === `End`) {
265
- e.preventDefault();
266
- this._tickled.setInputMode(`keyboard`);
267
- }
268
- switch (e.key) {
269
- case `ArrowDown`:
270
- this._tickled.handleKeyboardNavigation(`next`);
271
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
272
- if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
273
- if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
274
- break;
275
- case `ArrowUp`:
276
- this._tickled.handleKeyboardNavigation(`previous`);
277
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
278
- if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
279
- if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
280
- break;
281
- case `Home`:
282
- this._tickled.handleKeyboardNavigation(`first`);
283
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
284
- break;
285
- case `End`:
286
- this._tickled.handleKeyboardNavigation(`last`);
287
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
288
- break;
289
- case `Enter`: {
290
- const key = this._tickled.tickledId;
291
- if (!key) break;
292
- e.preventDefault();
293
- if (!this._editing && this.editability === `user`) {
294
- const entry = this.#rowEntry(key);
295
- if (entry) {
296
- const row = entry.row;
297
- if (this._lastClickedCell?.rowKey === key) {
298
- const col = this._visibleColumns.find((c) => c.key === this._lastClickedCell.colKey);
299
- if (col && this._isEditable(row, col)) {
300
- this._startEdit(key, col.key);
301
- break;
302
- }
303
- }
304
- const firstEditable = this._visibleColumns.find((c) => this._isEditable(row, c));
305
- if (firstEditable) {
306
- this._startEdit(key, firstEditable.key);
307
- break;
308
- }
309
- }
310
- }
311
- if (!this._editing) {
312
- const entry = this.#rowEntry(key);
313
- const idx = entry?.idx ?? -1;
314
- if (this._selectedKeys.has(key)) {
315
- this._activateRow(key, entry?.row, idx);
316
- break;
317
- }
318
- if (this.interactionMode !== `manual` && this.interactionMode !== `checked`) this._replaceSelection(key);
319
- this.dispatchEvent(new CustomEvent(`datagrid-cell-click`, {
320
- detail: {
321
- rowKey: key,
322
- colKey: ``,
323
- rowIndex: idx,
324
- colIndex: -1
325
- },
326
- bubbles: true,
327
- composed: true
328
- }));
329
- }
330
- break;
331
- }
332
- case ` `: {
333
- if (this._editing) {
334
- if ((this.columns.find((c) => c.key === this._editing.colKey)?.dataType ?? `text`) === `boolean`) {
335
- e.preventDefault();
336
- const isImmediate = this.editability === `immediate`;
337
- const rowKey = this._editing.rowKey;
338
- const colKey = this._editing.colKey;
339
- const active = this.shadowRoot?.activeElement;
340
- let next;
341
- if (active?.type === `checkbox`) {
342
- next = !active.checked;
343
- active.checked = next;
344
- } else {
345
- const row = this.#rowEntry(rowKey)?.row;
346
- next = !(this._editValue !== `` ? Boolean(this._editValue) : Boolean(row?.[colKey]));
347
- }
348
- if (isImmediate) this._commitImmediate(rowKey, colKey, next);
349
- else {
350
- this._editValue = next;
351
- this._commitEdit();
352
- }
353
- }
354
- break;
355
- }
356
- const key = this._tickled.tickledId;
357
- if (!key) break;
358
- e.preventDefault();
359
- if (this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `standard` || this.interactionMode === `vscode` || this.interactionMode === `manual`) this._toggleSelection(key);
360
- else this._replaceSelection(key);
361
- break;
362
- }
363
- case `Escape`: if (this._editing || this._editingRow) {
364
- e.preventDefault();
365
- this._editing = null;
366
- this._editingRow = null;
367
- this.requestUpdate();
368
- } else if (this._selectedKeys.size) {
369
- e.preventDefault();
370
- const prev = new Set(this._selectedKeys);
371
- this._selectedKeys = /* @__PURE__ */ new Set();
372
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
373
- detail: {
374
- selectedKeys: /* @__PURE__ */ new Set(),
375
- previous: prev
376
- },
377
- bubbles: true,
378
- composed: true
379
- }));
380
- }
418
+ const before = this._tickled.tickledId;
419
+ this.#handleKeyDown(e);
420
+ if (this._tickled.tickledId !== before) {
421
+ this.#pendingFocus = void 0;
422
+ this.#viewState.notify();
381
423
  }
382
424
  };
383
425
  this._suppressBlurCommit = false;
@@ -711,6 +753,21 @@ let DataGridElement = class DataGridElement extends LitElement {
711
753
  #lastViewportVisibleKeys;
712
754
  #resortRaf;
713
755
  #detachShiftWheel;
756
+ #viewState;
757
+ /** Saved selection for row keys not (yet) in `rows`. */
758
+ #pendingSelected;
759
+ #pendingFocus;
760
+ /** Saved width / visibility for column keys not (yet) in `columns`. */
761
+ #pendingColumns;
762
+ /** Saved column order, kept until every column it names is defined. */
763
+ #columnOrder;
764
+ /** `columns` as of the last column reconcile, to spot removed columns. */
765
+ #reconciledColumns;
766
+ /** Every row by key, built lazily (only when a key isn't in `_flat`); reset on resort. */
767
+ #rowIndex;
768
+ #rowIndexKeyFn;
769
+ /** Group keys in the current flat model. */
770
+ #groupKeys;
714
771
  connectedCallback() {
715
772
  super.connectedCallback();
716
773
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -754,6 +811,7 @@ let DataGridElement = class DataGridElement extends LitElement {
754
811
  this.#viewportResizeObserver = new ResizeObserver(() => this.#measureViewport());
755
812
  this.#viewportResizeObserver.observe(this._viewportEl);
756
813
  this.#measureViewport();
814
+ this.#viewState.observeScrollTarget(this._viewportEl);
757
815
  }
758
816
  /**
759
817
  * Re-read the viewport's size and update `_viewportH`/`_viewportW` if it
@@ -788,6 +846,7 @@ let DataGridElement = class DataGridElement extends LitElement {
788
846
  }
789
847
  updated(changed) {
790
848
  super.updated(changed);
849
+ this.#viewState.applyPendingScroll((s) => this.#applyScroll(s));
791
850
  if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`filterPredicate`) || changed.has(`rowHeight`)) this.#scheduleViewportChangeEmit();
792
851
  if (changed.has(`_editing`) || changed.has(`_editingRow`)) this.updateComplete.then(() => {
793
852
  let sel = null;
@@ -803,7 +862,8 @@ let DataGridElement = class DataGridElement extends LitElement {
803
862
  super.willUpdate(changed);
804
863
  this.setAttribute(`data-input-mode`, this._tickled.inputMode);
805
864
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
806
- this.#syncDerivedRows();
865
+ if (changed.has(`columns`)) this.#reconcileColumns();
866
+ if (this.#syncDerivedRows()) this.#reconcileRows();
807
867
  this._visibleColumns = this.columns.filter((c) => c.visible !== false);
808
868
  let off = 0;
809
869
  this._colOffsets = this._visibleColumns.map((c) => {
@@ -840,11 +900,14 @@ let DataGridElement = class DataGridElement extends LitElement {
840
900
  if (resort) {
841
901
  this.#sortInputs = sortInputs;
842
902
  this._sortedRows = this._applySort([...this.rows]);
903
+ this.#rowIndex = void 0;
843
904
  }
844
905
  if (resort || !sameRefs(this.#flatInputs, flatInputs)) {
845
906
  this.#flatInputs = flatInputs;
846
907
  this._rebuildFlat();
908
+ return true;
847
909
  }
910
+ return false;
848
911
  }
849
912
  /** Mark the derived rows as matching the current inputs (after an incremental update). */
850
913
  #markDerivedRowsSynced() {
@@ -904,6 +967,7 @@ let DataGridElement = class DataGridElement extends LitElement {
904
967
  for (let i = 0; i < this._sortedRows.length; i++) if (!pred || pred(this._sortedRows[i])) sourceIdx.push(i);
905
968
  const flat = [];
906
969
  const keys = [];
970
+ this.#groupKeys = /* @__PURE__ */ new Set();
907
971
  const pushRow = (idx) => {
908
972
  const row = this._sortedRows[idx];
909
973
  const key = this.getRowKey(row, idx);
@@ -924,6 +988,7 @@ let DataGridElement = class DataGridElement extends LitElement {
924
988
  if (arr) arr.push(idx);
925
989
  else groups.set(k, [idx]);
926
990
  }
991
+ this.#groupKeys = new Set(groups.keys());
927
992
  for (const [gKey, idxs] of groups) {
928
993
  const label = this.groupLabel ? this.groupLabel(gKey, idxs.map((i) => this._sortedRows[i])) : gKey;
929
994
  const gId = `__group__${gKey}`;
@@ -963,6 +1028,7 @@ let DataGridElement = class DataGridElement extends LitElement {
963
1028
  if (next.has(gKey)) next.delete(gKey);
964
1029
  else next.add(gKey);
965
1030
  this._collapsedGroups = next;
1031
+ this.#viewState.notify();
966
1032
  }
967
1033
  _applySort(rows) {
968
1034
  if (!this.sortState) return rows;
@@ -1001,6 +1067,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1001
1067
  };
1002
1068
  else next = null;
1003
1069
  this.sortState = next;
1070
+ this.#viewState.notify();
1004
1071
  this.dispatchEvent(new CustomEvent(`datagrid-sort`, {
1005
1072
  detail: {
1006
1073
  sort: next,
@@ -1071,6 +1138,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1071
1138
  _onVisibilityToggle(col, checked) {
1072
1139
  col.visible = checked;
1073
1140
  this.columns = [...this.columns];
1141
+ this.#viewState.notify();
1074
1142
  this.dispatchEvent(new CustomEvent(`datagrid-column-visibility`, {
1075
1143
  detail: {
1076
1144
  key: col.key,
@@ -1175,6 +1243,8 @@ let DataGridElement = class DataGridElement extends LitElement {
1175
1243
  const [moved] = vis.splice(fromIdx, 1);
1176
1244
  vis.splice(toIdx, 0, moved);
1177
1245
  this.columns = [...vis, ...hidden];
1246
+ this.#columnOrder = EMPTY_ORDER;
1247
+ this.#viewState.notify();
1178
1248
  this.dispatchEvent(new CustomEvent(`datagrid-column-reorder`, {
1179
1249
  detail: {
1180
1250
  from: fromIdx,
@@ -1222,10 +1292,10 @@ let DataGridElement = class DataGridElement extends LitElement {
1222
1292
  * set when the selection actually changes.
1223
1293
  */
1224
1294
  selectKeys(keys) {
1295
+ this.#pendingSelected = EMPTY_KEYS;
1225
1296
  let next = new Set([...keys].filter((k) => this._getRowForKey(k) !== void 0));
1226
- const multi = this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
1227
1297
  if (this.selectionMode === `none`) next = /* @__PURE__ */ new Set();
1228
- else if (!multi && next.size > 1) {
1298
+ else if (!this.#isMultiSelect() && next.size > 1) {
1229
1299
  const last = [...next].at(-1);
1230
1300
  next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
1231
1301
  }
@@ -1233,10 +1303,18 @@ let DataGridElement = class DataGridElement extends LitElement {
1233
1303
  if (prev.size === next.size && [...next].every((k) => prev.has(k))) return;
1234
1304
  this._selectedKeys = next;
1235
1305
  this._anchorKey = [...next].at(-1);
1306
+ this.#dispatchSelect(prev);
1307
+ }
1308
+ #isMultiSelect() {
1309
+ return this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
1310
+ }
1311
+ #dispatchSelect(previous) {
1312
+ if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
1313
+ this.#viewState.notify();
1236
1314
  this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1237
1315
  detail: {
1238
- selectedKeys: new Set(next),
1239
- previous: prev
1316
+ selectedKeys: new Set(this._selectedKeys),
1317
+ previous
1240
1318
  },
1241
1319
  bubbles: true,
1242
1320
  composed: true
@@ -1251,6 +1329,8 @@ let DataGridElement = class DataGridElement extends LitElement {
1251
1329
  if (!this.#flatIndexByKey.has(key) || key.startsWith(`__group__`)) return;
1252
1330
  this._tickled.focus(key);
1253
1331
  this._scrollRowIntoView(key);
1332
+ this.#pendingFocus = void 0;
1333
+ this.#viewState.notify();
1254
1334
  }
1255
1335
  /**
1256
1336
  * Append rows without the full O(n log n) rebuild that reassigning `rows`
@@ -1273,6 +1353,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1273
1353
  this._sortedRows = [...this._sortedRows, ...appended];
1274
1354
  this.#appendToFlat(appended, startIdx);
1275
1355
  this.#markDerivedRowsSynced();
1356
+ this.#resolveAppended(appended, startIdx);
1276
1357
  if (this.sortState != null) {
1277
1358
  cancelAnimationFrame(this.#resortRaf);
1278
1359
  this.#resortRaf = requestAnimationFrame(() => {
@@ -1311,14 +1392,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1311
1392
  this._selectedKeys = /* @__PURE__ */ new Set([key]);
1312
1393
  } else this._selectedKeys = /* @__PURE__ */ new Set([key]);
1313
1394
  this._anchorKey = key;
1314
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1315
- detail: {
1316
- selectedKeys: new Set(this._selectedKeys),
1317
- previous: prev
1318
- },
1319
- bubbles: true,
1320
- composed: true
1321
- }));
1395
+ this.#dispatchSelect(prev);
1322
1396
  }
1323
1397
  _toggleSelection(key) {
1324
1398
  const prev = new Set(this._selectedKeys);
@@ -1334,14 +1408,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1334
1408
  }
1335
1409
  this._selectedKeys = next;
1336
1410
  this._anchorKey = key;
1337
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1338
- detail: {
1339
- selectedKeys: new Set(this._selectedKeys),
1340
- previous: prev
1341
- },
1342
- bubbles: true,
1343
- composed: true
1344
- }));
1411
+ this.#dispatchSelect(prev);
1345
1412
  }
1346
1413
  _rangeSelectTo(key) {
1347
1414
  if (!this._anchorKey) {
@@ -1372,28 +1439,171 @@ let DataGridElement = class DataGridElement extends LitElement {
1372
1439
  const hi = Math.max(ga, gb);
1373
1440
  const prev = new Set(this._selectedKeys);
1374
1441
  this._selectedKeys = new Set(groupKeys.slice(lo, hi + 1));
1375
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1376
- detail: {
1377
- selectedKeys: new Set(this._selectedKeys),
1378
- previous: prev
1379
- },
1380
- bubbles: true,
1381
- composed: true
1382
- }));
1442
+ this.#dispatchSelect(prev);
1383
1443
  return;
1384
1444
  }
1385
1445
  const lo = Math.min(a, b);
1386
1446
  const hi = Math.max(a, b);
1387
1447
  const prev = new Set(this._selectedKeys);
1388
1448
  this._selectedKeys = new Set(keys.slice(lo, hi + 1));
1389
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1390
- detail: {
1391
- selectedKeys: new Set(this._selectedKeys),
1392
- previous: prev
1393
- },
1394
- bubbles: true,
1395
- composed: true
1396
- }));
1449
+ this.#dispatchSelect(prev);
1450
+ }
1451
+ #handleKeyDown(e) {
1452
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === `a`) {
1453
+ if (this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode` || this.interactionMode === `standard`) {
1454
+ e.preventDefault();
1455
+ const prev = new Set(this._selectedKeys);
1456
+ if (this.groupBy && !this.crossGroupSelection && this._anchorKey) {
1457
+ const g = this._getGroupKey(this._getRowForKey(this._anchorKey) ?? { key: this._anchorKey });
1458
+ this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row` && this._getGroupKey(f.row) === g).map((f) => f.key));
1459
+ } else this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
1460
+ this.#dispatchSelect(prev);
1461
+ }
1462
+ return;
1463
+ }
1464
+ if (this._editing && (e.key === `ArrowDown` || e.key === `ArrowUp`)) {
1465
+ if ((this.shadowRoot?.activeElement)?.tagName === `SELECT`) return;
1466
+ e.preventDefault();
1467
+ const cur = this._editing;
1468
+ const dir = e.key === `ArrowDown` ? 1 : -1;
1469
+ const colKey = cur.colKey;
1470
+ this._suppressBlurCommit = true;
1471
+ this._commitEdit();
1472
+ const rowKeys = this._flat.filter((f) => f.type === `row`).map((f) => f.key);
1473
+ const idx = rowKeys.indexOf(cur.rowKey);
1474
+ let nextIdx = idx + dir;
1475
+ while (nextIdx >= 0 && nextIdx < rowKeys.length) {
1476
+ const nk = rowKeys[nextIdx];
1477
+ const row = this.#rowEntry(nk)?.row;
1478
+ const col = this._visibleColumns.find((c) => c.key === colKey);
1479
+ if (row && col && this._isEditable(row, col)) {
1480
+ this._startEdit(nk, colKey);
1481
+ this._tickled.handleKeyboardNavigation(nextIdx > idx ? `next` : `previous`);
1482
+ this._scrollRowIntoView(nk);
1483
+ break;
1484
+ }
1485
+ nextIdx += dir;
1486
+ }
1487
+ if (nextIdx < 0 || nextIdx >= rowKeys.length) {
1488
+ this._tickled.handleKeyboardNavigation(dir > 0 ? `next` : `previous`);
1489
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1490
+ }
1491
+ requestAnimationFrame(() => {
1492
+ this._suppressBlurCommit = false;
1493
+ });
1494
+ return;
1495
+ }
1496
+ if (e.key === `ArrowDown` || e.key === `ArrowUp` || e.key === `Home` || e.key === `End`) {
1497
+ e.preventDefault();
1498
+ this._tickled.setInputMode(`keyboard`);
1499
+ }
1500
+ switch (e.key) {
1501
+ case `ArrowDown`:
1502
+ this._tickled.handleKeyboardNavigation(`next`);
1503
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1504
+ if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
1505
+ if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
1506
+ break;
1507
+ case `ArrowUp`:
1508
+ this._tickled.handleKeyboardNavigation(`previous`);
1509
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1510
+ if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
1511
+ if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
1512
+ break;
1513
+ case `Home`:
1514
+ this._tickled.handleKeyboardNavigation(`first`);
1515
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1516
+ break;
1517
+ case `End`:
1518
+ this._tickled.handleKeyboardNavigation(`last`);
1519
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1520
+ break;
1521
+ case `Enter`: {
1522
+ const key = this._tickled.tickledId;
1523
+ if (!key) break;
1524
+ e.preventDefault();
1525
+ if (!this._editing && this.editability === `user`) {
1526
+ const entry = this.#rowEntry(key);
1527
+ if (entry) {
1528
+ const row = entry.row;
1529
+ if (this._lastClickedCell?.rowKey === key) {
1530
+ const col = this._visibleColumns.find((c) => c.key === this._lastClickedCell.colKey);
1531
+ if (col && this._isEditable(row, col)) {
1532
+ this._startEdit(key, col.key);
1533
+ break;
1534
+ }
1535
+ }
1536
+ const firstEditable = this._visibleColumns.find((c) => this._isEditable(row, c));
1537
+ if (firstEditable) {
1538
+ this._startEdit(key, firstEditable.key);
1539
+ break;
1540
+ }
1541
+ }
1542
+ }
1543
+ if (!this._editing) {
1544
+ const entry = this.#rowEntry(key);
1545
+ const idx = entry?.idx ?? -1;
1546
+ if (this._selectedKeys.has(key)) {
1547
+ this._activateRow(key, entry?.row, idx);
1548
+ break;
1549
+ }
1550
+ if (this.interactionMode !== `manual` && this.interactionMode !== `checked`) this._replaceSelection(key);
1551
+ this.dispatchEvent(new CustomEvent(`datagrid-cell-click`, {
1552
+ detail: {
1553
+ rowKey: key,
1554
+ colKey: ``,
1555
+ rowIndex: idx,
1556
+ colIndex: -1
1557
+ },
1558
+ bubbles: true,
1559
+ composed: true
1560
+ }));
1561
+ }
1562
+ break;
1563
+ }
1564
+ case ` `: {
1565
+ if (this._editing) {
1566
+ if ((this.columns.find((c) => c.key === this._editing.colKey)?.dataType ?? `text`) === `boolean`) {
1567
+ e.preventDefault();
1568
+ const isImmediate = this.editability === `immediate`;
1569
+ const rowKey = this._editing.rowKey;
1570
+ const colKey = this._editing.colKey;
1571
+ const active = this.shadowRoot?.activeElement;
1572
+ let next;
1573
+ if (active?.type === `checkbox`) {
1574
+ next = !active.checked;
1575
+ active.checked = next;
1576
+ } else {
1577
+ const row = this.#rowEntry(rowKey)?.row;
1578
+ next = !(this._editValue !== `` ? Boolean(this._editValue) : Boolean(row?.[colKey]));
1579
+ }
1580
+ if (isImmediate) this._commitImmediate(rowKey, colKey, next);
1581
+ else {
1582
+ this._editValue = next;
1583
+ this._commitEdit();
1584
+ }
1585
+ }
1586
+ break;
1587
+ }
1588
+ const key = this._tickled.tickledId;
1589
+ if (!key) break;
1590
+ e.preventDefault();
1591
+ if (this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `standard` || this.interactionMode === `vscode` || this.interactionMode === `manual`) this._toggleSelection(key);
1592
+ else this._replaceSelection(key);
1593
+ break;
1594
+ }
1595
+ case `Escape`: if (this._editing || this._editingRow) {
1596
+ e.preventDefault();
1597
+ this._editing = null;
1598
+ this._editingRow = null;
1599
+ this.requestUpdate();
1600
+ } else if (this._selectedKeys.size) {
1601
+ e.preventDefault();
1602
+ const prev = new Set(this._selectedKeys);
1603
+ this._selectedKeys = /* @__PURE__ */ new Set();
1604
+ this.#dispatchSelect(prev);
1605
+ }
1606
+ }
1397
1607
  }
1398
1608
  _isEditable(row, col) {
1399
1609
  if (this.editability === `disabled`) return false;
@@ -1589,11 +1799,16 @@ let DataGridElement = class DataGridElement extends LitElement {
1589
1799
  ctx.font = font;
1590
1800
  return Math.ceil(ctx.measureText(text).width);
1591
1801
  };
1802
+ const extra = col.fixedExtraWidth ?? 0;
1592
1803
  let maxW = measure(col.label) + headerExtra;
1593
1804
  for (const r of this.rows) {
1805
+ if (col.measure) {
1806
+ maxW = Math.max(maxW, col.measure(r, col) + cellPad);
1807
+ continue;
1808
+ }
1594
1809
  const v = r[col.key];
1595
1810
  if (v === null || v === void 0 || v === ``) continue;
1596
- const w = measure(String(this._formatValue(v, col, r))) + cellPad;
1811
+ const w = measure(String(this._formatValue(v, col, r))) + cellPad + extra;
1597
1812
  if (w > maxW) maxW = w;
1598
1813
  }
1599
1814
  const min = col.minWidth ?? 40;
@@ -1604,6 +1819,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1604
1819
  const w = this._measureAutoWidth(col);
1605
1820
  col.width = w;
1606
1821
  this.requestUpdate();
1822
+ this.#viewState.notify();
1607
1823
  this.dispatchEvent(new CustomEvent(`datagrid-column-resize`, {
1608
1824
  detail: {
1609
1825
  key: col.key,
@@ -1616,20 +1832,22 @@ let DataGridElement = class DataGridElement extends LitElement {
1616
1832
  /**
1617
1833
  * Resize a single column, by key, to fit its contents (header label + every
1618
1834
  * row's formatted value), animating the width change smoothly. Only works
1619
- * for plain columns - one with a custom `renderer` can't be measured
1620
- * reliably from its raw value, so it's left untouched. Returns `false` (and
1621
- * does nothing) if the column doesn't exist, isn't resizable, or has a
1622
- * `renderer`.
1835
+ * for plain columns, or `renderer` columns that supply `measure` or
1836
+ * `fixedExtraWidth` - otherwise a renderer's output can't be measured
1837
+ * reliably from the raw value, so it's left untouched. Returns `false` (and
1838
+ * does nothing) if the column doesn't exist, isn't resizable, or has an
1839
+ * unmeasurable `renderer`.
1623
1840
  */
1624
1841
  autoSizeColumn(key) {
1625
1842
  const col = this.columns.find((c) => c.key === key);
1626
- if (!col || col.resizable === false || col.renderer) return false;
1843
+ if (!col || col.resizable === false || col.renderer && !col.measure && col.fixedExtraWidth === void 0) return false;
1627
1844
  const target = this._measureAutoWidth(col);
1628
1845
  if ((col.width ?? 150) === target) return true;
1629
1846
  this._animatingColKeys = new Set(this._animatingColKeys).add(key);
1630
1847
  requestAnimationFrame(() => {
1631
1848
  col.width = target;
1632
1849
  this.requestUpdate();
1850
+ this.#viewState.notify();
1633
1851
  this.dispatchEvent(new CustomEvent(`datagrid-column-resize`, {
1634
1852
  detail: {
1635
1853
  key,
@@ -1666,6 +1884,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1666
1884
  window.removeEventListener(`pointerup`, onUp);
1667
1885
  this._resizingColKey = void 0;
1668
1886
  const w = Math.max(col.minWidth ?? 40, Math.min(col.maxWidth ?? 600, startW + ev.clientX - startX));
1887
+ this.#viewState.notify();
1669
1888
  this.dispatchEvent(new CustomEvent(`datagrid-column-resize`, {
1670
1889
  detail: {
1671
1890
  key: col.key,
@@ -1678,6 +1897,179 @@ let DataGridElement = class DataGridElement extends LitElement {
1678
1897
  window.addEventListener(`pointermove`, onMove);
1679
1898
  window.addEventListener(`pointerup`, onUp);
1680
1899
  }
1900
+ /**
1901
+ * Snapshot selection, cursor, scroll, collapsed groups, column layout, sort
1902
+ * and (with `allowGrouping`) grouping as plain JSON, keyed by row / group /
1903
+ * column key. Includes pending state for keys not currently present. Rows
1904
+ * keyed by index (no `key` / `id` under the default `getRowKey`) are never
1905
+ * recorded.
1906
+ */
1907
+ getViewState(options) {
1908
+ const withPending = options?.includePending ?? true;
1909
+ const cursor = this._tickled.inputMode === `keyboard` ? this._tickled.tickledId : void 0;
1910
+ const focus = cursor !== void 0 && this.#isStableKey(cursor) ? cursor : void 0;
1911
+ const groupBy = this.groupBy === null || typeof this.groupBy === `string` ? this.groupBy : void 0;
1912
+ return this.#viewState.shape({
1913
+ selected: keysToArray([...this._selectedKeys].filter((k) => this.#isStableKey(k)), withPending ? this.#pendingSelected : []),
1914
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0),
1915
+ scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? this.#captureScroll(),
1916
+ expanded: collapsedToRecord(this._collapsedGroups, (k) => withPending || this.#groupKeys.has(k)),
1917
+ columns: captureColumns(this.columns, this.#pendingColumns, this.#columnOrder, withPending),
1918
+ sort: this.sortState,
1919
+ groupBy: this.allowGrouping ? groupBy : void 0
1920
+ }, options);
1921
+ }
1922
+ /**
1923
+ * Apply saved view state. Fields present replace the current ones; row and
1924
+ * column keys that aren't defined yet are applied as they appear (including
1925
+ * rows streamed in with `appendRows()`).
1926
+ */
1927
+ setViewState(state) {
1928
+ if (!this.#viewState.accepts(state)) return;
1929
+ this.#viewState.restoring(() => {
1930
+ if (state.columns && typeof state.columns === `object`) {
1931
+ const { pending, order } = columnStateFromRecord(state.columns);
1932
+ this.#pendingColumns = pending;
1933
+ this.#columnOrder = order;
1934
+ this.#reconcileColumns();
1935
+ }
1936
+ if (state.sort !== void 0 && isSortState(state.sort)) this.sortState = state.sort;
1937
+ if (state.groupBy === null || typeof state.groupBy === `string`) this.groupBy = state.groupBy;
1938
+ if (state.expanded && typeof state.expanded === `object`) this._collapsedGroups = collapsedFromRecord(state.expanded);
1939
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
1940
+ if (state.scroll) this.#viewState.pendingScroll = state.scroll;
1941
+ if (Array.isArray(state.selected)) {
1942
+ const prev = this._selectedKeys;
1943
+ this.#pendingSelected = capKeys(new Set(state.selected.filter((k) => typeof k === `string`)));
1944
+ this._selectedKeys = /* @__PURE__ */ new Set();
1945
+ this.#reconcileRows(prev);
1946
+ } else this.#reconcileRows();
1947
+ this.#viewState.notify();
1948
+ });
1949
+ this.requestUpdate();
1950
+ }
1951
+ clearPendingViewState() {
1952
+ this.#pendingSelected = EMPTY_KEYS;
1953
+ this.#pendingFocus = void 0;
1954
+ this.#pendingColumns = EMPTY_COLUMNS;
1955
+ this.#columnOrder = EMPTY_ORDER;
1956
+ this.#viewState.pendingScroll = void 0;
1957
+ if ([...this._collapsedGroups].some((k) => !this.#groupKeys.has(k))) this._collapsedGroups = new Set([...this._collapsedGroups].filter((k) => this.#groupKeys.has(k)));
1958
+ }
1959
+ /** Apply pending column layout to defined columns; demote layout of removed ones. */
1960
+ #reconcileColumns() {
1961
+ const r = reconcileColumns(this.#reconciledColumns, this.columns, this.#pendingColumns, this.#columnOrder);
1962
+ this.#pendingColumns = r.pending;
1963
+ this.#columnOrder = r.order;
1964
+ this.#reconciledColumns = r.columns;
1965
+ if (r.columns !== this.columns) {
1966
+ this.columns = r.columns;
1967
+ this.#viewState.restoring(() => this.#viewState.notify());
1968
+ }
1969
+ }
1970
+ /**
1971
+ * Re-key selection and cursor onto the current rows: state for rows that
1972
+ * left goes pending (silently), pending state for rows now present is
1973
+ * applied. `baseline` is the selection to report as `previous` when the
1974
+ * caller already replaced it (`setViewState`).
1975
+ */
1976
+ #reconcileRows(baseline) {
1977
+ this.#viewState.restoring(() => {
1978
+ const sel = reconcileSelectedKeys(this._selectedKeys, this.#pendingSelected, (k) => this.#hasRow(k), !this.#isMultiSelect(), this.selectionMode !== `none`);
1979
+ this.#pendingSelected = sel.pending;
1980
+ if (sel.changed) this.#setRestoredSelection(sel.selected);
1981
+ const before = baseline ?? this._selectedKeys;
1982
+ if (baseline ? !sameSet(baseline, this._selectedKeys) : sel.restored) this.#dispatchSelect(before);
1983
+ const cursor = this._tickled.tickledId;
1984
+ if (cursor !== void 0 && !this.#flatIndexByKey.has(cursor)) {
1985
+ if (this._tickled.inputMode === `keyboard` && this.#isStableKey(cursor)) this.#pendingFocus ??= cursor;
1986
+ this._tickled.clearTickled();
1987
+ }
1988
+ this.#applyPendingFocus();
1989
+ });
1990
+ }
1991
+ /** Streamed append: match pending state against the new slice only (never the whole row set). */
1992
+ #resolveAppended(appended, startIdx) {
1993
+ const wantSelection = this.#pendingSelected.size > 0 && this.selectionMode !== `none`;
1994
+ if (wantSelection || this.#rowIndex !== void 0) {
1995
+ const keys = appended.map((r, j) => this.getRowKey(r, startIdx + j));
1996
+ keys.forEach((k, j) => this.#rowIndex?.set(k, appended[j]));
1997
+ const present = wantSelection ? matchSlice(this.#pendingSelected, keys) : [];
1998
+ if (present.length > 0) this.#viewState.restoring(() => {
1999
+ const prev = this._selectedKeys;
2000
+ const sel = resolvePendingKeys(prev, this.#pendingSelected, present, !this.#isMultiSelect());
2001
+ this.#pendingSelected = sel.pending;
2002
+ this.#setRestoredSelection(sel.selected);
2003
+ this.#dispatchSelect(prev);
2004
+ });
2005
+ }
2006
+ if (this.#pendingFocus !== void 0) this.#viewState.restoring(() => this.#applyPendingFocus());
2007
+ }
2008
+ #setRestoredSelection(selected) {
2009
+ const lastKey = this._anchorKey;
2010
+ this._selectedKeys = new Set(selected);
2011
+ this._anchorKey = lastKey !== void 0 && selected.has(lastKey) ? lastKey : [...selected].at(-1);
2012
+ }
2013
+ #applyPendingFocus() {
2014
+ const key = this.#pendingFocus;
2015
+ if (key === void 0 || this._getRowForKey(key) === void 0) return;
2016
+ this.#pendingFocus = void 0;
2017
+ this._tickled.setInputMode(`keyboard`);
2018
+ this._tickled.setTickled(key);
2019
+ this.#viewState.notify();
2020
+ }
2021
+ /** Whether a row with this key is in `rows` (visible, filtered out or in a collapsed group). */
2022
+ #hasRow(key) {
2023
+ const i = this.#flatIndexByKey.get(key);
2024
+ if (i !== void 0) return this._flat[i].type === `row`;
2025
+ return this.#rowsByKey().has(key);
2026
+ }
2027
+ /** Every row by key — built only when a key isn't in `_flat`, then reused until the next resort. */
2028
+ #rowsByKey() {
2029
+ if (this.#rowIndex === void 0 || this.#rowIndexKeyFn !== this.getRowKey) {
2030
+ this.#rowIndexKeyFn = this.getRowKey;
2031
+ this.#rowIndex = new Map(this._sortedRows.map((r, j) => [this.getRowKey(r, j), r]));
2032
+ }
2033
+ return this.#rowIndex;
2034
+ }
2035
+ /** Index-derived keys (default `getRowKey`, row without `key` / `id`) can't be restored, so aren't recorded. */
2036
+ #isStableKey(key) {
2037
+ if (this.getRowKey !== defaultGetRowKey) return true;
2038
+ const row = this._getRowForKey(key) ?? this.#rowsByKey().get(key);
2039
+ return row !== void 0 && (row.key ?? row.id) !== void 0;
2040
+ }
2041
+ #captureScroll() {
2042
+ const vp = this._viewportEl;
2043
+ if (!vp) return void 0;
2044
+ const top = vp.scrollTop;
2045
+ const rh = this.rowHeight;
2046
+ const first = Math.max(0, Math.floor(top / rh));
2047
+ const end = Math.min(this._flat.length, first + 64);
2048
+ for (let i = first; i < end; i++) {
2049
+ const entry = this._flat[i];
2050
+ if (entry.type === `row`) return anchorFromRow(top, this.#isStableKey(entry.key) ? entry.key : void 0, i * rh, vp.scrollLeft);
2051
+ }
2052
+ return anchorFromRow(top, void 0, 0, vp.scrollLeft);
2053
+ }
2054
+ /** Returns `true` once the saved position is fully reached. */
2055
+ #applyScroll(scroll) {
2056
+ const vp = this._viewportEl;
2057
+ if (!vp) return false;
2058
+ let top = scroll.top;
2059
+ if (scroll.anchor) {
2060
+ const i = this.#flatIndexByKey.get(scroll.anchor.key);
2061
+ if (i === void 0) return false;
2062
+ top = i * this.rowHeight + scroll.anchor.offset;
2063
+ }
2064
+ const reachedY = scrollToOffset(vp, top);
2065
+ const reachedX = scroll.left === void 0 || scrollToOffset(vp, scroll.left, `horizontal`);
2066
+ const before = this.#windowRows(2);
2067
+ this._scrollTop = vp.scrollTop;
2068
+ this._scrollLeft = vp.scrollLeft;
2069
+ const after = this.#windowRows(2);
2070
+ if (before.start !== after.start || before.end !== after.end) this.requestUpdate();
2071
+ return reachedY && reachedX;
2072
+ }
1681
2073
  /** Height of the row area: the viewport minus the sticky header row inside it. */
1682
2074
  #bodyHeight() {
1683
2075
  return this._viewportEl.clientHeight - (this._headerRowEl?.offsetHeight ?? 0);
@@ -1962,6 +2354,30 @@ let DataGridElement = class DataGridElement extends LitElement {
1962
2354
  ${this.search.renderField()}
1963
2355
  </div>
1964
2356
  `;
2357
+ }
2358
+ #renderColumnCheckItem(col) {
2359
+ return html`<ixfx-menu-check-item command="col:${col.key}" label=${col.label} ?checked=${col.visible !== false}></ixfx-menu-check-item>`;
2360
+ }
2361
+ /** Ungrouped columns first, then one submenu per `group` (in first-seen order). */
2362
+ #renderColumnMenuItems() {
2363
+ const groups = /* @__PURE__ */ new Map();
2364
+ const ungrouped = [];
2365
+ for (const col of this.columns) {
2366
+ if (col.group === void 0 || col.group === ``) {
2367
+ ungrouped.push(col);
2368
+ continue;
2369
+ }
2370
+ groups.set(col.group, [...groups.get(col.group) ?? [], col]);
2371
+ }
2372
+ return html`
2373
+ ${repeat(ungrouped, (c) => c.key, (c) => this.#renderColumnCheckItem(c))}
2374
+ ${repeat([...groups], ([name]) => name, ([name, cols]) => html`
2375
+ <ixfx-menu-sub-trigger label=${name}>
2376
+ <ixfx-menu-container slot="submenu">
2377
+ ${repeat(cols, (c) => c.key, (c) => this.#renderColumnCheckItem(c))}
2378
+ </ixfx-menu-container>
2379
+ </ixfx-menu-sub-trigger>
2380
+ `)}`;
1965
2381
  }
1966
2382
  /** Built-in column/group menu, pinned over the header row's end. Only for `columnMenu: auto`. */
1967
2383
  #renderColumnMenu() {
@@ -1983,6 +2399,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1983
2399
  const v = value ?? ``;
1984
2400
  this.groupBy = v || null;
1985
2401
  this._collapsedGroups = /* @__PURE__ */ new Set();
2402
+ this.#viewState.notify();
1986
2403
  this.dispatchEvent(new CustomEvent(`datagrid-group-change`, {
1987
2404
  detail: { groupBy: this.groupBy },
1988
2405
  bubbles: true,
@@ -1993,9 +2410,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1993
2410
  }}>
1994
2411
  <ixfx-menu-container slot="menu">
1995
2412
  <ixfx-menu-header label="Columns"></ixfx-menu-header>
1996
- ${repeat(this.columns, (c) => c.key, (col) => html`
1997
- <ixfx-menu-check-item command="col:${col.key}" label=${col.label} ?checked=${col.visible !== false}></ixfx-menu-check-item>
1998
- `)}
2413
+ ${this.#renderColumnMenuItems()}
1999
2414
  ${this.allowGrouping ? html`
2000
2415
  <ixfx-menu-separator></ixfx-menu-separator>
2001
2416
  <ixfx-menu-header label="Group by"></ixfx-menu-header>