@ixfx/components 0.15.5 → 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 (76) hide show
  1. package/bundle/index.d.ts +917 -44
  2. package/bundle/index.js +702 -696
  3. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  4. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  5. package/dist/crumbs.d.ts +2 -115
  6. package/dist/crumbs.js +1 -1179
  7. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
  9. package/dist/data-grid.d.ts +3 -3
  10. package/dist/data-grid.js +1 -1
  11. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  13. package/dist/detail-list.d.ts +4 -4
  14. package/dist/detail-list.js +1 -1
  15. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-JPgQSmLW.d.ts} +27 -4
  16. package/dist/flat-list.d.ts +2 -2
  17. package/dist/flat-list.js +123 -4
  18. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  19. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  20. package/dist/grid-list.d.ts +3 -3
  21. package/dist/grid-list.js +1 -1
  22. package/dist/incr-search.d.ts +1 -1
  23. package/dist/index-9ttcdwlL.d.ts +71 -0
  24. package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  25. package/dist/index-CYRc6Dah.d.ts +475 -0
  26. package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
  27. package/dist/index.d.ts +334 -23
  28. package/dist/index.js +2247 -867
  29. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  30. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  31. package/dist/miller-BFRfn0ve.js +2237 -0
  32. package/dist/miller.d.ts +4 -437
  33. package/dist/miller.js +1 -1815
  34. package/dist/pending-BGidHppL.js +91 -0
  35. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  36. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  37. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  38. package/dist/split-layout.d.ts +1 -1
  39. package/dist/split-layout.js +1 -1
  40. package/dist/sunburst-tree.d.ts +2 -2
  41. package/dist/sunburst-tree.js +1 -1
  42. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_VxOHlo.js} +298 -79
  43. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
  44. package/dist/tree-CftHwtEb.js +146 -0
  45. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  46. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  47. package/dist/tree.d.ts +3 -3
  48. package/dist/tree.js +1 -1
  49. package/dist/types-LU1u0CB8.d.ts +87 -0
  50. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  51. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  52. package/dist/vertical-list.d.ts +3 -3
  53. package/dist/vertical-list.js +1 -1789
  54. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  55. package/docs-user/README.md +2 -0
  56. package/docs-user/crumbs.md +27 -0
  57. package/docs-user/data-grid.md +66 -1
  58. package/docs-user/detail-list.md +54 -0
  59. package/docs-user/dock.md +54 -12
  60. package/docs-user/flat-list.md +27 -0
  61. package/docs-user/grid-list.md +64 -11
  62. package/docs-user/hyperbolic-tree.md +31 -4
  63. package/docs-user/icicle-tree.md +36 -0
  64. package/docs-user/index.json +12 -2
  65. package/docs-user/llms.txt +2 -0
  66. package/docs-user/miller.md +52 -21
  67. package/docs-user/multicol-list.md +50 -8
  68. package/docs-user/split-layout.md +45 -17
  69. package/docs-user/sunburst-tree.md +32 -2
  70. package/docs-user/tree.md +45 -6
  71. package/docs-user/treemap.md +32 -0
  72. package/docs-user/twosplit-layout.md +22 -0
  73. package/docs-user/user-catalog.md +1 -1
  74. package/docs-user/vertical-list.md +55 -7
  75. package/llms.txt +2 -0
  76. package/package.json +1 -1
