@ixfx/components 0.13.7 → 0.13.9

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.
@@ -123,6 +123,24 @@ declare class TreeBaseElement extends LitElement implements TreeComponent, TreeS
123
123
  clearSelection(): void;
124
124
  selectAll(): void;
125
125
  deselectAll(): void;
126
+ /**
127
+ * Expand every already-loaded branch in a single update. With `maxDepth`,
128
+ * only branches at depth < `maxDepth` are expanded (the root's children are
129
+ * depth 0), so rows at depth `maxDepth` become visible but stay collapsed.
130
+ *
131
+ * Unloaded (lazy) branches are not fetched — call again once their children
132
+ * have loaded to expand further. Bulk expansion ignores `exclusivity`. Fires
133
+ * one `expand` event per branch that actually opens — none for branches that
134
+ * were already expanded, including ones opened by filter auto-expansion.
135
+ */
136
+ expandAll(maxDepth?: number): void;
137
+ /**
138
+ * Collapse expanded branches in a single update. With `maxDepth`, only
139
+ * branches at depth >= `maxDepth` are collapsed, leaving the tree open down
140
+ * to depth `maxDepth - 1` (`collapseAll(0)` is a full collapse). Fires one
141
+ * `collapse` event per branch that actually closes.
142
+ */
143
+ collapseAll(maxDepth?: number): void;
126
144
  navigateTo(keys: readonly string[], options?: TreeNavigateOptions): boolean;
127
145
  navigateToNode(predicate: (node: TreeNode) => boolean): boolean;
