@ixfx/components 0.13.6 → 0.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/miller.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { D as TreeTickleDetail, E as TreeSelectEvent, O as TreeTickleEvent, T as TreeSelectDetail, a as TreeComponent, b as TreeItemClickDetail, i as TreeCollapseEvent, k as TreeDataModel, n as TreeActivateEvent, o as TreeComponentEventMap, r as TreeCollapseDetail, t as TreeActivateDetail, v as TreeExpandDetail, w as TreeSearchHost, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-Bx40hxMz.js";
1
+ import { A as TreeTickleEvent, D as TreeSelectDetail, E as TreeSearchHost, O as TreeSelectEvent, a as TreeComponent, b as TreeItemClickDetail, i as TreeCollapseEvent, j as TreeDataModel, k as TreeTickleDetail, n as TreeActivateEvent, o as TreeComponentEventMap, r as TreeCollapseDetail, t as TreeActivateDetail, v as TreeExpandDetail, w as TreeNavigateOptions, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-Tslymyyl.js";
2
2
  import { a as TreeItem, n as TreeExclusivity, o as TreeNode, r as TreeFilterPredicate, s as TreeSelectionFilter, t as LoadChildrenCallback } from "./types-DQ7KOUIj.js";
3
- import { S as ListSearchController } from "./index-CzFRWPzh.js";
3
+ import { S as ListSearchController } from "./index-CO6m-YC0.js";
4
4
  import { n as TickledController } from "./tickled-controller-DodxD3eQ.js";
5
5
  import { LitElement, PropertyValues, TemplateResult, nothing } from "lit";
6
6
  import { CommandRegistry } from "@clinth/ui-commands";
@@ -218,7 +218,13 @@ declare class MillerBaseElement extends LitElement implements TreeComponent, Tre
218
218
  * Programmatically navigate to a node by key path, relative to the current
219
219
  * view root (rebased or full tree).
220
220
  */
221
- navigateTo(keys: readonly string[]): boolean;
221
+ navigateTo(keys: readonly string[], options?: TreeNavigateOptions): boolean;
222
+ /**
223
+ * Re-sync the element with children arrays patched in place (see
224
+ * `MillerTreeController.sync()`): columns whose selected node was removed are
225
+ * closed, selection entries for removed nodes are dropped, then it re-renders.
226
+ */
227
+ invalidateStructure(): void;
222
228
  /**
223
229
  * Programmatically navigate to the first node (breadth-first) matching a predicate.
224
230
  */
@@ -352,6 +358,11 @@ type MillerTreeControllerOptions<T> = {
352
358
  * `TraversableTree` node has any children.
353
359
  */
354
360
  valueToItem: (value: T, node: TraversableTree<T>) => TreeItem;
361
+ /**
362
+ * Children loader installed on the element by `connect()`. Defaults to a
363
+ * synchronous read from the source tree. `sync()` never touches it.
364
+ */
365
+ loadChildren?: LoadChildrenCallback;
355
366
  };
356
367
  /**
357
368
  * Bridges an `ixfx` `TraversableTree<T>` with an `ixfx-miller-*` component.
@@ -399,7 +410,28 @@ declare class MillerTreeController<T> {
399
410
  *
400
411
  * Returns `false` if no matching node is found.
401
412
  */
402
- navigateToValue(predicate: (value: T) => boolean): boolean;
413
+ navigateToValue(predicate: (value: T) => boolean, options?: TreeNavigateOptions): boolean;
414
+ /**
415
+ * Navigate using item keys **relative to the element's current view root**
416
+ * (the real root when not rebased). Ancestors with unloaded children are
417
+ * populated first. Returns `false` if a key is missing.
418
+ */
419
+ navigateToKeyPath(keys: readonly string[], options?: TreeNavigateOptions): boolean;
420
+ /**
421
+ * Navigate to the source node with this identity. Returns `false` when it is
422
+ * unknown or lies outside a rebased view (keys are relative to the view root).
423
+ */
424
+ navigateToIdentity(identity: unknown, options?: TreeNavigateOptions): boolean;
425
+ /** The source node whose item key is `key`, if it has been materialised. */
426
+ lookupByKey(key: string): TraversableTree<T> | undefined;
427
+ /**
428
+ * Re-derive children for every already-materialised node from the source tree
429
+ * and patch them in place. Children arrays are mutated (not replaced) so open
430
+ * columns keep their references, and `MillerNode`s are reused by key so
431
+ * selection survives. A rebased `viewRoot` is reset only if its node was
432
+ * removed. `loadChildren` is left alone. Refreshes the element once.
433
+ */
434
+ sync(): void;
403
435
  }
404
436
  //#endregion
405
437
  export { type TreeActivateDetail as MillerActivateDetail, type TreeActivateEvent as MillerActivateEvent, MillerBaseElement, MillerBaseElementEventMap, type TreeCollapseDetail as MillerCollapseDetail, type TreeCollapseEvent as MillerCollapseEvent, MillerConfig, type TreeExpandDetail as MillerExpandDetail, type TreeExpandEvent as MillerExpandEvent, MillerHeaderClickDetail, MillerHeaderClickEvent, MillerHeaderRightClickDetail, MillerHeaderRightClickEvent, type TreeItem as MillerItem, type TreeItemClickDetail as MillerItemClickDetail, type TreeItemClickEvent as MillerItemClickEvent, MillerItemDblClickDetail, MillerItemDblClickEvent, MillerItemRightClickDetail, MillerItemRightClickEvent, MillerItemState, MillerListElement, type TreeNode as MillerNode, MillerRebaseDetail, MillerRebaseEvent, type TreeSelectDetail as MillerSelectDetail, type TreeSelectEvent as MillerSelectEvent, MillerSelectionMode, type TreeTickleDetail as MillerTickleDetail, type TreeTickleEvent as MillerTickleEvent, MillerTreeController, MillerTreeControllerOptions };
package/dist/miller.js CHANGED
@@ -39,6 +39,8 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
39
39
  }