@@ -0,0 +1,2237 @@
1
+ import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
2
+ import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrAHEj1q.js";
3
+ import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
4
+ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
+ import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-JIhwB49y.js";
6
+ import { a as capKeys, d as partitionKeys, l as keysToArray, n as EMPTY_KEYS } from "./pending-BGidHppL.js";
7
+ import "./interaction-E-uMGHkJ.js";
8
+ import { t as collectDataResult } from "./data-provider-35TyWclX.js";
9
+ import { a as restoreElementScroll, i as onUserScrollIntent, o as scrollToOffset, r as captureElementScroll, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
10
+ import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
11
+ import { LitElement, css, html, nothing } from "lit";
12
+ import { property, state } from "lit/decorators.js";
13
+ import { createCommandRegistry } from "@clinth/ui-commands";
14
+ //#region src/miller/miller-view-state.ts
15
+ /** Keys of the node in each column whose children are the next column. */
16
+ function openPathKeys(columns) {
17
+ const keys = [];
18
+ for (let depth = 0; depth < columns.length - 1; depth++) {
19
+ const next = columns[depth + 1];
20
+ const parent = columns[depth].find((n) => n.children === next);
21
+ if (!parent) break;
22
+ keys.push(parent.item.key);
23
+ }
24
+ return keys;
25
+ }
26
+ /**
27
+ * Walk `keys` down from `rootColumn`. Stops at a key that isn't present
28
+ * (state stays pending for it), at a branch whose children are unloaded
29
+ * (`blocked`), or at a leaf (`done` — a leaf can never open).
30
+ */
31
+ function resolveOpenPath(rootColumn, keys) {
32
+ const columns = [rootColumn];
33
+ const nodes = [];
34
+ let current = rootColumn;
35
+ for (const key of keys) {
36
+ const node = current.find((n) => n.item.key === key);
37
+ if (!node) return {
38
+ columns,
39
+ nodes,
40
+ blocked: void 0,
41
+ done: false
42
+ };
43
+ nodes.push(node);
44
+ if (node.item.isLeaf === true) return {
45
+ columns,
46
+ nodes,
47
+ blocked: void 0,
48
+ done: true
49
+ };
50
+ if (node.children === void 0) return {
51
+ columns,
52
+ nodes,
53
+ blocked: node,
54
+ done: false
55
+ };
56
+ columns.push(node.children);
57
+ current = node.children;
58
+ }
59
+ return {
60
+ columns,
61
+ nodes,
62
+ blocked: void 0,
63
+ done: true
64
+ };
65
+ }
66
+ /** JSON `columnWidths` → depth → px. Entries that aren't a depth and a positive width are dropped. */
67
+ function widthsFromRecord(record) {
68
+ const out = /* @__PURE__ */ new Map();
69
+ for (const [depth, width] of Object.entries(record)) {
70
+ const d = Number(depth);
71
+ if (Number.isInteger(d) && d >= 0 && typeof width === `number` && Number.isFinite(width) && width > 0) out.set(d, width);
72
+ }
73
+ return out;
74
+ }
75
+ /** Same columns, by array identity. */
76
+ function sameColumns(a, b) {
77
+ return a.length === b.length && a.every((col, i) => col === b[i]);
78
+ }
79
+ /** Depth of the column containing `key`, or -1. */
80
+ function columnDepthOf(columns, key) {
81
+ return columns.findIndex((col) => col.some((n) => n.item.key === key));
82
+ }
83
+ /** Key → node for every loaded node under `nodes`. */
84
+ function indexLoaded(nodes) {
85
+ const index = /* @__PURE__ */ new Map();
86
+ const stack = [...nodes];
87
+ while (stack.length > 0) {
88
+ const node = stack.pop();
89
+ index.set(node.item.key, node);
90
+ if (node.children) stack.push(...node.children);
91
+ }
92
+ return index;
93
+ }
94
+ /**
95
+ * Selected keys no longer loaded go pending (deselected silently); pending
96
+ * keys now loaded are selected. With `single`, a restored key replaces the
97
+ * selection and nothing else may come back later.
98
+ */
99
+ function reconcileSelectedKeys(live, pending, has, single) {
100
+ const gone = [...live].filter((key) => !has(key));
101
+ const { present, missing } = partitionKeys(pending, has);
102
+ if (gone.length === 0 && present.length === 0) return {
103
+ live,
104
+ pending,
105
+ restored: [],
106
+ changed: false
107
+ };
108
+ if (single && present.length > 0) {
109
+ const winner = present.at(-1);
110
+ return {
111
+ live: /* @__PURE__ */ new Set([winner]),
112
+ pending: EMPTY_KEYS,
113
+ restored: [winner],
114
+ changed: true
115
+ };
116
+ }
117
+ return {
118
+ live: /* @__PURE__ */ new Set([...[...live].filter(has), ...present]),
119
+ pending: capKeys(gone.length === 0 ? missing : /* @__PURE__ */ new Set([...missing, ...gone])),
120
+ restored: present,
121
+ changed: true
122
+ };
123
+ }
124
+ /**
125
+ * Plan applying the pending `keys` to the columns. While `loading` (the
126
+ * branch whose load is in flight) is the blocking node, nothing changes.
127
+ * Per-depth selection follows the path, keeping the selection inside the
128
+ * deepest open column.
129
+ */
130
+ function planOpenPath(rootColumn, keys, columns, selectedByDepth, loading) {
131
+ const r = resolveOpenPath(rootColumn, keys);
132
+ const waiting = r.blocked !== void 0 && r.blocked === loading;
133
+ const change = !waiting && !sameColumns(r.columns, columns);
134
+ let byDepth;
135
+ if (change) {
136
+ byDepth = new Map(r.nodes.map((node, depth) => [depth, node]));
137
+ const tail = selectedByDepth.get(r.nodes.length);
138
+ if (tail && r.columns[r.nodes.length]?.includes(tail)) byDepth.set(r.nodes.length, tail);
139
+ }
140
+ return {
141
+ columns: change ? r.columns : void 0,
142
+ selectedByDepth: byDepth,
143
+ pending: r.done ? void 0 : keys,
144
+ load: r.blocked && !waiting && r.blocked.item.isLeaf !== true ? {
145
+ node: r.blocked,
146
+ depth: r.nodes.length - 1
147
+ } : void 0
148
+ };
149
+ }
150
+ /**
151
+ * Re-key the selection onto the loaded tree (`index`). Returns `undefined`
152
+ * when nothing changes (same keys, same node objects).
153
+ */
154
+ function planSelection(keys, pending, current, last, index, columns, single) {
155
+ const sel = reconcileSelectedKeys(keys, pending, (k) => index.has(k), single);
156
+ const nodes = [...sel.live].map((k) => index.get(k));
157
+ if (!sel.changed && nodes.length === current.size && nodes.every((n) => current.has(n))) return void 0;
158
+ const lastKey = sel.restored.at(-1) ?? last?.item.key;
159
+ const placed = [];
160
+ for (const key of sel.restored) {
161
+ const depth = columnDepthOf(columns, key);
162
+ if (depth >= 0) placed.push([depth, index.get(key)]);
163
+ }
164
+ return {
165
+ keys: new Set(sel.live),
166
+ nodes: new Set(nodes),
167
+ pending: sel.pending,
168
+ last: (lastKey === void 0 ? void 0 : index.get(lastKey)) ?? nodes.at(-1),
169
+ placed,
170
+ restored: sel.restored.length > 0
171
+ };
172
+ }
173
+ /** Cursor gone → pending (when it was a keyboard cursor); pending cursor visible → placed. */
174
+ function planFocus(cursor, keyboard, pending, index, columns) {
175
+ const clear = cursor !== void 0 && !index.has(cursor);
176
+ const want = pending ?? (clear && keyboard ? cursor : void 0);
177
+ const depth = want === void 0 ? -1 : columnDepthOf(columns, want);
178
+ if (depth < 0) return {
179
+ clear,
180
+ pending: want,
181
+ tickle: void 0
182
+ };
183
+ return {
184
+ clear,
185
+ pending: void 0,
186
+ tickle: {
187
+ node: columns[depth].find((n) => n.item.key === want),
188
+ depth
189
+ }
190
+ };
191
+ }
192
+ /**
193
+ * The rendered columns' `.column-content` elements, the key each column is
194
+ * keyed by (its parent node's) and the row keys per column.
195
+ */
196
+ function columnScrollDom(root, columns, displayKey, filter) {
197
+ return {
198
+ contents: [...root?.querySelectorAll(`.column:not(.loading-column) > .column-content`) ?? []],
199
+ parentKeys: displayKey === void 0 ? [] : [displayKey, ...openPathKeys(columns)],
200
+ keysAt: (depth) => (columns[depth] ?? []).filter((n) => !filter || filter(n, depth)).map((n) => n.item.key)
201
+ };
202
+ }
203
+ /**
204
+ * Scroll of each scrolled column, keyed by `parentKeys[depth]`. `contents`
205
+ * are the `.column-content` elements in depth order.
206
+ */
207
+ function captureColumnScrolls(contents, parentKeys, keysAt) {
208
+ const out = {};
209
+ contents.forEach((content, depth) => {
210
+ const parent = parentKeys[depth];
211
+ if (parent === void 0 || content.scrollTop === 0 && content.scrollLeft === 0) return;
212
+ const items = [...content.querySelectorAll(`.item`)];
213
+ const keys = keysAt(depth);
214
+ out[parent] = captureElementScroll(content, items, (el) => keys[items.indexOf(el)]);
215
+ });
216
+ return out;
217
+ }
218
+ /** Apply pending column scrolls whose column is rendered. Returns those still pending. */
219
+ function applyColumnScrolls(pending, contents, parentKeys, keysAt) {
220
+ let next;
221
+ for (const [parent, scroll] of pending) {
222
+ const depth = parentKeys.indexOf(parent);
223
+ const content = contents[depth];
224
+ if (depth < 0 || !content) continue;
225
+ let reached;
226
+ if (scroll.anchor) {
227
+ const index = keysAt(depth).indexOf(scroll.anchor.key);
228
+ const item = index < 0 ? void 0 : content.querySelectorAll(`.item`)[index];
229
+ reached = item ? restoreElementScroll(content, item, scroll.anchor.offset) : false;
230
+ } else reached = scrollToOffset(content, scroll.top);
231
+ if (reached) {
232
+ next ??= new Map(pending);
233
+ next.delete(parent);
234
+ }
235
+ }
236
+ return next ?? pending;
237
+ }
238
+ //#endregion
239
+ //#region src/miller/miller-base.ts
240
+ const EXPAND_DEBOUNCE_MS = 150;
241
+ let MillerBaseElement = class MillerBaseElement extends LitElement {
242
+ constructor(..._args) {
243
+ super(..._args);
244
+ this.headers = false;
245
+ this.stretchLastColumn = false;
246
+ this.interactionMode = `standard`;
247
+ this.selectionFilter = `leaf`;
248
+ this.exclusivity = `none`;
249
+ this._columns = [];
250
+ this._selectedByDepth = /* @__PURE__ */ new Map();
251
+ this._selectedKeys = /* @__PURE__ */ new Set();
252
+ this._selectedNodes = /* @__PURE__ */ new Set();
253
+ this.#columnWidths = /* @__PURE__ */ new Map();
254
+ this.#resizingDepth = -1;
255
+ this.#resizeStartX = 0;
256
+ this.#resizeStartWidth = 0;
257
+ this.restoreLoads = true;
258
+ this.#viewState = new ViewStateController(this, {
259
+ kind: `miller-list`,
260
+ capture: () => this.getViewState()
261
+ });
262
+ this.#pendingSelected = EMPTY_KEYS;
263
+ this.#pendingColumnScroll = /* @__PURE__ */ new Map();
264
+ this.#keyboardFocus = false;
265
+ this.#columnsSeen = [];
266
+ this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
267
+ labelSelector: `.label`,
268
+ highlightKey: `incr-search-highlight`
269
+ }) });
270
+ this.tickled = new TickledController(this, { getItems: () => {
271
+ const items = [];
272
+ for (let depth = 0; depth < this._columns.length; depth++) {
273
+ const col = this._columns[depth];
274
+ if (col) for (const node of col) items.push({ id: this.#buildTickledId(node, depth) });
275
+ }
276
+ return items;
277
+ } });
278
+ this.#onColumnScroll = (event) => {
279
+ if (this.#pendingColumnScroll.size === 0 && event.target.classList?.contains(`column-content`)) this.#viewState.notify();
280
+ };
281
+ this.#selectOrigin = `user`;
282
+ this.#selectSilent = false;
283
+ this.#onKeyDown = (event) => {
284
+ if (this.search.isOpen) return;
285
+ const currentDepth = this.#getTickledDepthOrDefault();
286
+ const col = this._columns[currentDepth];
287
+ if (!col || col.length === 0) {
288
+ const depth0Col = this._columns[0];
289
+ if (depth0Col && depth0Col.length > 0) {
290
+ this.tickled.setTickled(this.#buildTickledId(depth0Col[0], 0));
291
+ this.requestUpdate();
292
+ }
293
+ return;
294
+ }
295
+ switch (event.key) {
296
+ case `ArrowDown`:
297
+ event.preventDefault();
298
+ this.#moveCursor(col, currentDepth, event, (current) => Math.min(current + 1, col.length - 1));
299
+ break;
300
+ case `ArrowUp`:
301
+ event.preventDefault();
302
+ this.#moveCursor(col, currentDepth, event, (current) => Math.max(current - 1, 0));
303
+ break;
304
+ case `Home`:
305
+ event.preventDefault();
306
+ this.#moveCursor(col, currentDepth, event, () => 0, true);
307
+ break;
308
+ case `End`:
309
+ event.preventDefault();
310
+ this.#moveCursor(col, currentDepth, event, () => col.length - 1, true);
311
+ break;
312
+ case `PageUp`:
313
+ event.preventDefault();
314
+ this.#moveCursor(col, currentDepth, event, (current) => Math.max(current - this.#pageSize(currentDepth), 0), true);
315
+ break;
316
+ case `PageDown`:
317
+ event.preventDefault();
318
+ this.#moveCursor(col, currentDepth, event, (current) => Math.min(current + this.#pageSize(currentDepth), col.length - 1), true);
319
+ break;
320
+ case `ArrowRight`: {
321
+ event.preventDefault();
322
+ const tickled = this.#getTickledNodeAndDepth();
323
+ if (!tickled) {
324
+ if (col.length > 0) {
325
+ this.#setTickledNode(col[0], currentDepth);
326
+ this.requestUpdate();
327
+ }
328
+ return;
329
+ }
330
+ const { node, depth } = tickled;
331
+ if (this.interactionMode === `vscode` || this.interactionMode === `standard`) {
332
+ this._selectedByDepth.set(depth, node);
333
+ this.#expandNode(node, depth);
334
+ } else this.#onItemClick(node, depth);
335
+ if (this._columns.length > depth + 1) {
336
+ const nextCol = this._columns[depth + 1];
337
+ if (nextCol && nextCol.length > 0) this.#setTickledNode(nextCol[0], depth + 1);
338
+ }
339
+ this.requestUpdate();
340
+ break;
341
+ }
342
+ case `ArrowLeft`:
343
+ event.preventDefault();
344
+ if (currentDepth > 0) {
345
+ this.#abortPending();
346
+ this.#pendingPath = void 0;
347
+ this._columns = this._columns.slice(0, currentDepth);
348
+ const prevCol = this._columns[currentDepth - 1];
349
+ if (prevCol && prevCol.length > 0) {
350
+ const selectedNode = this._selectedByDepth.get(currentDepth - 1);
351
+ const targetNode = selectedNode && prevCol.includes(selectedNode) ? selectedNode : prevCol[0];
352
+ this.#setTickledNode(targetNode, currentDepth - 1);
353
+ }
354
+ this.requestUpdate();
355
+ }
356
+ break;
357
+ case `Enter`: {
358
+ event.preventDefault();
359
+ const tickled = this.#getTickledNodeAndDepth();
360
+ if (!tickled) return;
361
+ const { node, depth, index } = tickled;
362
+ if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
363
+ if (event.shiftKey && this.#anchor?.depth === depth) {
364
+ const from = Math.min(this.#anchor.index, index);
365
+ const to = Math.max(this.#anchor.index, index);
366
+ this._selectedKeys = new Set(col.slice(from, to + 1).map((n) => n.item.key));
367
+ } else {
368
+ this._selectedKeys.add(node.item.key);
369
+ this.#anchor = {
370
+ key: node.item.key,
371
+ depth,
372
+ index
373
+ };
374
+ }
375
+ this.#selectionChanged();
376
+ } else this._selectedByDepth.set(depth, node);
377
+ this.dispatchEvent(new CustomEvent(`activate`, {
378
+ detail: {
379
+ node,
380
+ depth
381
+ },
382
+ bubbles: true,
383
+ composed: true
384
+ }));
385
+ this.requestUpdate();
386
+ break;
387
+ }
388
+ case `Escape`:
389
+ event.preventDefault();
390
+ this.#abortPending();
391
+ if (this._columns.length > 1) {
392
+ this.#pendingPath = void 0;
393
+ this._columns = this._columns.slice(0, -1);
394
+ const newDepth = Math.max(0, currentDepth - 1);
395
+ const prevCol = this._columns[newDepth];
396
+ if (prevCol && prevCol.length > 0) {
397
+ const selectedNode = this._selectedByDepth.get(newDepth);
398
+ const targetNode = selectedNode && prevCol.includes(selectedNode) ? selectedNode : prevCol[0];
399
+ this.#setTickledNode(targetNode, newDepth);
400
+ }
401
+ this.requestUpdate();
402
+ }
403
+ }
404
+ };
405
+ this.#onResizeStart = (event, depth) => {
406
+ event.preventDefault();
407
+ event.stopPropagation();
408
+ this.#resizingDepth = depth;
409
+ this.#resizeStartX = event.clientX;
410
+ const el = this.shadowRoot?.querySelectorAll(`.column`)[depth];
411
+ this.#resizeStartWidth = el ? el.getBoundingClientRect().width : 200;
412
+ window.addEventListener(`pointermove`, this.#onResizeMove);
413
+ window.addEventListener(`pointerup`, this.#onResizeEnd);
414
+ };
415
+ this.#resizeRaf = null;
416
+ this.#resizePendingWidth = 0;
417
+ this.#onResizeMove = (event) => {
418
+ if (this.#resizingDepth < 0) return;
419
+ this.#resizePendingWidth = Math.max(32, this.#resizeStartWidth + (event.clientX - this.#resizeStartX));
420
+ this.#resizeRaf ??= requestAnimationFrame(this.#applyResizeWidth);
421
+ };
422
+ this.#applyResizeWidth = () => {
423
+ this.#resizeRaf = null;
424
+ if (this.#resizingDepth < 0) return;
425
+ (this.shadowRoot?.querySelectorAll(`.column`)[this.#resizingDepth])?.setAttribute(`style`, this.#explicitWidthStyle(this.#resizePendingWidth));
426
+ };
427
+ this.#onResizeEnd = () => {
428
+ if (this.#resizeRaf !== null) {
429
+ cancelAnimationFrame(this.#resizeRaf);
430
+ this.#resizeRaf = null;
431
+ }
432
+ if (this.#resizingDepth >= 0 && this.#resizePendingWidth > 0) {
433
+ this.#columnWidths.set(this.#resizingDepth, this.#resizePendingWidth);
434
+ this.#viewState.notify();
435
+ this.requestUpdate();
436
+ }
437
+ this.#resizePendingWidth = 0;
438
+ this.#resizingDepth = -1;
439
+ window.removeEventListener(`pointermove`, this.#onResizeMove);
440
+ window.removeEventListener(`pointerup`, this.#onResizeEnd);
441
+ };
442
+ }
443
+ #lastSelectedNode;
444
+ #anchor;
445
+ #columnWidths;
446
+ #pendingNode;
447
+ #pendingController;
448
+ #expandTimer;
449
+ #resizingDepth;
450
+ #resizeStartX;
451
+ #resizeStartWidth;
452
+ #modelUnsubscribe;
453
+ #viewState;
454
+ /** Saved open path still being applied (keys not loaded / present yet). */
455
+ #pendingPath;
456
+ #pendingSelected;
457
+ #pendingFocus;
458
+ #pendingColumnScroll;
459
+ /** The cursor was last placed by keyboard (only then is it recorded as `focus`). */
460
+ #keyboardFocus;
461
+ /** Columns last reported through `view-state-change`. */
462
+ #columnsSeen;
463
+ #searchRegistry;
464
+ #unregisterSearchCommands;
465
+ /** Command registry carrying this component's `list.search.*` commands. */
466
+ get registry() {
467
+ return this.#searchRegistry;
468
+ }
469
+ #effectiveSearchModes() {
470
+ const raw = this.searchModes?.trim();
471
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
472
+ const requested = new Set(raw.split(/\s+/));
473
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
474
+ }
475
+ #syncSearchCommands() {
476
+ if (!this.#searchRegistry) return;
477
+ this.search.setModes(this.#effectiveSearchModes());
478
+ this.#unregisterSearchCommands?.();
479
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
480
+ }
481
+ #buildTickledId(node, depth) {
482
+ return `${depth}:${node.item.key}`;
483
+ }
484
+ /** The node column 0 renders the children of: `viewRoot` when rebased, else `root`. */
485
+ get #displayRoot() {
486
+ return this.viewRoot ?? this.root;
487
+ }
488
+ #getTickledDepth() {
489
+ const id = this.tickled.tickledId;
490
+ if (!id) return void 0;
491
+ const colonIndex = id.indexOf(`:`);
492
+ if (colonIndex < 0) return void 0;
493
+ return Number(id.substring(0, colonIndex));
494
+ }
495
+ willUpdate(_changedProps) {
496
+ if (!sameColumns(this._columns, this.#columnsSeen)) {
497
+ this.#columnsSeen = this._columns;
498
+ this.#viewState.notify();
499
+ }
500
+ }
501
+ updated(changedProps) {
502
+ if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
503
+ if (changedProps.has(`root`) && this.root) {
504
+ const rebased = this.viewRoot !== void 0;
505
+ const path = rebased ? [] : openPathKeys(this._columns);
506
+ const focus = this.#focusKey();
507
+ this.#abortPending();
508
+ if (rebased) {
509
+ this.viewRoot = void 0;
510
+ this._selectedKeys = /* @__PURE__ */ new Set();
511
+ this._selectedNodes = /* @__PURE__ */ new Set();
512
+ }
513
+ this._selectedByDepth.clear();
514
+ this.#lastSelectedNode = void 0;
515
+ this.#anchor = void 0;
516
+ this.tickled.clearTickled();
517
+ const display = this.#displayRoot;
518
+ this._columns = display?.children ? [display.children] : [];
519
+ this.#columnsSeen = this._columns;
520
+ if (path.length > 0) this.#pendingPath ??= path;
521
+ if (focus !== void 0) this.#pendingFocus ??= focus;
522
+ this.#reconcileViewState();
523
+ }
524
+ if (changedProps.has(`viewRoot`)) this.#applyViewRoot();
525
+ if (changedProps.has(`model`)) {
526
+ this.#modelUnsubscribe?.();
527
+ this.#modelUnsubscribe = void 0;
528
+ const model = this.model;
529
+ if (model) {
530
+ this.root = model.root;
531
+ this.#modelUnsubscribe = model._subscribe((event) => {
532
+ if (event.type === `root`) {
533
+ this.root = event.root;
534
+ this.requestUpdate();
535
+ } else if (event.type === `invalidate`) {
536
+ for (let depth = 0; depth < this._columns.length - 1; depth++) if (this._columns[depth]?.some((n) => n.item.key === event.key)) {
537
+ this._columns = this._columns.slice(0, depth + 1);
538
+ this.requestUpdate();
539
+ break;
540
+ }
541
+ }
542
+ });
543
+ }
544
+ }
545
+ if (this.#pendingNode?.children !== void 0 && this._loadingDepth !== void 0) {
546
+ const loadingDepth = this._loadingDepth;
547
+ const resolvedChildren = this.#pendingNode.children;
548
+ this.#pendingNode = void 0;
549
+ this.#pendingController = void 0;
550
+ this._loadingDepth = void 0;
551
+ const cols = this._columns.slice(0, loadingDepth);
552
+ if (resolvedChildren.length > 0) cols.push(resolvedChildren);
553
+ this._columns = cols;
554
+ if (this.#hasPendingViewState()) this.#reconcileViewState();
555
+ }
556
+ if (this.#pendingColumnScroll.size > 0) {
557
+ const { contents, parentKeys, keysAt } = this.#columnScrollDom();
558
+ this.#pendingColumnScroll = applyColumnScrolls(this.#pendingColumnScroll, contents, parentKeys, keysAt);
559
+ }
560
+ const prevColumns = changedProps.get(`_columns`);
561
+ const columnAdded = changedProps.has(`_columns`) && (prevColumns?.length ?? 0) < this._columns.length;
562
+ const loadingAppeared = changedProps.has(`_loadingDepth`) && this._loadingDepth !== void 0;
563
+ if (columnAdded || loadingAppeared) {
564
+ const container = this.shadowRoot?.querySelector(`#container`);
565
+ container?.scrollTo({
566
+ left: container.scrollWidth,
567
+ behavior: `smooth`
568
+ });
569
+ }
570
+ }
571
+ firstUpdated() {
572
+ const container = this.shadowRoot?.querySelector(`#container`);
573
+ if (container) onUserScrollIntent(container, () => {
574
+ this.#pendingColumnScroll = /* @__PURE__ */ new Map();
575
+ });
576
+ this.renderRoot.addEventListener(`scroll`, this.#onColumnScroll, {
577
+ capture: true,
578
+ passive: true
579
+ });
580
+ }
581
+ #onColumnScroll;
582
+ connectedCallback() {
583
+ super.connectedCallback();
584
+ this.#searchRegistry ??= createCommandRegistry();
585
+ this.#syncSearchCommands();
586
+ }
587
+ disconnectedCallback() {
588
+ super.disconnectedCallback();
589
+ this.#modelUnsubscribe?.();
590
+ this.#modelUnsubscribe = void 0;
591
+ this.#unregisterSearchCommands?.();
592
+ this.#unregisterSearchCommands = void 0;
593
+ this.#searchRegistry = void 0;
594
+ }
595
+ /**
596
+ * Snapshot the open path, selection, keyboard cursor, column widths and
597
+ * column scroll as plain JSON, keyed by `item.key`. Includes pending state
598
+ * for keys not currently loaded.
599
+ */
600
+ getViewState(options) {
601
+ const withPending = options?.includePending ?? true;
602
+ const { contents, parentKeys, keysAt } = this.#columnScrollDom();
603
+ return this.#viewState.shape({
604
+ selected: keysToArray(this._selectedKeys, withPending ? this.#pendingSelected : []),
605
+ path: (withPending ? this.#pendingPath : void 0) ?? openPathKeys(this._columns),
606
+ focus: this.#focusKey() ?? (withPending ? this.#pendingFocus : void 0),
607
+ columnWidths: Object.fromEntries(this.#columnWidths),
608
+ columnScroll: {
609
+ ...captureColumnScrolls(contents, parentKeys, keysAt),
610
+ ...withPending ? Object.fromEntries(this.#pendingColumnScroll) : {}
611
+ }
612
+ }, options);
613
+ }
614
+ /**
615
+ * Apply saved view state. Fields present replace the current ones; keys not
616
+ * loaded yet are applied as they appear — including via `loadChildren` /
617
+ * `model` for the saved path when `restoreLoads`.
618
+ */
619
+ setViewState(state) {
620
+ if (!this.#viewState.accepts(state)) return;
621
+ this.#viewState.restoring(() => {
622
+ if (state.path) this.#pendingPath = [...state.path];
623
+ if (state.selected) {
624
+ this.#pendingSelected = capKeys(new Set(state.selected));
625
+ const prev = new Set(this._selectedNodes);
626
+ this._selectedKeys = /* @__PURE__ */ new Set();
627
+ this._selectedNodes = /* @__PURE__ */ new Set();
628
+ this.#lastSelectedNode = void 0;
629
+ this.#anchor = void 0;
630
+ if (prev.size > 0) this.#dispatchSelectEvent(prev);
631
+ }
632
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
633
+ if (state.columnWidths) {
634
+ this.#columnWidths = widthsFromRecord(state.columnWidths);
635
+ this.#viewState.notify();
636
+ }
637
+ if (state.columnScroll) this.#pendingColumnScroll = new Map(Object.entries(state.columnScroll));
638
+ this.#reconcileViewState();
639
+ });
640
+ this.requestUpdate();
641
+ }
642
+ clearPendingViewState() {
643
+ this.#pendingPath = void 0;
644
+ this.#pendingSelected = EMPTY_KEYS;
645
+ this.#pendingFocus = void 0;
646
+ this.#pendingColumnScroll = /* @__PURE__ */ new Map();
647
+ }
648
+ /** An explicit selection change supersedes a saved one still waiting. */
649
+ #selectionChanged() {
650
+ if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
651
+ this.#viewState.notify();
652
+ }
653
+ #hasPendingViewState() {
654
+ return this.#pendingPath !== void 0 || this.#pendingSelected.size > 0 || this.#pendingFocus !== void 0;
655
+ }
656
+ #tickledKey() {
657
+ const id = this.tickled.tickledId;
658
+ const colon = id?.indexOf(`:`) ?? -1;
659
+ return id === void 0 || colon < 0 ? void 0 : id.substring(colon + 1);
660
+ }
661
+ #focusKey() {
662
+ return this.#keyboardFocus ? this.#tickledKey() : void 0;
663
+ }
664
+ #columnScrollDom() {
665
+ return columnScrollDom(this.shadowRoot, this._columns, this.#displayRoot?.item.key, this.filterPredicate);
666
+ }
667
+ /**
668
+ * Move state whose keys are no longer loaded to pending; apply pending state
669
+ * whose keys now are (pure plans in miller-view-state.ts). Runs on every
670
+ * data change and on `setViewState()`.
671
+ */
672
+ #reconcileViewState() {
673
+ const rootColumn = this.#displayRoot?.children;
674
+ if (!rootColumn) return;
675
+ const index = indexLoaded(rootColumn);
676
+ this.#viewState.restoring(() => {
677
+ if (this.#pendingPath) this.#applyPathPlan(planOpenPath(rootColumn, this.#pendingPath, this._columns, this._selectedByDepth, this.#pendingNode));
678
+ const sel = planSelection(this._selectedKeys, this.#pendingSelected, this._selectedNodes, this.#lastSelectedNode, index, this._columns, this.interactionMode === `implicit`);
679
+ if (sel) this.#applySelectionPlan(sel);
680
+ const focus = planFocus(this.#tickledKey(), this.#keyboardFocus, this.#pendingFocus, index, this._columns);
681
+ if (focus.clear) this.tickled.clearTickled();
682
+ this.#pendingFocus = focus.pending;
683
+ if (focus.tickle) {
684
+ this.tickled.setTickled(this.#buildTickledId(focus.tickle.node, focus.tickle.depth));
685
+ this.#keyboardFocus = true;
686
+ this.#viewState.notify();
687
+ }
688
+ });
689
+ }
690
+ #applyPathPlan(plan) {
691
+ if (plan.columns) {
692
+ if (this.#pendingNode) this.#abortPending();
693
+ this._columns = plan.columns;
694
+ this._selectedByDepth = plan.selectedByDepth;
695
+ }
696
+ this.#pendingPath = plan.pending;
697
+ if (plan.load && this.restoreLoads && (this.model || this.loadChildren)) this.#loadUnloadedChildren(plan.load.node, plan.load.depth, true, true);
698
+ if (!sameColumns(this._columns, this.#columnsSeen)) {
699
+ this.#columnsSeen = this._columns;
700
+ this.#viewState.notify();
701
+ }
702
+ }
703
+ #applySelectionPlan(plan) {
704
+ const prev = this._selectedNodes;
705
+ this._selectedKeys = plan.keys;
706
+ this._selectedNodes = plan.nodes;
707
+ this.#pendingSelected = plan.pending;
708
+ this.#lastSelectedNode = plan.last;
709
+ if (this.interactionMode === `implicit`) for (const [depth, node] of plan.placed) this._selectedByDepth.set(depth, node);
710
+ if (!plan.restored) {
711
+ this.#viewState.notify();
712
+ return;
713
+ }
714
+ const origin = this.#selectOrigin;
715
+ this.#selectOrigin = `programmatic`;
716
+ try {
717
+ this.#dispatchSelectEvent(prev);
718
+ } finally {
719
+ this.#selectOrigin = origin;
720
+ }
721
+ }
722
+ /** Read current selection as an immutable set of item keys. */
723
+ get selectedKeys() {
724
+ return this._selectedKeys;
725
+ }
726
+ /** Replace entire selection and trigger a re-render. */
727
+ set selectedKeys(keys) {
728
+ this._selectedKeys = new Set(keys);
729
+ this.#selectionChanged();
730
+ this.requestUpdate();
731
+ }
732
+ /** The most recently selected node. */
733
+ get selectedNode() {
734
+ return this.#lastSelectedNode;
735
+ }
736
+ set selectedNode(node) {
737
+ this.#lastSelectedNode = node;
738
+ if (node) {
739
+ if (this.interactionMode === `implicit` || this.interactionMode === `standard`) {
740
+ this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
741
+ this.#selectionChanged();
742
+ }
743
+ }
744
+ this.requestUpdate();
745
+ }
746
+ /** Full selection set (implements TreeComponent interface). */
747
+ get selectedNodes() {
748
+ return this._selectedNodes;
749
+ }
750
+ /** Add `node` to the selection (respects selectionFilter). Dispatches `select` event. */
751
+ select(node) {
752
+ if (!this.#canSelect(node)) return;
753
+ const prev = new Set(this._selectedNodes);
754
+ this._selectedNodes.add(node);
755
+ this._selectedKeys.add(node.item.key);
756
+ this.#lastSelectedNode = node;
757
+ this.#dispatchSelectEvent(prev);
758
+ this.requestUpdate();
759
+ }
760
+ /** Remove `node` from the selection. Dispatches `select` event. */
761
+ deselect(node) {
762
+ if (!this._selectedNodes.has(node)) return;
763
+ const prev = new Set(this._selectedNodes);
764
+ this._selectedNodes.delete(node);
765
+ this._selectedKeys.delete(node.item.key);
766
+ if (this.#lastSelectedNode === node) this.#lastSelectedNode = [...this._selectedNodes].at(-1);
767
+ this.#dispatchSelectEvent(prev);
768
+ this.requestUpdate();
769
+ }
770
+ /**
771
+ * Clear all selection and trigger a re-render.
772
+ */
773
+ clearSelection() {
774
+ this.#selectionChanged();
775
+ const prev = new Set(this._selectedNodes);
776
+ this._selectedKeys = /* @__PURE__ */ new Set();
777
+ this._selectedNodes.clear();
778
+ this.#lastSelectedNode = void 0;
779
+ if (prev.size > 0) this.#dispatchSelectEvent(prev);
780
+ this.requestUpdate();
781
+ }
782
+ /** Not implemented on miller-list. */
783
+ selectAll() {}
784
+ /** Not implemented on miller-list. */
785
+ deselectAll() {}
786
+ /**
787
+ * Select or deselect a single node by key.
788
+ * In `checked` interactionMode this also recursively selects/deselects all loaded descendants.
789
+ */
790
+ setSelected(node, selected) {
791
+ if (this.interactionMode === `checked`) this.#applyCheckedToggle(node, selected);
792
+ else if (selected) {
793
+ this._selectedKeys.add(node.item.key);
794
+ this._selectedNodes.add(node);
795
+ } else {
796
+ this._selectedKeys.delete(node.item.key);
797
+ this._selectedNodes.delete(node);
798
+ }
799
+ this.#selectionChanged();
800
+ this.requestUpdate();
801
+ }
802
+ /**
803
+ * Return all MillerNodes whose key is in `_selectedKeys`, searching only
804
+ * the currently visible columns.
805
+ */
806
+ getSelected() {
807
+ const results = [];
808
+ for (const col of this._columns) for (const node of col) if (this._selectedKeys.has(node.item.key)) results.push(node);
809
+ return results;
810
+ }
811
+ /**
812
+ * Programmatically navigate to a node by key path, relative to the current
813
+ * view root (rebased or full tree).
814
+ */
815
+ navigateTo(keys, options = {}) {
816
+ const display = this.#displayRoot;
817
+ if (!display?.children) return false;
818
+ const prevOrigin = this.#selectOrigin;
819
+ const prevSilent = this.#selectSilent;
820
+ this.#selectOrigin = `programmatic`;
821
+ this.#selectSilent = options.silent === true;
822
+ try {
823
+ return this.#navigateTo(display.children, keys, options.reveal ?? `edge`);
824
+ } finally {
825
+ this.#selectOrigin = prevOrigin;
826
+ this.#selectSilent = prevSilent;
827
+ }
828
+ }
829
+ /**
830
+ * Re-sync the element with children arrays patched in place (see
831
+ * `MillerTreeController.sync()`): columns whose selected node was removed are
832
+ * closed, selection entries for removed nodes are dropped, then it re-renders.
833
+ */
834
+ invalidateStructure() {
835
+ const path = openPathKeys(this._columns);
836
+ if (path.length > 0) this.#pendingPath ??= path;
837
+ this.#reconcileViewState();
838
+ this.requestUpdate();
839
+ }
840
+ #selectOrigin;
841
+ #selectSilent;
842
+ #navigateTo(rootColumn, keys, reveal) {
843
+ this.#abortPending();
844
+ this.#pendingPath = void 0;
845
+ this._selectedByDepth.clear();
846
+ this.tickled.clearTickled();
847
+ const newColumns = [rootColumn];
848
+ let currentColumn = rootColumn;
849
+ let success = true;
850
+ let lastNode;
851
+ let lastDepth = 0;
852
+ for (let depth = 0; depth < keys.length; depth++) {
853
+ const idx = currentColumn.findIndex((n) => n.item.key === keys[depth]);
854
+ if (idx < 0) {
855
+ success = false;
856
+ break;
857
+ }
858
+ const node = currentColumn[idx];
859
+ this._selectedByDepth.set(depth, node);
860
+ lastNode = node;
861
+ lastDepth = depth;
862
+ if (node.item.isLeaf || !node.children || node.children.length === 0) {
863
+ if (depth < keys.length - 1) success = false;
864
+ break;
865
+ }
866
+ newColumns.push(node.children);
867
+ currentColumn = node.children;
868
+ }
869
+ this._columns = newColumns;
870
+ if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
871
+ if (this.interactionMode !== `implicit` && this.interactionMode !== `manual`) {
872
+ const leafKey = keys.at(-1);
873
+ if (leafKey) {
874
+ this._selectedKeys = /* @__PURE__ */ new Set([leafKey]);
875
+ this.#selectionChanged();
876
+ }
877
+ }
878
+ this.requestUpdate();
879
+ this.#scrollNavigatedItemIntoView(reveal === `center-if-hidden` ? `auto` : `smooth`, reveal);
880
+ return success;
881
+ }
882
+ /**
883
+ * Programmatically navigate to the first node (breadth-first) matching a predicate.
884
+ */
885
+ navigateToNode(predicate) {
886
+ const display = this.#displayRoot;
887
+ if (!display?.children) return false;
888
+ const keyPath = this.#findNodePath(display.children, predicate);
889
+ if (!keyPath) return false;
890
+ return this.navigateTo(keyPath);
891
+ }
892
+ /** Open the columns down to `key` and move the cursor to it, without changing the selection (search "navigate" mode). */
893
+ revealKey(key) {
894
+ const display = this.#displayRoot;
895
+ if (!display?.children) return false;
896
+ const path = this.#findNodePath(display.children, (n) => n.item.key === key);
897
+ if (!path) return false;
898
+ this.#pendingPath = void 0;
899
+ const columns = [display.children];
900
+ let current = display.children;
901
+ let lastNode;
902
+ let lastDepth = 0;
903
+ for (let depth = 0; depth < path.length; depth++) {
904
+ const node = current.find((n) => n.item.key === path[depth]);
905
+ if (!node) return false;
906
+ lastNode = node;
907
+ lastDepth = depth;
908
+ if (node.children && node.children.length > 0) {
909
+ columns.push(node.children);
910
+ current = node.children;
911
+ } else break;
912
+ }
913
+ this._columns = columns;
914
+ if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
915
+ this.requestUpdate();
916
+ this.#scrollNavigatedItemIntoView();
917
+ return true;
918
+ }
919
+ /** Replace the selection with `nodes` (search "select" mode). */
920
+ selectMany(nodes) {
921
+ const arr = this.selectionFilter === `none` ? [] : [...nodes];
922
+ const prev = new Set(this._selectedNodes);
923
+ this._selectedNodes = new Set(arr);
924
+ this._selectedKeys = new Set(arr.map((n) => n.item.key));
925
+ this.#lastSelectedNode = arr.at(-1);
926
+ this.#dispatchSelectEvent(prev);
927
+ this.requestUpdate();
928
+ }
929
+ getNodes() {
930
+ const display = this.#displayRoot;
931
+ if (!display?.children) return [];
932
+ const result = [];
933
+ const visit = (node, depth) => {
934
+ result.push({
935
+ node,
936
+ depth
937
+ });
938
+ if (node.children) for (const child of node.children) visit(child, depth + 1);
939
+ };
940
+ for (const child of display.children) visit(child, 0);
941
+ return result;
942
+ }
943
+ #findNodePath(nodes, predicate, prefix = []) {
944
+ for (const node of nodes) {
945
+ const path = [...prefix, node.item.key];
946
+ if (predicate(node)) return path;
947
+ if (node.children?.length) {
948
+ const result = this.#findNodePath(node.children, predicate, path);
949
+ if (result) return result;
950
+ }
951
+ }
952
+ }
953
+ /**
954
+ * Re-render the columns for the current `#displayRoot`, clearing selection.
955
+ * Runs on every `viewRoot` change.
956
+ */
957
+ #applyViewRoot() {
958
+ this.#abortPending();
959
+ const prev = new Set(this._selectedNodes);
960
+ this._selectedByDepth.clear();
961
+ this._selectedKeys = /* @__PURE__ */ new Set();
962
+ this._selectedNodes.clear();
963
+ this.#lastSelectedNode = void 0;
964
+ this.#anchor = void 0;
965
+ this.tickled.clearTickled();
966
+ const display = this.#displayRoot;
967
+ if (display?.children !== void 0) {
968
+ this._columns = display.children ? [display.children] : [];
969
+ this._loadingDepth = void 0;
970
+ } else if (display) {
971
+ this._columns = [];
972
+ this.#loadUnloadedChildren(display, -1, false);
973
+ } else {
974
+ this._columns = [];
975
+ this._loadingDepth = void 0;
976
+ }
977
+ if (prev.size > 0) this.#dispatchSelectEvent(prev);
978
+ if (this.#hasPendingViewState()) this.#reconcileViewState();
979
+ }
980
+ /**
981
+ * Rebase the view to show `target`'s children in column 0 (Finder column-view
982
+ * behavior on double-click), keeping the full tree in `root`.
983
+ *
984
+ * - Pass a `MillerNode` (used as-is) or a key (looked up in the full tree).
985
+ * - Pass `undefined` to restore the full-tree view.
986
+ * - Rebasing to the true root is equivalent to restoring the full view.
987
+ * - Leaves are refused (returns `false`).
988
+ *
989
+ * Selection is cleared; all depths, headers, and key paths are relative to
990
+ * the new view root. Fires a `rebase` event when the view actually changes.
991
+ * Returns `false` when nothing changed (unknown key, leaf, or same view).
992
+ */
993
+ rebaseTo(target) {
994
+ if (target === void 0) return this.#clearViewRoot();
995
+ const node = typeof target === `string` ? this.#findNodeByKey(target) : target;
996
+ if (!node || node.item.isLeaf === true) return false;
997
+ if (node === this.viewRoot || node.item.key === this.viewRoot?.item.key) return false;
998
+ if (node === this.root || !this.viewRoot && node.item.key === this.root?.item.key) return this.#clearViewRoot();
999
+ this.viewRoot = node;
1000
+ this.#dispatchRebaseEvent();
1001
+ return true;
1002
+ }
1003
+ /**
1004
+ * Rebase one level up from the current view root (no-op at the full tree).
1005
+ * Fires a `rebase` event when the view actually changes.
1006
+ */
1007
+ rebaseUp() {
1008
+ if (!this.viewRoot) return false;
1009
+ const parent = this.#findParentOf(this.viewRoot);
1010
+ if (!parent) return false;
1011
+ if (parent === this.root) return this.#clearViewRoot();
1012
+ this.viewRoot = parent;
1013
+ this.#dispatchRebaseEvent();
1014
+ return true;
1015
+ }
1016
+ #clearViewRoot() {
1017
+ if (this.viewRoot === void 0) return false;
1018
+ this.viewRoot = void 0;
1019
+ this.#dispatchRebaseEvent();
1020
+ return true;
1021
+ }
1022
+ #dispatchRebaseEvent() {
1023
+ this.#pendingPath = void 0;
1024
+ const viewRoot = this.viewRoot;
1025
+ const detail = {
1026
+ viewRoot,
1027
+ path: viewRoot ? this.#pathToRoot(viewRoot) : []
1028
+ };
1029
+ this.dispatchEvent(new CustomEvent(`rebase`, {
1030
+ detail,
1031
+ bubbles: true,
1032
+ composed: true
1033
+ }));
1034
+ }
1035
+ #findNodeByKey(key) {
1036
+ if (!this.root) return void 0;
1037
+ if (this.root.item.key === key) return this.root;
1038
+ return this.#findNodeIn(this.root.children ?? [], key);
1039
+ }
1040
+ #findNodeIn(nodes, key) {
1041
+ for (const node of nodes) {
1042
+ if (node.item.key === key) return node;
1043
+ if (node.children) {
1044
+ const found = this.#findNodeIn(node.children, key);
1045
+ if (found) return found;
1046
+ }
1047
+ }
1048
+ }
1049
+ /** Find the parent of `target` (by reference, falling back to key) in the full tree. */
1050
+ #findParentOf(target) {
1051
+ if (!this.root || target === this.root) return void 0;
1052
+ return this.#findParentIn(this.root, target);
1053
+ }
1054
+ #findParentIn(node, target) {
1055
+ if (!node.children) return void 0;
1056
+ for (const child of node.children) {
1057
+ if (child === target || child.item.key === target.item.key) return node;
1058
+ const found = this.#findParentIn(child, target);
1059
+ if (found) return found;
1060
+ }
1061
+ }
1062
+ /** Ancestry chain from the true root down to and including `target`. */
1063
+ #pathToRoot(target) {
1064
+ const chain = [target];
1065
+ let current = this.#findParentOf(target);
1066
+ while (current) {
1067
+ chain.unshift(current);
1068
+ if (current === this.root) break;
1069
+ current = this.#findParentOf(current);
1070
+ }
1071
+ return chain;
1072
+ }
1073
+ renderColumnContent(nodes, depth) {
1074
+ console.warn(`ixfx-miller-base: renderColumnContent not overridden. depth=${depth}, nodes=${nodes.length}`);
1075
+ return nothing;
1076
+ }
1077
+ _handleItemClick(node, depth, event) {
1078
+ this.#onItemClick(node, depth, event);
1079
+ }
1080
+ /**
1081
+ * Called by subclasses (in `checked` interactionMode) when a checkbox is clicked.
1082
+ */
1083
+ _handleCheckboxClick(event, node, _depth) {
1084
+ event.stopPropagation();
1085
+ this.#onCheckboxClick(node);
1086
+ }
1087
+ /**
1088
+ * Called by subclasses when an item is double-clicked.
1089
+ */
1090
+ _handleItemDblClick(node, depth, _event) {
1091
+ this.dispatchEvent(new CustomEvent(`item-dblclick`, {
1092
+ detail: {
1093
+ node,
1094
+ depth
1095
+ },
1096
+ bubbles: true,
1097
+ composed: true
1098
+ }));
1099
+ }
1100
+ /**
1101
+ * Called by subclasses when an item is right-clicked.
1102
+ */
1103
+ _handleItemRightClick(node, depth, event) {
1104
+ this.dispatchEvent(new CustomEvent(`item-rightclick`, {
1105
+ detail: {
1106
+ node,
1107
+ depth,
1108
+ mouseEvent: event
1109
+ },
1110
+ bubbles: true,
1111
+ composed: true
1112
+ }));
1113
+ }
1114
+ /**
1115
+ * Called by subclasses when a column header is right-clicked.
1116
+ */
1117
+ _handleHeaderRightClick(event, depth) {
1118
+ const label = this.#getColumnHeader(depth);
1119
+ const path = this.#buildPath(depth);
1120
+ this.dispatchEvent(new CustomEvent(`header-rightclick`, {
1121
+ detail: {
1122
+ depth,
1123
+ label,
1124
+ path,
1125
+ mouseEvent: event
1126
+ },
1127
+ bubbles: true,
1128
+ composed: true
1129
+ }));
1130
+ }
1131
+ _isSelected(node, depth) {
1132
+ if (this.interactionMode === `implicit`) return this._selectedByDepth.get(depth) === node;
1133
+ return this._selectedKeys.has(node.item.key);
1134
+ }
1135
+ #isAncestor(node, depth) {
1136
+ const nextCol = this._columns[depth + 1];
1137
+ return nextCol !== void 0 && node.children === nextCol;
1138
+ }
1139
+ _isOpen(node, depth) {
1140
+ return this.#isAncestor(node, depth);
1141
+ }
1142
+ /**
1143
+ * Return the display state for an item identified by key at a given depth.
1144
+ */
1145
+ getItemState(key, depth) {
1146
+ const node = this._columns[depth]?.find((n) => n.item.key === key);
1147
+ if (!node) return void 0;
1148
+ return {
1149
+ selected: this._isSelected(node, depth),
1150
+ open: this.#isAncestor(node, depth),
1151
+ focused: this._isFocused(node, depth),
1152
+ isLeaf: node.item.isLeaf ?? false
1153
+ };
1154
+ }
1155
+ #canSelect(node) {
1156
+ if (this.selectionFilter === `none`) return false;
1157
+ if (this.selectionFilter === `leaf`) return node.item.isLeaf !== false;
1158
+ if (this.selectionFilter === `branch`) return node.item.isLeaf === false || node.children !== void 0 && node.children.length > 0;
1159
+ return true;
1160
+ }
1161
+ #dispatchSelectEvent(previous) {
1162
+ this.#selectionChanged();
1163
+ if (this.#selectSilent) return;
1164
+ const detail = {
1165
+ selected: new Set(this._selectedNodes),
1166
+ previous,
1167
+ origin: this.#selectOrigin
1168
+ };
1169
+ this.dispatchEvent(new CustomEvent(`select`, {
1170
+ detail,
1171
+ bubbles: true,
1172
+ composed: true
1173
+ }));
1174
+ }
1175
+ #abortPending() {
1176
+ if (this.#expandTimer !== void 0) {
1177
+ clearTimeout(this.#expandTimer);
1178
+ this.#expandTimer = void 0;
1179
+ }
1180
+ if (this.#pendingController) {
1181
+ const node = this.#pendingNode;
1182
+ const depth = this._loadingDepth;
1183
+ this.#pendingController.abort();
1184
+ this.#pendingController = void 0;
1185
+ this.#pendingNode = void 0;
1186
+ this._loadingDepth = void 0;
1187
+ if (node && depth !== void 0) this.dispatchEvent(new CustomEvent(`collapse`, {
1188
+ detail: {
1189
+ node,
1190
+ depth
1191
+ },
1192
+ bubbles: true,
1193
+ composed: true
1194
+ }));
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Build the ancestry path for a given column depth.
1199
+ */
1200
+ #buildPath(depth) {
1201
+ const path = [];
1202
+ if (this.#displayRoot) path.push(this.#displayRoot);
1203
+ for (let d = 0; d < depth; d++) {
1204
+ const sel = this._selectedByDepth.get(d);
1205
+ if (sel) path.push(sel);
1206
+ }
1207
+ return path;
1208
+ }
1209
+ #onHeaderClick(depth) {
1210
+ const detail = {
1211
+ depth,
1212
+ label: this.#getColumnHeader(depth),
1213
+ path: this.#buildPath(depth)
1214
+ };
1215
+ this.dispatchEvent(new CustomEvent(`header-click`, {
1216
+ detail,
1217
+ bubbles: true,
1218
+ composed: true
1219
+ }));
1220
+ }
1221
+ #onItemClick(node, depth, event) {
1222
+ if (this._loadingDepth !== void 0 && this._loadingDepth > depth) this.#abortPending();
1223
+ this.tickled.setTickled(this.#buildTickledId(node, depth));
1224
+ this.#keyboardFocus = false;
1225
+ this.#pendingFocus = void 0;
1226
+ this.dispatchEvent(new CustomEvent(`item-click`, {
1227
+ detail: {
1228
+ node,
1229
+ depth
1230
+ },
1231
+ bubbles: true,
1232
+ composed: true
1233
+ }));
1234
+ switch (this.interactionMode) {
1235
+ case `implicit`:
1236
+ this.#handleImplicitClick(node, depth);
1237
+ break;
1238
+ case `standard`:
1239
+ this.#handleStandardClick(node, depth, event);
1240
+ break;
1241
+ case `vscode`:
1242
+ this.#handleStandardClick(node, depth, event);
1243
+ break;
1244
+ case `manual`: break;
1245
+ case `checked`: this.#expandNode(node, depth, true);
1246
+ }
1247
+ }
1248
+ #handleImplicitClick(node, depth) {
1249
+ const previous = new Set(this._selectedNodes);
1250
+ this._selectedByDepth.set(depth, node);
1251
+ this.#lastSelectedNode = node;
1252
+ this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
1253
+ this._selectedNodes.clear();
1254
+ this._selectedNodes.add(node);
1255
+ this.#expandNode(node, depth, true);
1256
+ if (!previous.has(node)) this.#dispatchSelectEvent(previous);
1257
+ }
1258
+ #handleStandardClick(node, depth, event) {
1259
+ const col = this._columns[depth];
1260
+ const index = col ? col.indexOf(node) : -1;
1261
+ const isMeta = event ? event.metaKey || event.ctrlKey : false;
1262
+ const isShift = event ? event.shiftKey : false;
1263
+ if (isMeta) {
1264
+ if (this._selectedKeys.has(node.item.key)) {
1265
+ this._selectedKeys.delete(node.item.key);
1266
+ this._selectedNodes.delete(node);
1267
+ } else {
1268
+ this._selectedKeys.add(node.item.key);
1269
+ this._selectedNodes.add(node);
1270
+ }
1271
+ this.#anchor = index >= 0 ? {
1272
+ key: node.item.key,
1273
+ depth,
1274
+ index
1275
+ } : this.#anchor;
1276
+ this.#selectionChanged();
1277
+ this.requestUpdate();
1278
+ return;
1279
+ }
1280
+ if (isShift && this.#anchor && this.#anchor.depth === depth && col) {
1281
+ const from = Math.min(this.#anchor.index, index);
1282
+ const to = Math.max(this.#anchor.index, index);
1283
+ const rangeNodes = col.slice(from, to + 1);
1284
+ this._selectedKeys = new Set(rangeNodes.map((n) => n.item.key));
1285
+ this._selectedNodes = new Set(rangeNodes);
1286
+ this.#selectionChanged();
1287
+ this.requestUpdate();
1288
+ return;
1289
+ }
1290
+ const previous = new Set(this._selectedNodes);
1291
+ this._selectedKeys = /* @__PURE__ */ new Set([node.item.key]);
1292
+ this._selectedNodes = /* @__PURE__ */ new Set([node]);
1293
+ this.#anchor = index >= 0 ? {
1294
+ key: node.item.key,
1295
+ depth,
1296
+ index
1297
+ } : void 0;
1298
+ this._selectedByDepth.set(depth, node);
1299
+ this.#lastSelectedNode = node;
1300
+ this.#expandNode(node, depth, true);
1301
+ if (!previous.has(node) || previous.size !== 1) this.#dispatchSelectEvent(previous);
1302
+ }
1303
+ /**
1304
+ * Open the column for `node`. `immediate` skips the lazy-load debounce: use
1305
+ * it for deliberate clicks, where the user has committed to this node. The
1306
+ * debounce is kept for keyboard navigation, which can sweep over many
1307
+ * branches in quick succession.
1308
+ */
1309
+ #expandNode(node, depth, immediate = false) {
1310
+ this.#pendingPath = void 0;
1311
+ if (node.item.isLeaf) {
1312
+ this._columns = this._columns.slice(0, depth + 1);
1313
+ this.requestUpdate();
1314
+ return;
1315
+ }
1316
+ if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true, immediate);
1317
+ else {
1318
+ const newColumns = this._columns.slice(0, depth + 1);
1319
+ newColumns.push(node.children);
1320
+ this._columns = newColumns;
1321
+ this.dispatchEvent(new CustomEvent(`expand`, {
1322
+ detail: {
1323
+ node,
1324
+ depth
1325
+ },
1326
+ bubbles: true,
1327
+ composed: true
1328
+ }));
1329
+ }
1330
+ }
1331
+ /**
1332
+ * Fetch children for a node whose `children === undefined` and show them as
1333
+ * the column after `parentDepth` (with the loading placeholder meanwhile).
1334
+ * Shared by branch expansion (`parentDepth` = the branch's column) and view
1335
+ * rebasing (`parentDepth` = -1, so the children become column 0).
1336
+ *
1337
+ * The `depth` reported to `loadChildren`/`model.getChildren` is the node's
1338
+ * own depth: its column for expansions, -1 for a view root (it sits above
1339
+ * column 0, so loaders computing child depth as `depth + 1` stay correct).
1340
+ */
1341
+ #loadUnloadedChildren(node, parentDepth, fireExpandEvent, immediate = false) {
1342
+ const nodeDepth = parentDepth + 1;
1343
+ this._columns = this._columns.slice(0, nodeDepth);
1344
+ this._loadingDepth = nodeDepth;
1345
+ this.#pendingController?.abort();
1346
+ this.#pendingNode = node;
1347
+ const controller = new AbortController();
1348
+ this.#pendingController = controller;
1349
+ if (fireExpandEvent) this.dispatchEvent(new CustomEvent(`expand`, {
1350
+ detail: {
1351
+ node,
1352
+ depth: parentDepth
1353
+ },
1354
+ bubbles: true,
1355
+ composed: true
1356
+ }));
1357
+ const applyChildren = (children) => {
1358
+ if (controller.signal.aborted) return;
1359
+ node.children = children;
1360
+ this.#pendingNode = void 0;
1361
+ this.#pendingController = void 0;
1362
+ this._loadingDepth = void 0;
1363
+ const cols = this._columns.slice(0, nodeDepth);
1364
+ if (children.length > 0) cols.push(children);
1365
+ this._columns = cols;
1366
+ if (this.#hasPendingViewState()) this.#reconcileViewState();
1367
+ };
1368
+ const handleError = (err) => {
1369
+ if (err.name !== `AbortError`) console.error(err);
1370
+ node.children = [];
1371
+ this.#pendingNode = void 0;
1372
+ this.#pendingController = void 0;
1373
+ this._loadingDepth = void 0;
1374
+ if (this.#hasPendingViewState()) this.#reconcileViewState();
1375
+ this.requestUpdate();
1376
+ };
1377
+ if (this.model || this.loadChildren) {
1378
+ if (this.#expandTimer !== void 0) {
1379
+ clearTimeout(this.#expandTimer);
1380
+ this.#expandTimer = void 0;
1381
+ }
1382
+ const load = () => {
1383
+ this.#expandTimer = void 0;
1384
+ if (this.#pendingNode !== node) return;
1385
+ (this.model ? this.model.getChildren(node, nodeDepth - 1, controller.signal) : collectDataResult(this.loadChildren({
1386
+ node,
1387
+ depth: nodeDepth - 1
1388
+ }, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
1389
+ };
1390
+ if (immediate) load();
1391
+ else this.#expandTimer = setTimeout(load, EXPAND_DEBOUNCE_MS);
1392
+ } else {
1393
+ this._loadingDepth = void 0;
1394
+ this.#pendingNode = void 0;
1395
+ this.#pendingController = void 0;
1396
+ const newColumns = this._columns.slice(0, nodeDepth);
1397
+ newColumns.push([]);
1398
+ this._columns = newColumns;
1399
+ }
1400
+ }
1401
+ #onCheckboxClick(node) {
1402
+ const previous = new Set(this._selectedNodes);
1403
+ const currentlySelected = this._selectedKeys.has(node.item.key);
1404
+ if (!!node.children?.length ? this.#allDescendantsSelected(node) : currentlySelected) this.#applyCheckedToggle(node, false);
1405
+ else this.#applyCheckedToggle(node, true);
1406
+ this.requestUpdate();
1407
+ this.#dispatchSelectEvent(previous);
1408
+ }
1409
+ #allDescendantsSelected(node) {
1410
+ if (!this._selectedKeys.has(node.item.key)) return false;
1411
+ if (!node.children?.length) return true;
1412
+ return node.children.every((child) => this.#allDescendantsSelected(child));
1413
+ }
1414
+ #applyCheckedToggle(node, selected) {
1415
+ if (selected) {
1416
+ this._selectedKeys.add(node.item.key);
1417
+ this._selectedNodes.add(node);
1418
+ } else {
1419
+ this._selectedKeys.delete(node.item.key);
1420
+ this._selectedNodes.delete(node);
1421
+ }
1422
+ if (node.children) for (const child of node.children) this.#applyCheckedToggle(child, selected);
1423
+ }
1424
+ #getTickledDepthOrDefault() {
1425
+ return this.#getTickledDepth() ?? 0;
1426
+ }
1427
+ #getTickledNodeAndDepth() {
1428
+ const id = this.tickled.tickledId;
1429
+ if (!id) return void 0;
1430
+ const colonIndex = id.indexOf(`:`);
1431
+ if (colonIndex < 0) return void 0;
1432
+ const depth = Number(id.substring(0, colonIndex));
1433
+ const key = id.substring(colonIndex + 1);
1434
+ const col = this._columns[depth];
1435
+ if (!col) return void 0;
1436
+ const index = col.findIndex((n) => n.item.key === key);
1437
+ if (index < 0) return void 0;
1438
+ return {
1439
+ node: col[index],
1440
+ depth,
1441
+ index
1442
+ };
1443
+ }
1444
+ #scrollNavigatedItemIntoView(behavior = `smooth`, reveal = `edge`) {
1445
+ requestAnimationFrame(() => {
1446
+ const tickled = this.#getTickledNodeAndDepth();
1447
+ if (!tickled) return;
1448
+ const container = this.shadowRoot?.querySelectorAll(`.column`)[tickled.depth]?.querySelector(`.column-content`);
1449
+ if (!container) return;
1450
+ const item = container.querySelectorAll(`.item`)[tickled.index];
1451
+ if (!item) return;
1452
+ if (reveal === `center-if-hidden`) {
1453
+ const rect = item.getBoundingClientRect();
1454
+ const box = container.getBoundingClientRect();
1455
+ const hidden = rect.top < box.top || rect.bottom > box.bottom;
1456
+ item.scrollIntoView({
1457
+ block: hidden ? `center` : `nearest`,
1458
+ inline: `nearest`,
1459
+ behavior
1460
+ });
1461
+ return;
1462
+ }
1463
+ item.scrollIntoView({
1464
+ block: `nearest`,
1465
+ behavior
1466
+ });
1467
+ });
1468
+ }
1469
+ #setTickledNode(node, depth) {
1470
+ this.tickled.setTickled(this.#buildTickledId(node, depth));
1471
+ this.#keyboardFocus = true;
1472
+ this.#pendingFocus = void 0;
1473
+ this.#viewState.notify();
1474
+ this.dispatchEvent(new CustomEvent(`tickle`, {
1475
+ detail: {
1476
+ node,
1477
+ depth
1478
+ },
1479
+ bubbles: true,
1480
+ composed: true
1481
+ }));
1482
+ }
1483
+ /**
1484
+ * Number of items a PageUp/PageDown moves: how many rows fit in the column's
1485
+ * visible area, less one so the previous page's last row stays in view.
1486
+ */
1487
+ #pageSize(depth) {
1488
+ const content = this.shadowRoot?.querySelectorAll(`.column`)[depth]?.querySelector(`.column-content`);
1489
+ const item = content?.querySelector(`.item`);
1490
+ if (!content || !item || item.offsetHeight === 0) return 10;
1491
+ return Math.max(1, Math.floor(content.clientHeight / item.offsetHeight) - 1);
1492
+ }
1493
+ /**
1494
+ * Move the cursor within column `col` to the index returned by `target`
1495
+ * (given the current cursor index, or -1 when the cursor isn't in this
1496
+ * column, which behaves as "before the first item"). Handles Shift range
1497
+ * extension and anchor seating for `standard` / `vscode` modes.
1498
+ * `scroll` brings the new item into view (for jumps larger than one row).
1499
+ */
1500
+ #moveCursor(col, currentDepth, event, target, scroll = false) {
1501
+ const current = this.#getTickledNodeAndDepth();
1502
+ const targetIndex = target(current !== void 0 && current.depth === currentDepth ? current.index : -1);
1503
+ const targetNode = col[targetIndex];
1504
+ if (!targetNode) return;
1505
+ this.#setTickledNode(targetNode, currentDepth);
1506
+ if (this.interactionMode === `standard` || this.interactionMode === `vscode`) {
1507
+ const prevIndex = current?.index ?? 0;
1508
+ if (event.shiftKey) {
1509
+ if (!this.#anchor || this.#anchor.depth !== currentDepth) {
1510
+ const anchorNode = col[prevIndex];
1511
+ if (anchorNode) this.#anchor = {
1512
+ key: anchorNode.item.key,
1513
+ depth: currentDepth,
1514
+ index: prevIndex,
1515
+ selectionSnapshot: this.interactionMode === `vscode` ? new Set(this._selectedKeys) : void 0
1516
+ };
1517
+ }
1518
+ if (this.#anchor) {
1519
+ const from = Math.min(this.#anchor.index, targetIndex);
1520
+ const to = Math.max(this.#anchor.index, targetIndex);
1521
+ const rangeKeys = col.slice(from, to + 1).map((n) => n.item.key);
1522
+ if (this.interactionMode === `vscode`) {
1523
+ const snap = this.#anchor.selectionSnapshot;
1524
+ for (const k of rangeKeys) if (snap.has(k)) this._selectedKeys.delete(k);
1525
+ else this._selectedKeys.add(k);
1526
+ } else this._selectedKeys = new Set(rangeKeys);
1527
+ this.#selectionChanged();
1528
+ }
1529
+ } else if (this.interactionMode === `standard`) this.#anchor = {
1530
+ key: targetNode.item.key,
1531
+ depth: currentDepth,
1532
+ index: targetIndex
1533
+ };
1534
+ else this.#anchor = void 0;
1535
+ }
1536
+ this.requestUpdate();
1537
+ if (scroll) this.#scrollNavigatedItemIntoView(`auto`);
1538
+ }
1539
+ #onKeyDown;
1540
+ #onResizeStart;
1541
+ #resizeRaf;
1542
+ #resizePendingWidth;
1543
+ #onResizeMove;
1544
+ #applyResizeWidth;
1545
+ #onResizeEnd;
1546
+ #getColumnHeader(depth) {
1547
+ if (depth === 0) return this.#displayRoot?.item.label ?? ``;
1548
+ const explicit = this._selectedByDepth.get(depth - 1);
1549
+ if (explicit) return explicit.item.label;
1550
+ const prevCol = this._columns[depth - 1];
1551
+ const thisCol = this._columns[depth];
1552
+ if (prevCol && thisCol) {
1553
+ const parent = prevCol.find((n) => n.children === thisCol);
1554
+ if (parent) return parent.item.label;
1555
+ }
1556
+ return ``;
1557
+ }
1558
+ #explicitWidthStyle(width) {
1559
+ return `width:${width}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
1560
+ }
1561
+ #columnStyle(depth, isLast) {
1562
+ const w = this.#columnWidths.get(depth);
1563
+ if (w !== void 0) return this.#explicitWidthStyle(w);
1564
+ if (this.stretchLastColumn && isLast) return `width:max-content;flex-grow:1;flex-shrink:0;flex-basis:auto;min-width:0`;
1565
+ return `width:max-content;flex-grow:0;flex-shrink:0;flex-basis:auto`;
1566
+ }
1567
+ #isFocused(node, depth) {
1568
+ return this.tickled.isTickled(this.#buildTickledId(node, depth));
1569
+ }
1570
+ /**
1571
+ * Override to provide a custom loading indicator.
1572
+ */
1573
+ renderLoadingIndicator() {
1574
+ return html`<div class="loading-spinner"></div>`;
1575
+ }
1576
+ #renderLoadingColumn() {
1577
+ const depth = this._loadingDepth;
1578
+ const stretched = this.stretchLastColumn && this.#columnWidths.get(depth) === void 0;
1579
+ return html`
1580
+ <div class="column loading-column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, true)}>
1581
+ ${this.headers ? html`
1582
+ <div class="column-header">${this.#getColumnHeader(depth)}</div>
1583
+ ` : nothing}
1584
+ <div class="column-content loading-content">
1585
+ ${this.renderLoadingIndicator()}
1586
+ </div>
1587
+ </div>
1588
+ `;
1589
+ }
1590
+ render() {
1591
+ return html`
1592
+ <div id="container" @keydown=${this.#onKeyDown} tabindex="0">
1593
+ ${this._columns.map((nodes, depth) => {
1594
+ const visibleNodes = this.filterPredicate ? nodes.filter((n) => this.filterPredicate(n, depth)) : nodes;
1595
+ const isLast = depth === this._columns.length - 1 && this._loadingDepth === void 0;
1596
+ const stretched = this.stretchLastColumn && isLast && this.#columnWidths.get(depth) === void 0;
1597
+ return html`
1598
+ <div class="column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, isLast)}>
1599
+ ${this.headers ? html`
1600
+ <div class="column-header"
1601
+ @click=${() => this.#onHeaderClick(depth)}
1602
+ @contextmenu=${(e) => this._handleHeaderRightClick(e, depth)}>
1603
+ ${this.#getColumnHeader(depth)}
1604
+ </div>
1605
+ ` : nothing}
1606
+ <div class="column-content">
1607
+ ${this.renderColumnContent(visibleNodes, depth)}
1608
+ </div>
1609
+ <div class="resize-handle"
1610
+ @pointerdown=${(e) => this.#onResizeStart(e, depth)}>
1611
+ </div>
1612
+ </div>
1613
+ `;
1614
+ })}
1615
+ ${this._loadingDepth !== void 0 ? this.#renderLoadingColumn() : nothing}
1616
+ </div>
1617
+ ${this.search.renderField()}
1618
+ `;
1619
+ }
1620
+ _getFocusedIndex(depth) {
1621
+ const tickled = this.#getTickledNodeAndDepth();
1622
+ if (tickled && tickled.depth === depth) return tickled.index;
1623
+ return 0;
1624
+ }
1625
+ _isFocused(node, depth) {
1626
+ return this.#isFocused(node, depth);
1627
+ }
1628
+ /**
1629
+ * Called by subclasses when pointer enters an item.
1630
+ * Fires tickle event and switches to mouse mode.
1631
+ */
1632
+ _handleItemPointerEnter(node, depth) {
1633
+ const id = this.#buildTickledId(node, depth);
1634
+ this.tickled.handlePointerEnter(id);
1635
+ if (this.tickled.tickledId === id) this.#keyboardFocus = false;
1636
+ this.dispatchEvent(new CustomEvent(`tickle`, {
1637
+ detail: {
1638
+ node,
1639
+ depth
1640
+ },
1641
+ bubbles: true,
1642
+ composed: true
1643
+ }));
1644
+ }
1645
+ /**
1646
+ * Called by subclasses when pointer leaves the column container.
1647
+ */
1648
+ _handleColumnPointerLeave() {
1649
+ this.tickled.handlePointerLeave(new PointerEvent(`pointerleave`));
1650
+ }
1651
+ static {
1652
+ this.styles = [
1653
+ themeFallbacks,
1654
+ tickledStyles,
1655
+ listSearchStyles,
1656
+ css`
1657
+ :host {
1658
+ --column-bg: var(--surface-3);
1659
+ --column-header-bg: var(--surface-2);
1660
+ --column-header-text: var(--surface-muted-text);
1661
+ --column-separator: var(--surface-5);
1662
+ --column-resize-handle: var(--surface-5);
1663
+ --column-resize-handle-hover: var(--accent);
1664
+ --column-resize-width: 4px;
1665
+ --item-hover-bg: var(--item-bg-tickled);
1666
+ --item-open-bg: color-mix(in oklch, var(--accent, oklch(60% 0.15 140)) 15%, transparent);
1667
+ --item-selected-bg: var(--item-bg-selected);
1668
+ --item-selected-text: var(--item-text-selected);
1669
+
1670
+ display: block;
1671
+ overflow: hidden;
1672
+ position: relative;
1673
+ box-sizing: border-box;
1674
+ }
1675
+
1676
+ #container {
1677
+ display: flex;
1678
+ height: 100%;
1679
+ overflow-x: auto;
1680
+ outline: none;
1681
+ }
1682
+
1683
+ .column {
1684
+ display: flex;
1685
+ flex-direction: column;
1686
+ position: relative;
1687
+ background: var(--column-bg);
1688
+ border-right: 1px solid var(--column-separator);
1689
+ flex-shrink: 0;
1690
+ /* Content-sized columns never grow wider than 80% of the available
1691
+ width — pathological labels truncate with ellipsis instead of
1692
+ pushing every other column out of view */
1693
+ max-width: 80%;
1694
+ /* Smoothly animate the last column stretching to fill remaining
1695
+ space and shrinking back to content width when it stops being last */
1696
+ transition: flex-grow var(--transition, 0.2s ease-out);
1697
+ }
1698
+
1699
+ .column.stretch-last {
1700
+ min-width: 0;
1701
+ /* Stretching fills remaining space by definition, so it is exempt
1702
+ from the 80% auto-size cap */
1703
+ max-width: none;
1704
+ }
1705
+
1706
+ .column-header {
1707
+ background: var(--column-header-bg);
1708
+ color: var(--column-header-text);
1709
+ font-size: var(--text-s, 11px);
1710
+ font-family: var(--font-family, system-ui, sans-serif);
1711
+ font-weight: var(--font-weight-bold, 600);
1712
+ padding: var(--space-xs, 3px) var(--space-m, 8px);
1713
+ border-bottom: 1px solid var(--column-separator);
1714
+ white-space: nowrap;
1715
+ overflow: hidden;
1716
+ text-overflow: ellipsis;
1717
+ flex-shrink: 0;
1718
+ cursor: pointer;
1719
+ user-select: none;
1720
+ transition: background var(--transition, 0.15s ease-out),
1721
+ color var(--transition, 0.15s ease-out);
1722
+ }
1723
+
1724
+ .column-header:hover {
1725
+ background: var(--item-hover-bg);
1726
+ color: var(--surface-2-text, #fff);
1727
+ }
1728
+
1729
+ .column-content {
1730
+ flex: 1;
1731
+ overflow-y: auto;
1732
+ overflow-x: visible;
1733
+ }
1734
+
1735
+ .resize-handle {
1736
+ position: absolute;
1737
+ top: 0;
1738
+ right: 0;
1739
+ width: var(--column-resize-width);
1740
+ height: 100%;
1741
+ background: var(--column-resize-handle);
1742
+ cursor: ew-resize;
1743
+ z-index: 1;
1744
+ transition: background var(--transition, 0.15s ease-out);
1745
+ }
1746
+
1747
+ .resize-handle:hover {
1748
+ background: var(--column-resize-handle-hover);
1749
+ }
1750
+
1751
+ .loading-column {
1752
+ min-width: 60px;
1753
+ }
1754
+
1755
+ .loading-content {
1756
+ display: flex;
1757
+ align-items: center;
1758
+ justify-content: center;
1759
+ }
1760
+
1761
+ .loading-spinner {
1762
+ width: 22px;
1763
+ height: 22px;
1764
+ border: 2px solid var(--column-separator);
1765
+ border-top-color: var(--accent, oklch(60% 0.15 140));
1766
+ border-radius: 50%;
1767
+ animation: miller-spin 0.7s linear infinite;
1768
+ }
1769
+
1770
+ @keyframes miller-spin {
1771
+ to { transform: rotate(360deg); }
1772
+ }
1773
+
1774
+ ::highlight(incr-search-highlight) {
1775
+ background: var(--search-highlight-bg, var(--accent));
1776
+ color: var(--search-highlight-color, var(--accent-text, #fff));
1777
+ border-radius: 2px;
1778
+ padding: 0 1px;
1779
+ }
1780
+ `
1781
+ ];
1782
+ }
1783
+ };
1784
+ __decorate([property({ type: Object })], MillerBaseElement.prototype, "root", void 0);
1785
+ __decorate([property({ attribute: false })], MillerBaseElement.prototype, "model", void 0);
1786
+ __decorate([property({ type: Boolean })], MillerBaseElement.prototype, "headers", void 0);
1787
+ __decorate([property({
1788
+ type: Boolean,
1789
+ attribute: `stretch-last-column`,
1790
+ reflect: true
1791
+ })], MillerBaseElement.prototype, "stretchLastColumn", void 0);
1792
+ __decorate([property({ attribute: false })], MillerBaseElement.prototype, "viewRoot", void 0);
1793
+ __decorate([property({ type: String })], MillerBaseElement.prototype, "interactionMode", void 0);
1794
+ __decorate([property({
1795
+ reflect: true,
1796
+ attribute: `selection-filter`
1797
+ })], MillerBaseElement.prototype, "selectionFilter", void 0);
1798
+ __decorate([property({
1799
+ reflect: true,
1800
+ attribute: `exclusivity`
1801
+ })], MillerBaseElement.prototype, "exclusivity", void 0);
1802
+ __decorate([property({ type: Object })], MillerBaseElement.prototype, "loadChildren", void 0);
1803
+ __decorate([property({ attribute: false })], MillerBaseElement.prototype, "filterPredicate", void 0);
1804
+ __decorate([property({
1805
+ type: String,
1806
+ attribute: `search-modes`,
1807
+ reflect: true
1808
+ })], MillerBaseElement.prototype, "searchModes", void 0);
1809
+ __decorate([state()], MillerBaseElement.prototype, "_columns", void 0);
1810
+ __decorate([state()], MillerBaseElement.prototype, "_loadingDepth", void 0);
1811
+ __decorate([state()], MillerBaseElement.prototype, "_selectedNodes", void 0);
1812
+ __decorate([property({ attribute: false })], MillerBaseElement.prototype, "restoreLoads", void 0);
1813
+ MillerBaseElement = __decorate([safeCustomElement(`ixfx-miller-base`)], MillerBaseElement);
1814
+ //#endregion
1815
+ //#region src/miller/miller-list.ts
1816
+ const HAS_CHECKED = 1;
1817
+ const HAS_UNCHECKED = 2;
1818
+ /**
1819
+ * Walk the loaded subtree under `node`, returning which kinds of leaf were
1820
+ * seen (`HAS_CHECKED` | `HAS_UNCHECKED`). Stops as soon as both have been
1821
+ * seen, since the result can no longer change.
1822
+ */
1823
+ function scanCheckState(node, selectedKeys) {
1824
+ if (node.item.isLeaf || !node.children?.length) return selectedKeys.has(node.item.key) ? HAS_CHECKED : HAS_UNCHECKED;
1825
+ let seen = 0;
1826
+ for (const child of node.children) {
1827
+ seen |= scanCheckState(child, selectedKeys);
1828
+ if (seen === 3) break;
1829
+ }
1830
+ return seen;
1831
+ }
1832
+ /**
1833
+ * Compute the checkbox state for a node based on `selectedKeys`.
1834
+ *
1835
+ * - Unloaded branch (`children === undefined`): treated as a leaf — we can only
1836
+ * report on what is known.
1837
+ * - Leaf or confirmed-empty branch: checked iff its key is in `selectedKeys`.
1838
+ * - Loaded branch: checked if ALL descendants are checked; unchecked if NONE are;
1839
+ * indeterminate otherwise.
1840
+ */
1841
+ function computeCheckboxState(node, selectedKeys) {
1842
+ const seen = scanCheckState(node, selectedKeys);
1843
+ if (seen === HAS_CHECKED) return `checked`;
1844
+ if (seen === HAS_UNCHECKED) return `unchecked`;
1845
+ return `indeterminate`;
1846
+ }
1847
+ let MillerListElement = class MillerListElement extends MillerBaseElement {
1848
+ renderColumnContent(nodes, depth) {
1849
+ const hasIcons = nodes.some((node) => !!node.item.icon);
1850
+ const isCheckedMode = this.interactionMode === `checked`;
1851
+ return html`
1852
+ <ul
1853
+ class="item-list ${hasIcons ? `has-icons` : ``} ${isCheckedMode ? `checked-mode` : ``}"
1854
+ role="listbox"
1855
+ aria-label="Column ${depth}"
1856
+ @pointerleave=${() => this._handleColumnPointerLeave()}
1857
+ >
1858
+ ${nodes.map((node) => this.#renderListItem(node, depth))}
1859
+ </ul>
1860
+ `;
1861
+ }
1862
+ #renderListItem(node, depth) {
1863
+ const selected = this._isSelected(node, depth);
1864
+ const tickled = this._isFocused(node, depth);
1865
+ const open = this._isOpen(node, depth);
1866
+ const isBranch = node.item.isLeaf !== true;
1867
+ const icon = node.item.icon;
1868
+ const iconColor = appearanceColor(node.item.iconColor);
1869
+ const textColor = appearanceColor(node.item.textColor);
1870
+ const badge = appearanceAbsent(node.item.badge) ? void 0 : node.item.badge;
1871
+ const tooltip = node.item.tooltip;
1872
+ const isCheckedMode = this.interactionMode === `checked`;
1873
+ let checkboxTpl = nothing;
1874
+ if (isCheckedMode) {
1875
+ const state = computeCheckboxState(node, this._selectedKeys);
1876
+ checkboxTpl = html`
1877
+ <input
1878
+ type="checkbox"
1879
+ class="item-checkbox"
1880
+ .checked=${state === `checked`}
1881
+ .indeterminate=${state === `indeterminate`}
1882
+ aria-label="Select ${node.item.label}"
1883
+ @click=${(e) => this._handleCheckboxClick(e, node, depth)}
1884
+ />
1885
+ `;
1886
+ }
1887
+ return html`
1888
+ <li
1889
+ class="item ${selected ? `selected` : ``} ${open ? `open` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`}"
1890
+ role="option"
1891
+ aria-selected=${selected}
1892
+ @click=${(e) => this._handleItemClick(node, depth, e)}
1893
+ @dblclick=${(e) => this._handleItemDblClick(node, depth, e)}
1894
+ @contextmenu=${(e) => this._handleItemRightClick(node, depth, e)}
1895
+ @pointerenter=${() => this._handleItemPointerEnter(node, depth)}
1896
+ >
1897
+ ${checkboxTpl}
1898
+ ${icon ? html`<ixfx-icon class="item-icon" name="${icon}" style=${iconColor ? `color:${iconColor}` : nothing}></ixfx-icon>` : nothing}
1899
+ <span class="label" style=${textColor ? `color:${textColor}` : nothing}>${node.item.label}</span>
1900
+ ${badge === void 0 ? nothing : html`<span class="item-badge" part="badge">${badge}</span>`}
1901
+ ${tooltip ? html`<ixfx-tooltip class="item-tooltip" anchor="--tooltip-miller-${depth}-${node.item.key}" .value=${tooltip}></ixfx-tooltip>` : nothing}
1902
+ ${isBranch ? html`<span class="caret" aria-hidden="true">›</span>` : nothing}
1903
+ </li>
1904
+ `;
1905
+ }
1906
+ static {
1907
+ this.styles = [
1908
+ themeFallbacks,
1909
+ ...MillerBaseElement.styles,
1910
+ tickledItemStyles,
1911
+ css`
1912
+ .item-list {
1913
+ list-style: none;
1914
+ margin: 0;
1915
+ padding: var(--space-xs, 3px) 0;
1916
+ }
1917
+
1918
+ .item {
1919
+ display: flex;
1920
+ align-items: center;
1921
+ justify-content: space-between;
1922
+ }
1923
+
1924
+ .item.selected {
1925
+ background: var(--item-selected-bg);
1926
+ color: var(--item-selected-text, #fff);
1927
+ outline: 2px dashed var(--surface-3, #333);
1928
+ outline-offset: -2px;
1929
+ }
1930
+
1931
+ /* Item is on the currently open path (ancestor of deepest column) */
1932
+ .item.open:not(.selected) {
1933
+ background: var(--item-open-bg);
1934
+ }
1935
+
1936
+ .item-icon {
1937
+ flex-shrink: 0;
1938
+ width: var(--icon-size, 1em);
1939
+ height: var(--icon-size, 1em);
1940
+ margin-right: var(--icon-spacing, 0.3em);
1941
+ }
1942
+
1943
+ /* Align text on icon-less rows with icon rows in a mixed list */
1944
+ .item-list.has-icons .item:not(:has(.item-icon)) .label {
1945
+ margin-left: calc(var(--icon-size, 1em) + var(--icon-spacing, 0.3em));
1946
+ }
1947
+
1948
+ .label {
1949
+ flex: 1;
1950
+ overflow: hidden;
1951
+ text-overflow: ellipsis;
1952
+ }
1953
+
1954
+ /* Finder-style badge: sits between the label and the caret, flush to
1955
+ the right edge of the row. The label is flex:1, so the gap opens up
1956
+ ahead of the badge automatically. */
1957
+ .item-badge {
1958
+ flex-shrink: 0;
1959
+ display: inline-flex;
1960
+ align-items: center;
1961
+ margin-inline-start: auto;
1962
+ padding-inline-start: var(--miller-list-badge-gap, 0.5em);
1963
+ }
1964
+
1965
+ .item-tooltip {
1966
+ position: absolute;
1967
+ }
1968
+
1969
+ .caret {
1970
+ flex-shrink: 0;
1971
+ margin-left: var(--space-s, 4px);
1972
+ color: var(--surface-muted-text);
1973
+ font-size: var(--text-l, 13px);
1974
+ }
1975
+
1976
+ .item.selected .caret {
1977
+ color: var(--item-selected-text, #fff);
1978
+ }
1979
+
1980
+ /* ---- checked mode ---- */
1981
+
1982
+ .item-checkbox {
1983
+ flex-shrink: 0;
1984
+ margin-right: var(--space-xs, 3px);
1985
+ cursor: pointer;
1986
+ accent-color: var(--accent);
1987
+ /* Prevent the checkbox click from also triggering item styles */
1988
+ pointer-events: auto;
1989
+ }
1990
+
1991
+ /* In checked mode the item row itself is not highlighted on selection —
1992
+ the checkbox communicates state instead. */
1993
+ .item-list.checked-mode .item.selected {
1994
+ background: transparent;
1995
+ color: var(--surface-3-text, #fff);
1996
+ }
1997
+
1998
+
1999
+ `
2000
+ ];
2001
+ }
2002
+ };
2003
+ MillerListElement = __decorate([safeCustomElement(`ixfx-miller-list`)], MillerListElement);
2004
+ //#endregion
2005
+ //#region src/miller/miller-tree-controller.ts
2006
+ /**
2007
+ * Bridges an `ixfx` `TraversableTree<T>` with an `ixfx-miller-*` component.
2008
+ *
2009
+ * Usage:
2010
+ * ```ts
2011
+ * import { Trees } from '@ixfx/collections';
2012
+ * import { MillerTreeController } from 'ixfx-components/miller';
2013
+ *
2014
+ * const root = Trees.Mutable.createNode({ key: 'root', label: 'Root' });
2015
+ * Trees.Mutable.addValue({ key: 'a', label: 'Alpha' }, root);
2016
+ *
2017
+ * const ctrl = new MillerTreeController(
2018
+ * Trees.Mutable.asDynamicTraversable(root),
2019
+ * document.querySelector('ixfx-miller-list'),
2020
+ * { valueToItem: v => ({ key: v.key, label: v.label }) }
2021
+ * );
2022
+ * ctrl.connect();
2023
+ * ```
2024
+ *
2025
+ * Children are populated lazily: the controller provides a `loadChildren`
2026
+ * callback that synchronously reads from the underlying tree.
2027
+ *
2028
+ * Call `disconnect()` to remove all event listeners and release resources.
2029
+ */
2030
+ var MillerTreeController = class {
2031
+ #root;
2032
+ #element;
2033
+ #options;
2034
+ #nodeToTree = /* @__PURE__ */ new WeakMap();
2035
+ #identityToMillerNode = /* @__PURE__ */ new Map();
2036
+ #keyToTreeNode = /* @__PURE__ */ new Map();
2037
+ #identityToTree = /* @__PURE__ */ new Map();
2038
+ /** `valueToItem` results per source node identity; replaced wholesale by `sync()`. */
2039
+ #itemCache = /* @__PURE__ */ new WeakMap();
2040
+ #connectSignal;
2041
+ constructor(root, element, options) {
2042
+ this.#root = root;
2043
+ this.#element = element;
2044
+ this.#options = options;
2045
+ }
2046
+ /**
2047
+ * Wire up the controller: sets `element.root` and provides `loadChildren` callback.
2048
+ * Safe to call again after `disconnect()`.
2049
+ */
2050
+ connect() {
2051
+ this.disconnect();
2052
+ this.#connectSignal = new AbortController();
2053
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
2054
+ this.#identityToMillerNode.clear();
2055
+ this.#identityToTree.clear();
2056
+ this.#keyToTreeNode.clear();
2057
+ const rootNode = { item: this.#itemOf(this.#root) };
2058
+ this.#nodeToTree.set(rootNode, this.#root);
2059
+ rootNode.children = this.#childrenOf(this.#root);
2060
+ this.#element.root = rootNode;
2061
+ this.#element.loadChildren = this.#options.loadChildren ?? (({ node }) => {
2062
+ const treeNode = this.#nodeToTree.get(node);
2063
+ if (!treeNode) return [];
2064
+ return this.#childrenOf(treeNode);
2065
+ });
2066
+ }
2067
+ /** Remove all event listeners and release internal state. */
2068
+ disconnect() {
2069
+ this.#connectSignal?.abort();
2070
+ this.#connectSignal = void 0;
2071
+ this.#element.loadChildren = void 0;
2072
+ }
2073
+ /**
2074
+ * Navigate the component to the first node (depth-first) in the ixfx tree
2075
+ * whose value satisfies `predicate`.
2076
+ *
2077
+ * Any ancestor `MillerNode`s with unloaded children are populated
2078
+ * synchronously before calling `element.navigateTo()`.
2079
+ *
2080
+ * Key paths are relative to the element's current view root: when the view
2081
+ * is rebased, only matches at or below the view root can be reached.
2082
+ *
2083
+ * Returns `false` if no matching node is found.
2084
+ */
2085
+ navigateToValue(predicate, options) {
2086
+ const treePath = this.#findTreePath(this.#root, predicate);
2087
+ if (!treePath) return false;
2088
+ return this.#navigateToPath(treePath, options);
2089
+ }
2090
+ /**
2091
+ * Navigate using item keys **relative to the element's current view root**
2092
+ * (the real root when not rebased). Ancestors with unloaded children are
2093
+ * populated first. Returns `false` if a key is missing.
2094
+ */
2095
+ navigateToKeyPath(keys, options) {
2096
+ const base = this.#element.viewRoot ?? this.#element.root;
2097
+ if (!base || keys.length === 0) return false;
2098
+ this.#ensureChildren(base);
2099
+ let column = base.children ?? [];
2100
+ for (let i = 0; i < keys.length - 1; i++) {
2101
+ const node = column.find((n) => n.item.key === keys[i]);
2102
+ if (!node) return false;
2103
+ this.#ensureChildren(node);
2104
+ column = node.children ?? [];
2105
+ }
2106
+ return this.#element.navigateTo(keys, options);
2107
+ }
2108
+ /**
2109
+ * Navigate to the source node with this identity. Returns `false` when it is
2110
+ * unknown or lies outside a rebased view (keys are relative to the view root).
2111
+ */
2112
+ navigateToIdentity(identity, options) {
2113
+ const target = this.#treeNodeByIdentity(identity);
2114
+ if (!target) return false;
2115
+ const path = [];
2116
+ const rootId = this.#root.getIdentity();
2117
+ for (let n = target; n; n = n.getParent()) {
2118
+ path.unshift(n);
2119
+ if (n.getIdentity() === rootId) return this.#navigateToPath(path, options);
2120
+ }
2121
+ return false;
2122
+ }
2123
+ /** The source node whose item key is `key`, if it has been materialised. */
2124
+ lookupByKey(key) {
2125
+ return this.#keyToTreeNode.get(key);
2126
+ }
2127
+ /**
2128
+ * Re-derive children for every already-materialised node from the source tree
2129
+ * and patch them in place. Children arrays are mutated (not replaced) so open
2130
+ * columns keep their references, and `MillerNode`s are reused by key so
2131
+ * selection survives. A rebased `viewRoot` is reset only if its node was
2132
+ * removed. `loadChildren` is left alone. Refreshes the element once.
2133
+ */
2134
+ sync() {
2135
+ const root = this.#element.root;
2136
+ if (!root) return;
2137
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
2138
+ this.#identityToMillerNode.clear();
2139
+ this.#identityToTree.clear();
2140
+ this.#keyToTreeNode.clear();
2141
+ this.#nodeToTree.set(root, this.#root);
2142
+ root.item = this.#itemOf(this.#root);
2143
+ const kept = /* @__PURE__ */ new Set([root]);
2144
+ if (root.children) this.#syncChildren(this.#root, root.children, kept);
2145
+ const view = this.#element.viewRoot;
2146
+ if (view && !kept.has(view)) this.#element.viewRoot = void 0;
2147
+ this.#element.invalidateStructure();
2148
+ }
2149
+ #navigateToPath(treePath, options) {
2150
+ const base = this.#element.viewRoot ?? this.#element.root;
2151
+ if (!base) return false;
2152
+ let startIndex = 1;
2153
+ if (this.#element.viewRoot) {
2154
+ const mappedBase = this.#nodeToTree.get(base);
2155
+ if (!mappedBase) return false;
2156
+ const baseId = mappedBase.getIdentity();
2157
+ const baseIndex = treePath.findIndex((n) => n.getIdentity() === baseId);
2158
+ if (baseIndex < 0) return false;
2159
+ startIndex = baseIndex + 1;
2160
+ }
2161
+ if (startIndex >= treePath.length) return false;
2162
+ return this.navigateToKeyPath(treePath.slice(startIndex).map((n) => this.#itemOf(n).key), options);
2163
+ }
2164
+ #ensureChildren(node) {
2165
+ if (node.children !== void 0) return;
2166
+ const mapped = this.#nodeToTree.get(node);
2167
+ if (mapped) node.children = this.#childrenOf(mapped);
2168
+ }
2169
+ #treeNodeByIdentity(identity) {
2170
+ return this.#identityToTree.get(identity);
2171
+ }
2172
+ #itemOf(treeNode) {
2173
+ const id = treeNode.getIdentity();
2174
+ let item = this.#itemCache.get(id);
2175
+ if (!item) {
2176
+ item = this.#options.valueToItem(treeNode.getValue(), treeNode);
2177
+ this.#itemCache.set(id, item);
2178
+ }
2179
+ return item;
2180
+ }
2181
+ /** Convert a TraversableTree node's children to MillerNode[]. */
2182
+ #childrenOf(treeNode) {
2183
+ return Array.from(treeNode.children(), (child) => this.#toMillerNode(child));
2184
+ }
2185
+ #toMillerNode(treeNode) {
2186
+ const childList = [...treeNode.children()];
2187
+ for (const child of childList) this.#identityToTree.set(child.getIdentity(), child);
2188
+ const userItem = this.#itemOf(treeNode);
2189
+ const isLeaf = userItem.isLeaf ?? childList.length === 0;
2190
+ const millerNode = {
2191
+ item: {
2192
+ ...userItem,
2193
+ isLeaf
2194
+ },
2195
+ children: isLeaf ? [] : void 0
2196
+ };
2197
+ this.#register(millerNode, treeNode);
2198
+ return millerNode;
2199
+ }
2200
+ #register(node, treeNode) {
2201
+ this.#nodeToTree.set(node, treeNode);
2202
+ this.#identityToMillerNode.set(treeNode.getIdentity(), node);
2203
+ this.#identityToTree.set(treeNode.getIdentity(), treeNode);
2204
+ this.#keyToTreeNode.set(node.item.key, treeNode);
2205
+ }
2206
+ /** Patch `existing` (in place) to match the source children of `treeNode`; recurse into loaded nodes. */
2207
+ #syncChildren(treeNode, existing, kept) {
2208
+ const byKey = new Map(existing.map((n) => [n.item.key, n]));
2209
+ const next = Array.from(treeNode.children(), (child) => {
2210
+ const fresh = this.#toMillerNode(child);
2211
+ const old = byKey.get(fresh.item.key);
2212
+ if (!old) {
2213
+ kept.add(fresh);
2214
+ return fresh;
2215
+ }
2216
+ old.item = fresh.item;
2217
+ this.#register(old, child);
2218
+ kept.add(old);
2219
+ if (fresh.item.isLeaf) old.children = [];
2220
+ else if (old.children !== void 0) this.#syncChildren(child, old.children, kept);
2221
+ else old.children = void 0;
2222
+ return old;
2223
+ });
2224
+ existing.splice(0, existing.length, ...next);
2225
+ }
2226
+ /** DFS through the ixfx tree returning the path of nodes to the first match. */
2227
+ #findTreePath(node, predicate, ancestors = []) {
2228
+ const path = [...ancestors, node];
2229
+ if (predicate(node.getValue())) return path;
2230
+ for (const child of node.children()) {
2231
+ const result = this.#findTreePath(child, predicate, path);
2232
+ if (result) return result;
2233
+ }
2234
+ }
2235
+ };
2236
+ //#endregion
2237
+ export { MillerListElement as n, MillerBaseElement as r, MillerTreeController as t };