128
146
  /** Expand ancestors + move the cursor to `key`, without touching the selection (search "navigate" mode). */
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ import { a as ColourPickerInputEvent, i as ColourPickerEventDetail, n as ColourP
9
9
  import { t as LabelledRangeInput } from "./index-CoqtMSd9.js";
10
10
  import { A as TreeTickleEvent, B as ListNativeDragStartEvent, C as TreeItemDblClickEvent, D as TreeSelectDetail, E as TreeSearchHost, F as ListDragPreviewContext, I as ListDragPreviewRenderer, L as ListDragPreviewSourceResolver, M as TreeDataModelOptions, N as ListDragEntry, O as TreeSelectEvent, P as ListDragPreview, R as ListDragSource, S as TreeItemDblClickDetail, T as TreeRevealMode, _ as TreeDropTarget, a as TreeComponent, b as TreeItemClickDetail, c as TreeDragContext, d as TreeDragStartEvent, f as TreeDropApplyContext, 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, z as ListNativeDragStartDetail } from "./tree-component-Tslymyyl.js";
11
11
  import { a as TreeItem, i as TreeInteractionMode, n as TreeExclusivity, o as TreeNode, r as TreeFilterPredicate, s as TreeSelectionFilter, t as LoadChildrenCallback } from "./types-DQ7KOUIj.js";
12
- 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-BG8E13J5.js";
12
+ 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";
13
13
  import "./menu-item-bF6narIz.js";
14
14
  import { a as NarrowedTextItem, i as NarrowedTextChangeEvent, n as FuzzySearchResult, o as Orientation, r as NarrowedTextChangeDetail, s as SelectionMode, t as NarrowedTextElement } from "./index-o-CWDt11.js";
15
15
  import { CrumbNavigationChangeDetail, CrumbNavigationChangeEvent, CrumbNavigationElement, CrumbNavigationElementEventMap, CrumbNavigationExpandDetail, CrumbNavigationExpandEvent, CrumbNavigationItemClickDetail, CrumbNavigationItemClickEvent } from "./crumbs.js";
@@ -2446,6 +2446,8 @@ type MulticolCategoryDefaults = {
2446
2446
  type MulticolCategory = {
2447
2447
  readonly key: string;
2448
2448
  readonly label: string;
2449
+ /** Shown as a tooltip on the column header label. */
2450
+ readonly description?: string;
2449
2451
  readonly children?: readonly MulticolCategory[];
2450
2452
  readonly defaults?: MulticolCategoryDefaults;
2451
2453
  };
@@ -2523,6 +2525,8 @@ declare class MulticolListElement extends LitElement {
2523
2525
  protected updated(changedProperties: Map<string, unknown>): void;
2524
2526
  private _renderColumn;
2525
2527
  private _getCategoryLabel;
2528
+ /** Category description if present, else the default 'Change category' hint. */
2529
+ private _getCategoryTooltip;
2526
2530
  private _onHeaderPointerDown;
2527
2531
  private _onResizePointerDown;
2528
2532
  private _openCategoryPicker;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { t as CheckedSelect } from "./checked-select-lDgAQQ7v.js";
27
27
  import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-BCMU7W7R.js";
28
28
  import { LabelledRangeInput } from "./labelled-range-input.js";
29
29
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
30
- import { a as TreeDataModel, c as pruneDraggedDescendants, i as TreeDropController, n as TreeListElement, o as applyTreeDrop, r as TreeBaseElement, s as dropTargetFor, t as TreeController } from "./tree-BjrfaX7d.js";
30
+ import { a as TreeDataModel, c as pruneDraggedDescendants, i as TreeDropController, n as TreeListElement, o as applyTreeDrop, r as TreeBaseElement, s as dropTargetFor, t as TreeController } from "./tree-9Hs7VxuX.js";
31
31
  import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
32
32
  import { NarrowedTextElement } from "./narrowed-text.js";
33
33
  import { CrumbNavigationElement } from "./crumbs.js";
@@ -3848,7 +3848,7 @@ TabListItemElement = __decorate([safeCustomElement(`ixfx-tab-list-item`)], TabLi
3848
3848
  //#endregion
3849
3849
  //#region src/dock/dock-panel.ts
3850
3850
  /** Pointer movement threshold before a titlebar press becomes a dock drag. */
3851
- const DRAG_THRESHOLD_PX$3 = 5;
3851
+ const DRAG_THRESHOLD_PX$4 = 5;
3852
3852
  let DockPanelElement = class DockPanelElement extends LitElement {
3853
3853
  constructor(..._args) {
3854
3854
  super(..._args);
@@ -3880,7 +3880,7 @@ let DockPanelElement = class DockPanelElement extends LitElement {
3880
3880
  this._onPointerMove = (event) => {
3881
3881
  if (this._dragPointerId === null || event.pointerId !== this._dragPointerId) return;
3882
3882
  if (!this._dragStarted) {
3883
- if (Math.hypot(event.clientX - this._dragStartX, event.clientY - this._dragStartY) < DRAG_THRESHOLD_PX$3) return;
3883
+ if (Math.hypot(event.clientX - this._dragStartX, event.clientY - this._dragStartY) < DRAG_THRESHOLD_PX$4) return;
3884
3884
  this._dragStarted = true;
3885
3885
  const pointerId = this._dragPointerId;
3886
3886
  this._teardownDragListeners();
@@ -4035,7 +4035,7 @@ DockPanelElement = __decorate([safeCustomElement(`ixfx-dock-panel`)], DockPanelE
4035
4035
  /** Fraction of the leaf rect (from each edge) that forms the split bands. */
4036
4036
  const BAND = .25;
4037
4037
  /** Pointer movement threshold before a press becomes a dock drag (matches tab-list). */
4038
- const DRAG_THRESHOLD_PX$2 = 5;
4038
+ const DRAG_THRESHOLD_PX$3 = 5;
4039
4039
  /** Tolerance (px) when matching a child gutter endpoint to a parent gutter line. */
4040
4040
  const JUNCTION_TOL_PX = 6;
4041
4041
  /** Junction points closer than this merge into one handle (a 4-way cross yields one per side). */
@@ -4152,7 +4152,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4152
4152
  this._onTabDragMove = (event) => {
4153
4153
  const pending = this._tabDragPending;
4154
4154
  if (pending === null || event.pointerId !== pending.pointerId) return;
4155
- if (Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY) < DRAG_THRESHOLD_PX$2) return;
4155
+ if (Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY) < DRAG_THRESHOLD_PX$3) return;
4156
4156
  this._tabDragPending = null;
4157
4157
  this._teardownTabDragListeners();
4158
4158
  this._suppressNextClick();
@@ -5880,6 +5880,16 @@ function createVerticalListAdapter(renderItem, options) {
5880
5880
  });
5881
5881
  return adapter;
5882
5882
  }
5883
+ //#endregion
5884
+ //#region src/hyperbolic-tree/child-preview.ts
5885
+ /** Lucide `arrow-up`. */
5886
+ const upIcon = svg`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>`;
5887
+ /** Picks the popover's side for a node anchored at `anchor` in a `width` x `height` host. */
5888
+ function choosePreviewSide(anchor, width, height) {
5889
+ if (anchor.y < height * .25) return `below`;
5890
+ if (anchor.y > height * .75) return `above`;
5891
+ return anchor.x > width / 2 ? `left` : `right`;
5892
+ }
5883
5893
  /**
5884
5894
  * Whether `n` deserves a preview: it has children and at least one of them
5885
5895
  * isn't legible right now — laid out without a label, or not laid out at
@@ -5906,7 +5916,7 @@ function renderChildPreview(preview, handlers) {
5906
5916
  const more = children.length - listed.length;
5907
5917
  return html`
5908
5918
  <div
5909
- class="child-preview ${preview.above ? `above` : `below`}"
5919
+ class="child-preview ${preview.side}"
5910
5920
  part="child-preview"
5911
5921
  role="dialog"
5912
5922
  aria-label=${`Children of ${preview.node.item.label}`}
@@ -5915,7 +5925,17 @@ function renderChildPreview(preview, handlers) {
5915
5925
  @pointerleave=${handlers.onPointerLeave}
5916
5926
  @pointerdown=${(e) => e.stopPropagation()}
5917
5927
  >
5918
- <div class="child-preview-title">${preview.node.item.label}</div>
5928
+ <div class="child-preview-title">
5929
+ ${preview.parent ? html`<button
5930
+ type="button"
5931
+ class="child-preview-up"
5932
+ tabindex="-1"
5933
+ title=${`Up to ${preview.parent.item.label}`}
5934
+ aria-label=${`Up to ${preview.parent.item.label}`}
5935
+ @click=${() => handlers.onPick(preview.parent)}
5936
+ >${upIcon}</button>` : nothing}
5937
+ <span>${preview.node.item.label}</span>
5938
+ </div>
5919
5939
  <ul>
5920
5940
  ${listed.map((child) => html`<li>
5921
5941
  <button type="button" tabindex="-1" @click=${() => handlers.onPick(child)}>${child.item.label}</button>
@@ -5926,9 +5946,11 @@ function renderChildPreview(preview, handlers) {
5926
5946
  `;
5927
5947
  }
5928
5948
  /**
5929
- * Show/hide timing for the preview: shows at once for a qualifying node;
5930
- * otherwise hides after a short grace period, unless the pointer is over the
5931
- * popover itself — so it can travel from the node onto the list.
5949
+ * Show/hide timing for the preview: opens only after the pointer has rested
5950
+ * on a qualifying node for {@link PREVIEW_SHOW_DELAY_MS} (re-checked when the
5951
+ * delay elapses, so a drag in the meantime suppresses it); otherwise hides
5952
+ * after a short grace period, unless the pointer is over the popover itself —
5953
+ * so it can travel from the node onto the list.
5932
5954
  */
5933
5955
  function createChildPreviewTiming(host) {
5934
5956
  let hovered = false;
@@ -5937,7 +5959,11 @@ function createChildPreviewTiming(host) {
5937
5959
  clearTimeout(timer);
5938
5960
  const candidate = host.candidate(id);
5939
5961
  if (candidate !== void 0) {
5940
- host.setPreviewId(candidate);
5962
+ if (candidate === host.currentId()) return;
5963
+ timer = setTimeout(() => {
5964
+ const still = host.candidate(id);
5965
+ if (still !== void 0) host.setPreviewId(still);
5966
+ }, 600);
5941
5967
  return;
5942
5968
  }
5943
5969
  timer = setTimeout(() => {
@@ -6279,6 +6305,7 @@ function isBackground(event) {
6279
6305
  function createGestureHandlers(host) {
6280
6306
  const pointers = /* @__PURE__ */ new Map();
6281
6307
  let pan;
6308
+ let dragged = false;
6282
6309
  let pinch;
6283
6310
  const screenOf = (event) => {
6284
6311
  const rect = host.svgRect();
@@ -6289,11 +6316,13 @@ function createGestureHandlers(host) {
6289
6316
  x: (a.x + b.x) / 2,
6290
6317
  y: (a.y + b.y) / 2
6291
6318
  });
6292
- const startPan = (pointerId, screen) => {
6319
+ const startPan = (pointerId, screen, downClient, armed) => {
6293
6320
  pan = {
6294
6321
  pointerId,
6295
6322
  start: diskOf(screen),
6296
- baseView: host.takeView()
6323
+ baseView: host.takeView(),
6324
+ downClient,
6325
+ armed
6297
6326
  };
6298
6327
  };
6299
6328
  const startPinch = () => {
@@ -6309,14 +6338,21 @@ function createGestureHandlers(host) {
6309
6338
  };
6310
6339
  return {
6311
6340
  onPointerDown(event) {
6312
- if (!isBackground(event)) return;
6341
+ dragged = false;
6342
+ const onBackground = isBackground(event);
6313
6343
  host.focusTree();
6314
6344
  const screen = screenOf(event);
6315
6345
  if (!screen) return;
6316
- event.currentTarget.setPointerCapture?.(event.pointerId);
6346
+ if (onBackground) {
6347
+ event.currentTarget.setPointerCapture?.(event.pointerId);
6348
+ host.onDragStart?.();
6349
+ }
6317
6350
  pointers.set(event.pointerId, screen);
6318
6351
  if (pointers.size === 2 && host.zoomGesturesEnabled()) startPinch();
6319
- else if (pointers.size === 1) startPan(event.pointerId, screen);
6352
+ else if (pointers.size === 1) startPan(event.pointerId, screen, {
6353
+ x: event.clientX,
6354
+ y: event.clientY
6355
+ }, onBackground);
6320
6356
  },
6321
6357
  onPointerMove(event) {
6322
6358
  if (!pointers.has(event.pointerId)) return;
@@ -6337,14 +6373,26 @@ function createGestureHandlers(host) {
6337
6373
  y: mid.y - clampedZoom * pinch.anchor.y
6338
6374
  }
6339
6375
  }));
6340
- } else if (pan && pan.pointerId === event.pointerId) host.setView(panView(pan.baseView, pan.start, diskOf(screen)));
6376
+ } else if (pan && pan.pointerId === event.pointerId) {
6377
+ if (!pan.armed) {
6378
+ if (Math.hypot(event.clientX - pan.downClient.x, event.clientY - pan.downClient.y) < 4) return;
6379
+ pan.armed = true;
6380
+ dragged = true;
6381
+ event.currentTarget.setPointerCapture?.(event.pointerId);
6382
+ host.onDragStart?.();
6383
+ }
6384
+ host.setView(panView(pan.baseView, pan.start, diskOf(screen)));
6385
+ }
6341
6386
  },
6342
6387
  onPointerUp(event) {
6343
6388
  if (!pointers.delete(event.pointerId)) return;
6344
6389
  if (pinch) {
6345
6390
  pinch = void 0;
6346
6391
  const [remaining] = [...pointers.entries()];
6347
- if (remaining) startPan(remaining[0], remaining[1]);
6392
+ if (remaining) startPan(remaining[0], remaining[1], {
6393
+ x: 0,
6394
+ y: 0
6395
+ }, true);
6348
6396
  } else if (pan?.pointerId === event.pointerId) pan = void 0;
6349
6397
  },
6350
6398
  /**
@@ -6363,6 +6411,14 @@ function createGestureHandlers(host) {
6363
6411
  const factor = Math.exp(-delta * (modified ? .01 : .002));
6364
6412
  host.setZoomState(zoomAt(host.zoomState(), screen, factor));
6365
6413
  },
6414
+ /** Whether the view is being dragged right now. */
6415
+ isDragging() {
6416
+ return pan?.armed === true;
6417
+ },
6418
+ /** Whether the last press turned into a drag — so the click that follows it shouldn't count. */
6419
+ wasDragged() {
6420
+ return dragged;
6421
+ },
6366
6422
  /** Forget any in-progress pan (e.g. when a focus change takes over the view). */
6367
6423
  cancel() {
6368
6424
  pan = void 0;
@@ -6565,6 +6621,15 @@ const hyperbolicTreeStyles = css`
6565
6621
  border-radius: var(--radius-m, 6px);
6566
6622
  box-shadow: var(--shadow-m);
6567
6623
  font-size: var(--hyperbolictree-font-size);
6624
+ /* Beside the node, never over it, with the dot level with the popover's middle. */
6625
+ transform: translate(14px, -50%);
6626
+ }
6627
+
6628
+ .child-preview.left {
6629
+ transform: translate(calc(-100% - 14px), -50%);
6630
+ }
6631
+
6632
+ .child-preview.below {
6568
6633
  transform: translate(-50%, 14px);
6569
6634
  }
6570
6635
 
@@ -6573,13 +6638,26 @@ const hyperbolicTreeStyles = css`
6573
6638
  }
6574
6639
 
6575
6640
  .child-preview-title {
6641
+ display: flex;
6642
+ align-items: center;
6643
+ gap: 4px;
6576
6644
  padding: 2px 6px 4px;
6577
6645
  font-weight: 600;
6646
+ }
6647
+
6648
+ .child-preview-title span {
6578
6649
  overflow: hidden;
6579
6650
  text-overflow: ellipsis;
6580
6651
  white-space: nowrap;
6581
6652
  }
6582
6653
 
6654
+ .child-preview button.child-preview-up {
6655
+ display: inline-flex;
6656
+ flex: none;
6657
+ width: auto;
6658
+ padding: 2px;
6659
+ }
6660
+
6583
6661
  .child-preview ul {
6584
6662
  margin: 0;
6585
6663
  padding: 0;
@@ -7535,7 +7613,6 @@ function renderNodeGroup(n, args, transform, content) {
7535
7613
  part="node node-${partToken(key)}"
7536
7614
  style=${weightStyle + branchStyle(id, ctx.branches)}
7537
7615
  transform=${transform}
7538
- @pointerdown=${handlers.onPointerDown}
7539
7616
  @mousedown=${preventMouseDownFocus}
7540
7617
  @click=${(e) => handlers.onClick(n, e)}
7541
7618
  @dblclick=${(e) => handlers.onDblClick(n, e)}
@@ -7657,8 +7734,9 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
7657
7734
  candidate: (id) => {
7658
7735
  const nodes = this.#layout().nodes;
7659
7736
  const n = id === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === id);
7660
- return n && needsChildPreview(n, nodes, this.#lastLabelIds) ? n.id : void 0;
7737
+ return n && !this.#gestures.isDragging() && needsChildPreview(n, nodes, this.#lastLabelIds) ? n.id : void 0;
7661
7738
  },
7739
+ currentId: () => this._previewId,
7662
7740
  setPreviewId: (id) => {
7663
7741
  this._previewId = id;
7664
7742
  }
@@ -7701,6 +7779,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
7701
7779
  };
7702
7780
  this.#gestures = createGestureHandlers({
7703
7781
  svgRect: () => this.renderRoot.querySelector(`svg.hyperbolictree`)?.getBoundingClientRect(),
7782
+ onDragStart: () => this.#preview.hide(),
7704
7783
  takeView: () => {
7705
7784
  const view = this.#currentView();
7706
7785
  this.#cancelAnimation();
@@ -8170,10 +8249,6 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8170
8249
  #focusTree() {
8171
8250
  this.renderRoot.querySelector(`svg.hyperbolictree`)?.focus({ preventScroll: true });
8172
8251
  }
8173
- #handleNodePointerDown(event) {
8174
- event.stopPropagation();
8175
- this.#focusTree();
8176
- }
8177
8252
  #pickPreviewChild(child) {
8178
8253
  this.#preview.hide();
8179
8254
  if (this.interactionMode !== `manual`) this.select(child);
@@ -8187,10 +8262,12 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8187
8262
  if (!n) return void 0;
8188
8263
  const { width, height } = this._hostSize;
8189
8264
  const anchor = diskToHostPx(n.position, this.#zoomState(), width || 420, height || 420);
8265
+ const parent = n.parentId === void 0 ? void 0 : nodes.find((v) => v.id === n.parentId)?.node;
8190
8266
  return {
8191
8267
  node: n.node,
8192
8268
  anchor,
8193
- above: anchor.y > (height || 420) / 2
8269
+ side: choosePreviewSide(anchor, width || 420, height || 420),
8270
+ parent
8194
8271
  };
8195
8272
  }
8196
8273
  #nodeContext(nodes, branches) {
@@ -8221,14 +8298,13 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8221
8298
  isPending: (node) => this.#lazy.isPending(node)
8222
8299
  },
8223
8300
  handlers: {
8224
- onPointerDown: (e) => this.#handleNodePointerDown(e),
8225
8301
  onClick: (n, e) => {
8226
8302
  e.stopPropagation();
8227
- this.#handleItemClick(n, e);
8303
+ if (!this.#gestures.wasDragged()) this.#handleItemClick(n, e);
8228
8304
  },
8229
8305
  onDblClick: (n, e) => {
8230
8306
  e.stopPropagation();
8231
- this.#handleItemDblClick(n);
8307
+ if (!this.#gestures.wasDragged()) this.#handleItemDblClick(n);
8232
8308
  },
8233
8309
  onPointerEnter: (id) => this.tickled.handlePointerEnter(id)
8234
8310
  }
@@ -10026,7 +10102,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10026
10102
  ><span class="ixfx-handle" data-handle="dotted" data-shape="vertical" part="reorder-handle"></span></span>
10027
10103
  <button
10028
10104
  class="column-label"
10029
- data-tooltip="Change category"
10105
+ data-tooltip=${this._getCategoryTooltip(col.categoryKey)}
10030
10106
  @click=${(e) => {
10031
10107
  e.stopPropagation();
10032
10108
  this._openCategoryPicker(col, e.currentTarget);
@@ -10056,6 +10132,10 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10056
10132
  _getCategoryLabel(key) {
10057
10133
  return this.categories.find((c) => c.key === key)?.label ?? key;
10058
10134
  }
10135
+ /** Category description if present, else the default 'Change category' hint. */
10136
+ _getCategoryTooltip(key) {
10137
+ return this.categories.find((c) => c.key === key)?.description ?? `Change category`;
10138
+ }
10059
10139
  #createController(id, category) {
10060
10140
  return new MulticolColumnController({
10061
10141
  id,
@@ -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;
package/dist/tree.d.ts CHANGED
@@ -1,4 +1,4 @@
1
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 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-BG8E13J5.js";
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
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 };
@@ -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.7",
4
- "generated": "2026-10-05T21:23:39.070Z",
3
+ "version": "0.13.9",
4
+ "generated": "2026-10-09T19:37:11.157Z",
5
5
  "components": [
6
6
  {
7
7
  "name": "ac-text",
@@ -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.7",
4
+ "version": "0.13.9",
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "description": "Web components for ixfx — Lit-based UI toolkit",
7
7
  "author": "Clint Heyer",