40
40
  return items;
41
41
  } });
42
+ this.#selectOrigin = `user`;
43
+ this.#selectSilent = false;
42
44
  this.#onKeyDown = (event) => {
43
45
  if (this.search.isOpen) return;
44
46
  const currentDepth = this.#getTickledDepthOrDefault();
@@ -403,14 +405,47 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
403
405
  * Programmatically navigate to a node by key path, relative to the current
404
406
  * view root (rebased or full tree).
405
407
  */
406
- navigateTo(keys) {
408
+ navigateTo(keys, options = {}) {
407
409
  const display = this.#displayRoot;
408
410
  if (!display?.children) return false;
411
+ const prevOrigin = this.#selectOrigin;
412
+ const prevSilent = this.#selectSilent;
413
+ this.#selectOrigin = `programmatic`;
414
+ this.#selectSilent = options.silent === true;
415
+ try {
416
+ return this.#navigateTo(display.children, keys, options.reveal ?? `edge`);
417
+ } finally {
418
+ this.#selectOrigin = prevOrigin;
419
+ this.#selectSilent = prevSilent;
420
+ }
421
+ }
422
+ /**
423
+ * Re-sync the element with children arrays patched in place (see
424
+ * `MillerTreeController.sync()`): columns whose selected node was removed are
425
+ * closed, selection entries for removed nodes are dropped, then it re-renders.
426
+ */
427
+ invalidateStructure() {
428
+ const present = /* @__PURE__ */ new Set();
429
+ for (const col of this._columns) for (const n of col) present.add(n);
430
+ for (let depth = 0; depth < this._columns.length; depth++) {
431
+ const selected = this._selectedByDepth.get(depth);
432
+ if (selected && !this._columns[depth].includes(selected)) {
433
+ this._columns = this._columns.slice(0, depth + 1);
434
+ for (const d of [...this._selectedByDepth.keys()]) if (d >= depth) this._selectedByDepth.delete(d);
435
+ break;
436
+ }
437
+ }
438
+ for (const n of [...this._selectedNodes]) if (!present.has(n)) this._selectedNodes.delete(n);
439
+ this.requestUpdate();
440
+ }
441
+ #selectOrigin;
442
+ #selectSilent;
443
+ #navigateTo(rootColumn, keys, reveal) {
409
444
  this.#abortPending();
410
445
  this._selectedByDepth.clear();
411
446
  this.tickled.clearTickled();
412
- const newColumns = [display.children];
413
- let currentColumn = display.children;
447
+ const newColumns = [rootColumn];
448
+ let currentColumn = rootColumn;
414
449
  let success = true;
415
450
  let lastNode;
416
451
  let lastDepth = 0;
@@ -438,7 +473,7 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
438
473
  if (leafKey) this._selectedKeys = /* @__PURE__ */ new Set([leafKey]);
439
474
  }
440
475
  this.requestUpdate();
441
- this.#scrollNavigatedItemIntoView();
476
+ this.#scrollNavigatedItemIntoView(reveal === `center-if-hidden` ? `auto` : `smooth`, reveal);
442
477
  return success;
443
478
  }
444
479
  /**
@@ -718,9 +753,11 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
718
753
  return true;
719
754
  }
720
755
  #dispatchSelectEvent(previous) {
756
+ if (this.#selectSilent) return;
721
757
  const detail = {
722
758
  selected: new Set(this._selectedNodes),
723
- previous
759
+ previous,
760
+ origin: this.#selectOrigin
724
761
  };
725
762
  this.dispatchEvent(new CustomEvent(`select`, {
726
763
  detail,
@@ -990,13 +1027,26 @@ let MillerBaseElement = class MillerBaseElement extends LitElement {
990
1027
  index
991
1028
  };
992
1029
  }
993
- #scrollNavigatedItemIntoView(behavior = `smooth`) {
1030
+ #scrollNavigatedItemIntoView(behavior = `smooth`, reveal = `edge`) {
994
1031
  requestAnimationFrame(() => {
995
1032
  const tickled = this.#getTickledNodeAndDepth();
996
1033
  if (!tickled) return;
997
1034
  const container = this.shadowRoot?.querySelectorAll(`.column`)[tickled.depth]?.querySelector(`.column-content`);
998
1035
  if (!container) return;
999
- container.querySelectorAll(`.item`)[tickled.index]?.scrollIntoView({
1036
+ const item = container.querySelectorAll(`.item`)[tickled.index];
1037
+ if (!item) return;
1038
+ if (reveal === `center-if-hidden`) {
1039
+ const rect = item.getBoundingClientRect();
1040
+ const box = container.getBoundingClientRect();
1041
+ const hidden = rect.top < box.top || rect.bottom > box.bottom;
1042
+ item.scrollIntoView({
1043
+ block: hidden ? `center` : `nearest`,
1044
+ inline: `nearest`,
1045
+ behavior
1046
+ });
1047
+ return;
1048
+ }
1049
+ item.scrollIntoView({
1000
1050
  block: `nearest`,
1001
1051
  behavior
1002
1052
  });
@@ -1562,6 +1612,10 @@ var MillerTreeController = class {
1562
1612
  #options;
1563
1613
  #nodeToTree = /* @__PURE__ */ new WeakMap();
1564
1614
  #identityToMillerNode = /* @__PURE__ */ new Map();
1615
+ #keyToTreeNode = /* @__PURE__ */ new Map();
1616
+ #identityToTree = /* @__PURE__ */ new Map();
1617
+ /** `valueToItem` results per source node identity; replaced wholesale by `sync()`. */
1618
+ #itemCache = /* @__PURE__ */ new WeakMap();
1565
1619
  #connectSignal;
1566
1620
  constructor(root, element, options) {
1567
1621
  this.#root = root;
@@ -1575,18 +1629,19 @@ var MillerTreeController = class {
1575
1629
  connect() {
1576
1630
  this.disconnect();
1577
1631
  this.#connectSignal = new AbortController();
1578
- const rootValue = this.#root.getValue();
1579
- const rootItem = this.#options.valueToItem(rootValue, this.#root);
1580
- const rootChildren = this.#childrenOf(this.#root);
1581
- this.#element.root = {
1582
- item: rootItem,
1583
- children: rootChildren
1584
- };
1585
- this.#element.loadChildren = ({ node }) => {
1632
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
1633
+ this.#identityToMillerNode.clear();
1634
+ this.#identityToTree.clear();
1635
+ this.#keyToTreeNode.clear();
1636
+ const rootNode = { item: this.#itemOf(this.#root) };
1637
+ this.#nodeToTree.set(rootNode, this.#root);
1638
+ rootNode.children = this.#childrenOf(this.#root);
1639
+ this.#element.root = rootNode;
1640
+ this.#element.loadChildren = this.#options.loadChildren ?? (({ node }) => {
1586
1641
  const treeNode = this.#nodeToTree.get(node);
1587
1642
  if (!treeNode) return [];
1588
1643
  return this.#childrenOf(treeNode);
1589
- };
1644
+ });
1590
1645
  }
1591
1646
  /** Remove all event listeners and release internal state. */
1592
1647
  disconnect() {
@@ -1606,46 +1661,110 @@ var MillerTreeController = class {
1606
1661
  *
1607
1662
  * Returns `false` if no matching node is found.
1608
1663
  */
1609
- navigateToValue(predicate) {
1664
+ navigateToValue(predicate, options) {
1610
1665
  const treePath = this.#findTreePath(this.#root, predicate);
1611
1666
  if (!treePath) return false;
1667
+ return this.#navigateToPath(treePath, options);
1668
+ }
1669
+ /**
1670
+ * Navigate using item keys **relative to the element's current view root**
1671
+ * (the real root when not rebased). Ancestors with unloaded children are
1672
+ * populated first. Returns `false` if a key is missing.
1673
+ */
1674
+ navigateToKeyPath(keys, options) {
1675
+ const base = this.#element.viewRoot ?? this.#element.root;
1676
+ if (!base || keys.length === 0) return false;
1677
+ this.#ensureChildren(base);
1678
+ let column = base.children ?? [];
1679
+ for (let i = 0; i < keys.length - 1; i++) {
1680
+ const node = column.find((n) => n.item.key === keys[i]);
1681
+ if (!node) return false;
1682
+ this.#ensureChildren(node);
1683
+ column = node.children ?? [];
1684
+ }
1685
+ return this.#element.navigateTo(keys, options);
1686
+ }
1687
+ /**
1688
+ * Navigate to the source node with this identity. Returns `false` when it is
1689
+ * unknown or lies outside a rebased view (keys are relative to the view root).
1690
+ */
1691
+ navigateToIdentity(identity, options) {
1692
+ const target = this.#treeNodeByIdentity(identity);
1693
+ if (!target) return false;
1694
+ const path = [];
1695
+ const rootId = this.#root.getIdentity();
1696
+ for (let n = target; n; n = n.getParent()) {
1697
+ path.unshift(n);
1698
+ if (n.getIdentity() === rootId) return this.#navigateToPath(path, options);
1699
+ }
1700
+ return false;
1701
+ }
1702
+ /** The source node whose item key is `key`, if it has been materialised. */
1703
+ lookupByKey(key) {
1704
+ return this.#keyToTreeNode.get(key);
1705
+ }
1706
+ /**
1707
+ * Re-derive children for every already-materialised node from the source tree
1708
+ * and patch them in place. Children arrays are mutated (not replaced) so open
1709
+ * columns keep their references, and `MillerNode`s are reused by key so
1710
+ * selection survives. A rebased `viewRoot` is reset only if its node was
1711
+ * removed. `loadChildren` is left alone. Refreshes the element once.
1712
+ */
1713
+ sync() {
1714
+ const root = this.#element.root;
1715
+ if (!root) return;
1716
+ this.#itemCache = /* @__PURE__ */ new WeakMap();
1717
+ this.#identityToMillerNode.clear();
1718
+ this.#identityToTree.clear();
1719
+ this.#keyToTreeNode.clear();
1720
+ this.#nodeToTree.set(root, this.#root);
1721
+ root.item = this.#itemOf(this.#root);
1722
+ const kept = /* @__PURE__ */ new Set([root]);
1723
+ if (root.children) this.#syncChildren(this.#root, root.children, kept);
1724
+ const view = this.#element.viewRoot;
1725
+ if (view && !kept.has(view)) this.#element.viewRoot = void 0;
1726
+ this.#element.invalidateStructure();
1727
+ }
1728
+ #navigateToPath(treePath, options) {
1612
1729
  const base = this.#element.viewRoot ?? this.#element.root;
1613
1730
  if (!base) return false;
1614
1731
  let startIndex = 1;
1615
1732
  if (this.#element.viewRoot) {
1616
1733
  const mappedBase = this.#nodeToTree.get(base);
1617
1734
  if (!mappedBase) return false;
1618
- const baseIndex = treePath.indexOf(mappedBase);
1735
+ const baseId = mappedBase.getIdentity();
1736
+ const baseIndex = treePath.findIndex((n) => n.getIdentity() === baseId);
1619
1737
  if (baseIndex < 0) return false;
1620
1738
  startIndex = baseIndex + 1;
1621
- if (base.children === void 0) base.children = this.#childrenOf(mappedBase);
1622
1739
  }
1623
- let currentColumn = base.children ?? [];
1624
- const keys = [];
1625
- for (let i = startIndex; i < treePath.length; i++) {
1626
- const treeNode = treePath[i];
1627
- const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
1628
- keys.push(key);
1629
- if (i < treePath.length - 1) {
1630
- const millerNode = currentColumn.find((n) => n.item.key === key);
1631
- if (!millerNode) return false;
1632
- if (millerNode.children === void 0) {
1633
- const mapped = this.#nodeToTree.get(millerNode);
1634
- if (mapped) millerNode.children = this.#childrenOf(mapped);
1635
- }
1636
- currentColumn = millerNode.children ?? [];
1637
- }
1740
+ if (startIndex >= treePath.length) return false;
1741
+ return this.navigateToKeyPath(treePath.slice(startIndex).map((n) => this.#itemOf(n).key), options);
1742
+ }
1743
+ #ensureChildren(node) {
1744
+ if (node.children !== void 0) return;
1745
+ const mapped = this.#nodeToTree.get(node);
1746
+ if (mapped) node.children = this.#childrenOf(mapped);
1747
+ }
1748
+ #treeNodeByIdentity(identity) {
1749
+ return this.#identityToTree.get(identity);
1750
+ }
1751
+ #itemOf(treeNode) {
1752
+ const id = treeNode.getIdentity();
1753
+ let item = this.#itemCache.get(id);
1754
+ if (!item) {
1755
+ item = this.#options.valueToItem(treeNode.getValue(), treeNode);
1756
+ this.#itemCache.set(id, item);
1638
1757
  }
1639
- return this.#element.navigateTo(keys);
1758
+ return item;
1640
1759
  }
1641
1760
  /** Convert a TraversableTree node's children to MillerNode[]. */
1642
1761
  #childrenOf(treeNode) {
1643
1762
  return Array.from(treeNode.children(), (child) => this.#toMillerNode(child));
1644
1763
  }
1645
1764
  #toMillerNode(treeNode) {
1646
- const value = treeNode.getValue();
1647
1765
  const childList = [...treeNode.children()];
1648
- const userItem = this.#options.valueToItem(value, treeNode);
1766
+ for (const child of childList) this.#identityToTree.set(child.getIdentity(), child);
1767
+ const userItem = this.#itemOf(treeNode);
1649
1768
  const isLeaf = userItem.isLeaf ?? childList.length === 0;
1650
1769
  const millerNode = {
1651
1770
  item: {
@@ -1654,10 +1773,35 @@ var MillerTreeController = class {
1654
1773
  },
1655
1774
  children: isLeaf ? [] : void 0
1656
1775
  };
1657
- this.#nodeToTree.set(millerNode, treeNode);
1658
- this.#identityToMillerNode.set(treeNode.getIdentity(), millerNode);
1776
+ this.#register(millerNode, treeNode);
1659
1777
  return millerNode;
1660
1778
  }
1779
+ #register(node, treeNode) {
1780
+ this.#nodeToTree.set(node, treeNode);
1781
+ this.#identityToMillerNode.set(treeNode.getIdentity(), node);
1782
+ this.#identityToTree.set(treeNode.getIdentity(), treeNode);
1783
+ this.#keyToTreeNode.set(node.item.key, treeNode);
1784
+ }
1785
+ /** Patch `existing` (in place) to match the source children of `treeNode`; recurse into loaded nodes. */
1786
+ #syncChildren(treeNode, existing, kept) {
1787
+ const byKey = new Map(existing.map((n) => [n.item.key, n]));
1788
+ const next = Array.from(treeNode.children(), (child) => {
1789
+ const fresh = this.#toMillerNode(child);
1790
+ const old = byKey.get(fresh.item.key);
1791
+ if (!old) {
1792
+ kept.add(fresh);
1793
+ return fresh;
1794
+ }
1795
+ old.item = fresh.item;
1796
+ this.#register(old, child);
1797
+ kept.add(old);
1798
+ if (fresh.item.isLeaf) old.children = [];
1799
+ else if (old.children !== void 0) this.#syncChildren(child, old.children, kept);
1800
+ else old.children = void 0;
1801
+ return old;
1802
+ });
1803
+ existing.splice(0, existing.length, ...next);
1804
+ }
1661
1805
  /** DFS through the ixfx tree returning the path of nodes to the first match. */
1662
1806
  #findTreePath(node, predicate, ancestors = []) {
1663
1807
  const path = [...ancestors, node];
@@ -2386,6 +2386,69 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2386
2386
  this.#dispatchSelect(prev);
2387
2387
  this.requestUpdate();
2388
2388
  }
2389
+ /**
2390
+ * Expand every already-loaded branch in a single update. With `maxDepth`,
2391
+ * only branches at depth < `maxDepth` are expanded (the root's children are
2392
+ * depth 0), so rows at depth `maxDepth` become visible but stay collapsed.
2393
+ *
2394
+ * Unloaded (lazy) branches are not fetched — call again once their children
2395
+ * have loaded to expand further. Bulk expansion ignores `exclusivity`. Fires
2396
+ * one `expand` event per branch that actually opens — none for branches that
2397
+ * were already expanded, including ones opened by filter auto-expansion.
2398
+ */
2399
+ expandAll(maxDepth) {
2400
+ this.#bulkExpand(true, maxDepth);
2401
+ }
2402
+ /**
2403
+ * Collapse expanded branches in a single update. With `maxDepth`, only
2404
+ * branches at depth >= `maxDepth` are collapsed, leaving the tree open down
2405
+ * to depth `maxDepth - 1` (`collapseAll(0)` is a full collapse). Fires one
2406
+ * `collapse` event per branch that actually closes.
2407
+ */
2408
+ collapseAll(maxDepth) {
2409
+ this.#bulkExpand(false, maxDepth);
2410
+ }
2411
+ /** Shared bulk expand / collapse: one `_expandedKeys` write, per-node events for real changes only. */
2412
+ #bulkExpand(expand, maxDepth) {
2413
+ const root = this.root;
2414
+ if (!root) return;
2415
+ const inScope = (depth) => expand ? depth < (maxDepth ?? Number.POSITIVE_INFINITY) : depth >= (maxDepth ?? 0);
2416
+ const changed = [];
2417
+ let next = this._expandedKeys;
2418
+ const visit = (node, depth) => {
2419
+ const children = node.children;
2420
+ if (node.item.isLeaf !== true && children !== void 0 && children.length > 0 && inScope(depth)) {
2421
+ const keyed = next.has(node.item.key);
2422
+ if (expand && !keyed) {
2423
+ next = withKey(next, node.item.key);
2424
+ if (!this._isExpanded(node, depth)) changed.push({
2425
+ node,
2426
+ depth
2427
+ });
2428
+ } else if (!expand && keyed) {
2429
+ next = withoutKey(next, node.item.key);
2430
+ changed.push({
2431
+ node,
2432
+ depth
2433
+ });
2434
+ }
2435
+ }
2436
+ for (const child of children ?? []) visit(child, depth + 1);
2437
+ };
2438
+ for (const child of root.children ?? []) visit(child, 0);
2439
+ if (next === this._expandedKeys) return;
2440
+ this.#setExpanded(next);
2441
+ const type = expand ? `expand` : `collapse`;
2442
+ for (const { node, depth } of changed) this.dispatchEvent(new CustomEvent(type, {
2443
+ detail: {
2444
+ node,
2445
+ depth
2446
+ },
2447
+ bubbles: true,
2448
+ composed: true
2449
+ }));
2450
+ this.requestUpdate();
2451
+ }
2389
2452
  navigateTo(keys, options = {}) {
2390
2453
  if (!this.root?.children) return false;
2391
2454
  const prevOrigin = this.#selectOrigin;
@@ -213,6 +213,14 @@ type TreeDropEvent = CustomEvent<TreeDropDetail>;
213
213
  type TreeDragEndEvent = CustomEvent<{
214
214
  dropped: boolean;
215
215
  }>;
216
+ /** How `navigateTo` scrolls its target: edge-align it, or centre it when it is off screen. */
217
+ type TreeRevealMode = `edge` | `center-if-hidden`;
218
+ type TreeNavigateOptions = {
219
+ /** Scroll behaviour for the target (default `edge`). */
220
+ reveal?: TreeRevealMode;
221
+ /** Suppress the `select` event this navigation would otherwise fire. */
222
+ silent?: boolean;
223
+ };
216
224
  /**
217
225
  * Fired when the selection set changes by any means (click, keyboard, programmatic).
218
226
  * Event name: `select`
@@ -413,4 +421,4 @@ interface TreeSearchHost extends TreeComponent {
413
421
  readonly updateComplete: Promise<boolean>;
414
422
  }
415
423
  //#endregion
416
- export { TreeDataModelOptions as A, TreeItemDblClickEvent as C, TreeTickleDetail as D, TreeSelectEvent as E, ListDragPreviewSourceResolver as F, ListDragSource as I, ListNativeDragStartDetail as L, ListDragPreview as M, ListDragPreviewContext as N, TreeTickleEvent as O, ListDragPreviewRenderer as P, ListNativeDragStartEvent as R, TreeItemDblClickDetail as S, TreeSelectDetail as T, TreeDropTarget as _, TreeComponent as a, TreeItemClickDetail as b, TreeDragContext as c, TreeDragStartEvent as d, TreeDropApplyContext as f, TreeDropPosition as g, TreeDropEvent as h, TreeCollapseEvent as i, ListDragEntry as j, TreeDataModel as k, TreeDragEndEvent as l, TreeDropDetail as m, TreeActivateEvent as n, TreeComponentEventMap as o, TreeDropContext as p, TreeCollapseDetail as r, TreeDndMode as s, TreeActivateDetail as t, TreeDragOverEvent as u, TreeExpandDetail as v, TreeSearchHost as w, TreeItemClickEvent as x, TreeExpandEvent as y };
424
+ export { TreeTickleEvent as A, ListNativeDragStartEvent as B, TreeItemDblClickEvent as C, TreeSelectDetail as D, TreeSearchHost as E, ListDragPreviewContext as F, ListDragPreviewRenderer as I, ListDragPreviewSourceResolver as L, TreeDataModelOptions as M, ListDragEntry as N, TreeSelectEvent as O, ListDragPreview as P, ListDragSource as R, TreeItemDblClickDetail as S, TreeRevealMode as T, TreeDropTarget as _, TreeComponent as a, TreeItemClickDetail as b, TreeDragContext as c, TreeDragStartEvent as d, TreeDropApplyContext as f, TreeDropPosition as g, TreeDropEvent as h, TreeCollapseEvent as i, TreeDataModel as j, TreeTickleDetail as k, TreeDragEndEvent as l, TreeDropDetail as m, TreeActivateEvent as n, TreeComponentEventMap as o, TreeDropContext as p, TreeCollapseDetail as r, TreeDndMode as s, TreeActivateDetail as t, TreeDragOverEvent as u, TreeExpandDetail as v, TreeNavigateOptions as w, TreeItemClickEvent as x, TreeExpandEvent as y, ListNativeDragStartDetail as z };
package/dist/tree.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as TreeDataModelOptions, D as TreeTickleDetail, E as TreeSelectEvent, O as TreeTickleEvent, T as TreeSelectDetail, _ as TreeDropTarget, b as TreeItemClickDetail, c as TreeDragContext, d as TreeDragStartEvent, g as TreeDropPosition, h as TreeDropEvent, i as TreeCollapseEvent, k as TreeDataModel, l as TreeDragEndEvent, m as TreeDropDetail, n as TreeActivateEvent, o as TreeComponentEventMap, p as TreeDropContext, r as TreeCollapseDetail, s as TreeDndMode, t as TreeActivateDetail, u as TreeDragOverEvent, v as TreeExpandDetail, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-Bx40hxMz.js";
1
+ import { A as TreeTickleEvent, D as TreeSelectDetail, M as TreeDataModelOptions, O as TreeSelectEvent, T as TreeRevealMode, _ as TreeDropTarget, b as TreeItemClickDetail, c as TreeDragContext, d as TreeDragStartEvent, g as TreeDropPosition, h as TreeDropEvent, i as TreeCollapseEvent, j as TreeDataModel, k as TreeTickleDetail, l as TreeDragEndEvent, m as TreeDropDetail, n as TreeActivateEvent, o as TreeComponentEventMap, p as TreeDropContext, r as TreeCollapseDetail, s as TreeDndMode, t as TreeActivateDetail, u as TreeDragOverEvent, v as TreeExpandDetail, w as TreeNavigateOptions, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-Tslymyyl.js";
2
2
  import { a as TreeItem, n as TreeExclusivity, o as TreeNode, r as TreeFilterPredicate, s as TreeSelectionFilter, t as LoadChildrenCallback } from "./types-DQ7KOUIj.js";
3
- import { a as TreeDropVisibleRow, c as TreeListElement, d as TreeBaseElementEventMap, f as TreeNavigateOptions, h as TreeConfig, i as TreeDropRowInfo, l as TreeCheckboxState, m as TreeVirtualize, n as TreeDropControllerConfig, o as TreeController, p as TreeRevealMode, r as TreeDropResolvedDrag, s as TreeControllerOptions, t as TreeDropController, u as TreeBaseElement } from "./index-AifRs3k9.js";
4
- export { type LoadChildrenCallback, type TreeActivateDetail, type TreeActivateEvent, TreeBaseElement, TreeBaseElementEventMap, type TreeCheckboxState, type TreeCollapseDetail, type TreeCollapseEvent, type TreeComponentEventMap, TreeConfig, TreeController, TreeControllerOptions, TreeDataModel, type TreeDataModelOptions, type TreeDndMode, type TreeDragContext, type TreeDragEndEvent, type TreeDragOverEvent, type TreeDragStartEvent, type TreeDropContext, TreeDropController, TreeDropControllerConfig, type TreeDropDetail, type TreeDropEvent, type TreeDropPosition, TreeDropResolvedDrag, TreeDropRowInfo, type TreeDropTarget, TreeDropVisibleRow, type TreeExclusivity, type TreeExpandDetail, type TreeExpandEvent, type TreeFilterPredicate, type TreeItem, type TreeItemClickDetail, type TreeItemClickEvent, TreeListElement, TreeNavigateOptions, type TreeNode, TreeRevealMode, type TreeSelectDetail, type TreeSelectEvent, type TreeSelectionFilter, type TreeTickleDetail, type TreeTickleEvent, TreeVirtualize };
3
+ import { a as TreeDropVisibleRow, c as TreeListElement, d as TreeBaseElementEventMap, f as TreeVirtualize, i as TreeDropRowInfo, l as TreeCheckboxState, n as TreeDropControllerConfig, o as TreeController, p as TreeConfig, r as TreeDropResolvedDrag, s as TreeControllerOptions, t as TreeDropController, u as TreeBaseElement } from "./index-CSCrK0hR.js";
4
+ export { type LoadChildrenCallback, type TreeActivateDetail, type TreeActivateEvent, TreeBaseElement, TreeBaseElementEventMap, type TreeCheckboxState, type TreeCollapseDetail, type TreeCollapseEvent, type TreeComponentEventMap, TreeConfig, TreeController, TreeControllerOptions, TreeDataModel, type TreeDataModelOptions, type TreeDndMode, type TreeDragContext, type TreeDragEndEvent, type TreeDragOverEvent, type TreeDragStartEvent, type TreeDropContext, TreeDropController, TreeDropControllerConfig, type TreeDropDetail, type TreeDropEvent, type TreeDropPosition, TreeDropResolvedDrag, TreeDropRowInfo, type TreeDropTarget, TreeDropVisibleRow, type TreeExclusivity, type TreeExpandDetail, type TreeExpandEvent, type TreeFilterPredicate, type TreeItem, type TreeItemClickDetail, type TreeItemClickEvent, TreeListElement, type TreeNavigateOptions, type TreeNode, type TreeRevealMode, type TreeSelectDetail, type TreeSelectEvent, type TreeSelectionFilter, type TreeTickleDetail, type TreeTickleEvent, TreeVirtualize };
package/dist/tree.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-BjrfaX7d.js";
1
+ import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-9Hs7VxuX.js";
2
2
  export { TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeListElement };
@@ -1,6 +1,6 @@
1
- import { F as ListDragPreviewSourceResolver, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent } from "./tree-component-Bx40hxMz.js";
1
+ import { B as ListNativeDragStartEvent, I as ListDragPreviewRenderer, L as ListDragPreviewSourceResolver } from "./tree-component-Tslymyyl.js";
2
2
  import { n as ListViewportChangeEvent } from "./list-viewport-types-BRZsLcvp.js";
3
- import { S as ListSearchController } from "./index-CzFRWPzh.js";
3
+ import { S as ListSearchController } from "./index-CO6m-YC0.js";
4
4
  import { i as ListInteractionMode, l as ListSelectionMode, r as ListElementEventMap } from "./list-selection-types-PjKdhF-a.js";
5
5
  import "./editable-label-rzXkjf1H.js";
6
6
  import { LitElement, PropertyValues } from "lit";
@@ -1,5 +1,5 @@
1
- import { I as ListDragSource, L as ListNativeDragStartDetail, M as ListDragPreview, N as ListDragPreviewContext, P as ListDragPreviewRenderer, R as ListNativeDragStartEvent, j as ListDragEntry } from "./tree-component-Bx40hxMz.js";
1
+ import { B as ListNativeDragStartEvent, F as ListDragPreviewContext, I as ListDragPreviewRenderer, N as ListDragEntry, P as ListDragPreview, R as ListDragSource, z as ListNativeDragStartDetail } from "./tree-component-Tslymyyl.js";
2
2
  import { n as ListViewportChangeEvent, t as ListViewportChangeDetail } from "./list-viewport-types-BRZsLcvp.js";
3
3
  import { a as ListItemClickDetail, c as ListSelectEvent, d as ListTickleEvent, i as ListInteractionMode, l as ListSelectionMode, n as ListActivateEvent, o as ListItemClickEvent, r as ListElementEventMap, s as ListSelectDetail, t as ListActivateDetail, u as ListTickleDetail } from "./list-selection-types-PjKdhF-a.js";
4
- import { a as ListContextMenuDetail, c as ListDragLeaveEvent, d as ListDropEvent, f as ListEditCancelDetail, h as ListEditCommitEvent, i as VerticalListElementEventMap, l as ListDragOverEvent, m as ListEditCommitDetail, n as CustomCheckboxOptions, o as ListContextMenuEvent, p as ListEditCancelEvent, r as VerticalListElement, s as ListDragLeaveDetail, t as AddItemOptions, u as ListDropDetail } from "./vertical-list-BohPAS04.js";
4
+ import { a as ListContextMenuDetail, c as ListDragLeaveEvent, d as ListDropEvent, f as ListEditCancelDetail, h as ListEditCommitEvent, i as VerticalListElementEventMap, l as ListDragOverEvent, m as ListEditCommitDetail, n as CustomCheckboxOptions, o as ListContextMenuEvent, p as ListEditCancelEvent, r as VerticalListElement, s as ListDragLeaveDetail, t as AddItemOptions, u as ListDropDetail } from "./vertical-list-B1Fq06xr.js";
5
5
  export { AddItemOptions, CustomCheckboxOptions, type ListActivateDetail, type ListActivateEvent, type ListContextMenuDetail, type ListContextMenuEvent, type ListDragEntry, type ListDragLeaveDetail, type ListDragLeaveEvent, type ListDragOverEvent, type ListDragPreview, type ListDragPreviewContext, type ListDragPreviewRenderer, type ListDragSource, type ListDropDetail, type ListDropEvent, type ListEditCancelDetail, type ListEditCancelEvent, type ListEditCommitDetail, type ListEditCommitEvent, type ListElementEventMap, type ListInteractionMode, type ListItemClickDetail, type ListItemClickEvent, type ListNativeDragStartDetail, type ListNativeDragStartEvent, type ListSelectDetail, type ListSelectEvent, type ListSelectionMode, type ListTickleDetail, type ListTickleEvent, type ListViewportChangeDetail, type ListViewportChangeEvent, VerticalListElement, VerticalListElementEventMap };
@@ -118,7 +118,7 @@ Labels keep their on-screen size while zooming, so zooming in spreads a crowd ap
118
118
 
119
119
  ### Child preview
120
120
 
121
- On by default; `disable-child-preview` turns it off. Hovering or keyboard-tickling a node whose children aren't all legible opens a popover listing them. That means some children had their labels culled, or sit beyond `maxRadius`. It shows up to 12 children, then "+N more". Clicking an entry focuses that child and selects it, per `interactionMode`. The popover stays open while the pointer moves onto it, and Escape closes it. Style it with `::part(child-preview)`. Nodes that can show a preview drop their native `<title>` tooltip so the two don't stack. With the preview disabled, every node keeps its tooltip.
121
+ On by default; `disable-child-preview` turns it off. Hovering or keyboard-tickling a node whose children aren't all legible, and resting there for 600ms, opens a popover listing them. That means some children had their labels culled, or sit beyond `maxRadius`. It shows up to 12 children, then "+N more". Clicking an entry focuses that child and selects it, per `interactionMode`. The popover stays open while the pointer moves onto it, and Escape closes it. Starting a drag (pan) cancels a pending preview and closes an open one. Style it with `::part(child-preview)`. Nodes that can show a preview drop their native `<title>` tooltip so the two don't stack. With the preview disabled, every node keeps its tooltip.
122
122
 
123
123
  ### Crowded fans
124
124
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
- "version": "0.13.6",
4
- "generated": "2026-10-05T20:21:38.161Z",
3
+ "version": "0.13.8",
4
+ "generated": "2026-10-08T21:09:54.870Z",
5
5
  "components": [
6
6
  {
7
7
  "name": "ac-text",
@@ -54,7 +54,11 @@ Inherited from theme:
54
54
  // Navigate to a node by key path (root-to-target keys, excluding root itself).
55
55
  // Only works with already-loaded children — returns false if any node along
56
56
  // the path has unloaded children or if a key is not found.
57
- navigateTo(keys: readonly string[]): boolean
57
+ navigateTo(keys: readonly string[], options?: { reveal?: 'edge' | 'center-if-hidden'; silent?: boolean }): boolean
58
+
59
+ // Re-sync after children arrays were patched in place (used by MillerTreeController.sync()):
60
+ // closes columns whose selected node was removed, then re-renders.
61
+ invalidateStructure(): void
58
62
 
59
63
  // Navigate to the first loaded node (breadth-first) matching a predicate.
60
64
  // Returns false if no match is found in the currently loaded tree.
@@ -398,11 +402,27 @@ type MillerTreeControllerOptions<T> = {
398
402
  // key and label are required. If isLeaf is omitted, it is inferred
399
403
  // from whether the TraversableTree node has any children.
400
404
  valueToItem: (value: T, node: TraversableTree<T>) => MillerItem;
405
+ // Installed as element.loadChildren by connect(). Defaults to a synchronous
406
+ // read from the source tree. sync() never touches it.
407
+ loadChildren?: LoadChildrenCallback;
401
408
  };
402
409
  ```
403
410
 
404
411
  ### Methods
405
412
 
413
+ ```typescript
414
+ // Re-derive children of already-loaded nodes from the source tree and patch them
415
+ // in place (arrays mutated, MillerNodes reused by key). viewRoot is reset only if
416
+ // its node was removed. Refreshes the element once.
417
+ sync(): void
418
+
419
+ // Lookups that avoid searching the source tree. Key paths are RELATIVE TO THE
420
+ // VIEW ROOT; identities outside a rebased view return false.
421
+ lookupByKey(key: string): TraversableTree<T> | undefined
422
+ navigateToKeyPath(keys: readonly string[], options?: TreeNavigateOptions): boolean
423
+ navigateToIdentity(identity: unknown, options?: TreeNavigateOptions): boolean
424
+ ```
425
+
406
426
  ```typescript
407
427
  // Wire up the controller — sets element.root and provides loadChildren callback.
408
428
  // Safe to call again after disconnect().
@@ -17,7 +17,7 @@ provider returns that column's choices; items may carry a `count` for display.
17
17
 
18
18
  | Property | Type | Default | Description |
19
19
  |----------|------|---------|-------------|
20
- | `categories` | `readonly MulticolCategory[]` | `[]` | Facet definitions (tree, shown in the header picker menu). Each may carry `defaults`: `grouped`, `groupBy`, `allLabel`, `sort`. |
20
+ | `categories` | `readonly MulticolCategory[]` | `[]` | Facet definitions (tree, shown in the header picker menu). Each may carry `description` (shown as a tooltip on the column header label) and `defaults`: `grouped`, `groupBy`, `allLabel`, `sort`. |
21
21
  | `dataProvider` | `IDataProvider<MulticolQuery, MulticolItem>` \| `undefined` | — | Called per column whenever that column's inputs change. Sync arrays, Promises and async-iterables are supported; the `AbortSignal` is honoured. |
22
22
  | `selectionMode` | `ListSelectionMode` | `'multiple'` | `'none'`, `'single'` or `'multiple'` selection within a column. |
23
23
  | `minColumns` | `number` | `1` | Floor for column removal. |
package/docs-user/tree.md CHANGED
@@ -330,6 +330,39 @@ el.addEventListener('select', ({ detail }) => {
330
330
 
331
331
  ---
332
332
 
333
+ ## Expansion
334
+
335
+ ### Programmatic expansion
336
+
337
+ ```typescript
338
+ // Expand every already-loaded branch
339
+ el.expandAll();
340
+
341
+ // Expand down to depth 1: top-level branches open, so their children
342
+ // (depth 1) are visible (root's children are depth 0)
343
+ el.expandAll(1);
344
+
345
+ // Collapse everything
346
+ el.collapseAll();
347
+
348
+ // Collapse branches at depth 1 and deeper — top-level branches stay open,
349
+ // so the tree remains open down to depth 0
350
+ el.collapseAll(1);
351
+ ```
352
+
353
+ Notes:
354
+
355
+ - Both methods apply in a single update — one re-render for the whole tree.
356
+ - Only **already-loaded** branches are expanded; lazily loaded children are not
357
+ fetched. Call `expandAll()` again after children load to expand further.
358
+ - Bulk expansion ignores `exclusivity` — it is an explicit "open everything"
359
+ command.
360
+ - One `expand` / `collapse` event fires per branch whose state actually changes;
361
+ branches already in the target state (including those opened by filter
362
+ auto-expansion) stay silent.
363
+
364
+ ---
365
+
333
366
  ## Events
334
367
 
335
368
  All events **bubble** and are **composed** (cross shadow-DOM boundaries).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
3
  "type": "module",
4
- "version": "0.13.6",
4
+ "version": "0.13.8",
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "description": "Web components for ixfx — Lit-based UI toolkit",
7
7
  "author": "Clint Heyer",