@ixfx/components 0.14.0 → 0.14.2

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/index.d.ts CHANGED
@@ -2442,6 +2442,7 @@ type MulticolCategoryDefaults = {
2442
2442
  groupBy?: (item: MulticolItem) => string;
2443
2443
  allLabel?: string;
2444
2444
  sort?: SortDirection;
2445
+ menu?: boolean;
2445
2446
  };
2446
2447
  type MulticolCategory = {
2447
2448
  readonly key: string;
@@ -2456,6 +2457,7 @@ type MulticolColumnState = {
2456
2457
  readonly categoryKey: string;
2457
2458
  readonly sort: SortDirection;
2458
2459
  readonly grouped: boolean;
2460
+ readonly menu: boolean;
2459
2461
  readonly width?: number;
2460
2462
  };
2461
2463
  type MulticolColumnsChangeReason = `add` | `remove` | `reorder` | `category` | `sort` | `grouping` | `resize`;
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-9Hs7VxuX.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-BM2CY6uu.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";
@@ -9449,6 +9449,28 @@ __decorate([state()], IcicleTreeListElement.prototype, "_zoomKey", void 0);
9449
9449
  __decorate([query(`#icicle-tooltip`)], IcicleTreeListElement.prototype, "_tooltipEl", void 0);
9450
9450
  IcicleTreeListElement = __decorate([safeCustomElement(`ixfx-icicletree-list`)], IcicleTreeListElement);
9451
9451
  //#endregion
9452
+ //#region src/multicol-list/categories.ts
9453
+ /** A group node only organises the picker menu; it is not a selectable category. */
9454
+ function isGroup(category) {
9455
+ return category.children !== void 0 && category.children.length > 0;
9456
+ }
9457
+ /** All selectable categories (leaves) of a category tree, depth-first. */
9458
+ function flattenCategories(categories) {
9459
+ return categories.flatMap((c) => isGroup(c) ? flattenCategories(c.children) : [c]);
9460
+ }
9461
+ /** Finds any node (group or leaf) by key, searching the whole tree depth-first. */
9462
+ function findCategory(categories, key) {
9463
+ for (const category of categories) {
9464
+ if (category.key === key) return category;
9465
+ const nested = category.children ? findCategory(category.children, key) : void 0;
9466
+ if (nested) return nested;
9467
+ }
9468
+ }
9469
+ /** Like `findCategory`, but only resolves selectable leaves (never a group node). */
9470
+ function findLeafCategory(categories, key) {
9471
+ return flattenCategories(categories).find((c) => c.key === key);
9472
+ }
9473
+ //#endregion
9452
9474
  //#region src/multicol-list/types.ts
9453
9475
  const ALL_KEY = `__all__`;
9454
9476
  //#endregion
@@ -9522,6 +9544,7 @@ var MulticolColumnController = class {
9522
9544
  categoryKey: options.category.key,
9523
9545
  sort: options.categoryDefaults?.sort ?? `asc`,
9524
9546
  grouped: options.categoryDefaults?.grouped ?? false,
9547
+ menu: options.categoryDefaults?.menu ?? true,
9525
9548
  width: 220
9526
9549
  };
9527
9550
  }
@@ -9532,6 +9555,9 @@ var MulticolColumnController = class {
9532
9555
  get categoryKey() {
9533
9556
  return this.#category.key;
9534
9557
  }
9558
+ get menu() {
9559
+ return this.#state.menu;
9560
+ }
9535
9561
  getState() {
9536
9562
  return { ...this.#state };
9537
9563
  }
@@ -9585,7 +9611,8 @@ var MulticolColumnController = class {
9585
9611
  ...this.#state,
9586
9612
  categoryKey: category.key,
9587
9613
  sort: category.defaults?.sort ?? `asc`,
9588
- grouped: category.defaults?.grouped ?? false
9614
+ grouped: category.defaults?.grouped ?? false,
9615
+ menu: category.defaults?.menu ?? true
9589
9616
  };
9590
9617
  this.clearSelection();
9591
9618
  this.#syncGrouping();
@@ -9775,7 +9802,7 @@ function renderItem(item) {
9775
9802
  }
9776
9803
  //#endregion
9777
9804
  //#region src/multicol-list/multicol-list.ts
9778
- registerIcon(`menu`, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16"/><path d="M4 18h16"/><path d="M4 6h16"/></svg>`);
9805
+ registerIcon(`menu`, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`);
9779
9806
  const DEFAULT_COLUMN_WIDTH = 220;
9780
9807
  const MIN_COLUMN_WIDTH = 80;
9781
9808
  const MAX_COLUMN_WIDTH = 480;
@@ -9877,7 +9904,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
9877
9904
  }
9878
9905
  .reorder-handle .ixfx-handle {
9879
9906
  --ixfx-handle-length: 14px;
9880
- --ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 10px);
9907
+ --ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
9881
9908
  --ixfx-handle-dot-size: 3px;
9882
9909
  }
9883
9910
  .column-label {
@@ -9956,6 +9983,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
9956
9983
  position: relative;
9957
9984
  display: flex;
9958
9985
  flex-direction: column;
9986
+ --list-gutter: 0;
9959
9987
  }
9960
9988
  .all-row {
9961
9989
  display: flex;
@@ -10010,7 +10038,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10010
10038
  const existing = existingMap.get(colState.id);
10011
10039
  if (existing) {
10012
10040
  if (existing.categoryKey !== colState.categoryKey) {
10013
- const newCategory = this.categories.find((c) => c.key === colState.categoryKey);
10041
+ const newCategory = findLeafCategory(this.categories, colState.categoryKey);
10014
10042
  if (newCategory) existing.setCategory(newCategory);
10015
10043
  }
10016
10044
  existing.updateState(colState);
@@ -10018,7 +10046,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10018
10046
  existingMap.delete(colState.id);
10019
10047
  continue;
10020
10048
  }
10021
- const category = this.categories.find((c) => c.key === colState.categoryKey);
10049
+ const category = findLeafCategory(this.categories, colState.categoryKey);
10022
10050
  if (!category) continue;
10023
10051
  const controller = this.#createController(colState.id, category);
10024
10052
  controller.updateState(colState);
@@ -10033,7 +10061,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10033
10061
  }
10034
10062
  addColumn(categoryKey, position) {
10035
10063
  if (this.maxColumns > 0 && this._columns.length >= this.maxColumns) return;
10036
- const category = this.categories.find((c) => c.key === categoryKey);
10064
+ const category = findLeafCategory(this.categories, categoryKey);
10037
10065
  if (!category) return;
10038
10066
  const id = `col-${++this._idCounter}`;
10039
10067
  const controller = this.#createController(id, category);
@@ -10075,9 +10103,10 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10075
10103
  `;
10076
10104
  }
10077
10105
  updated(changedProperties) {
10078
- if (!this._autoSeeded && this._columns.length === 0 && this.categories.length > 0) {
10106
+ const seedCategory = flattenCategories(this.categories)[0];
10107
+ if (!this._autoSeeded && this._columns.length === 0 && seedCategory) {
10079
10108
  this._autoSeeded = true;
10080
- this.addColumn(this.categories[0].key);
10109
+ this.addColumn(seedCategory.key);
10081
10110
  }
10082
10111
  if (changedProperties.has(`selectionMode`)) for (const col of this._columns) col.setSelectionMode(this.selectionMode);
10083
10112
  if (changedProperties.has(`dataProvider`)) for (const col of this._columns) col.setDataProvider(this.dataProvider);
@@ -10116,6 +10145,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10116
10145
  this._openCategoryPicker(col, e.currentTarget);
10117
10146
  }}
10118
10147
  >${this._getCategoryLabel(col.categoryKey)}</button>
10148
+ ${col.menu ? html`
10119
10149
  <button
10120
10150
  class="burger-btn"
10121
10151
  data-tooltip="Column options"
@@ -10125,7 +10155,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10125
10155
  }}
10126
10156
  >
10127
10157
  <ixfx-icon name="menu" size="14"></ixfx-icon>
10128
- </button>
10158
+ </button>` : nothing}
10129
10159
  <span
10130
10160
  class="resize-handle"
10131
10161
  ?data-active=${this._resizingColumnId === col.id}
@@ -10138,11 +10168,11 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10138
10168
  `;
10139
10169
  }
10140
10170
  _getCategoryLabel(key) {
10141
- return this.categories.find((c) => c.key === key)?.label ?? key;
10171
+ return findCategory(this.categories, key)?.label ?? key;
10142
10172
  }
10143
10173
  /** Category description if present, else the default 'Change category' hint. */
10144
10174
  _getCategoryTooltip(key) {
10145
- return this.categories.find((c) => c.key === key)?.description ?? `Change category`;
10175
+ return findCategory(this.categories, key)?.description ?? `Change category`;
10146
10176
  }
10147
10177
  #createController(id, category) {
10148
10178
  return new MulticolColumnController({
@@ -10313,7 +10343,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10313
10343
  container.appendChild(separator);
10314
10344
  const atMax = this.maxColumns > 0 && this._columns.length >= this.maxColumns;
10315
10345
  const usedCats = new Set(this._columns.map((c) => c.categoryKey));
10316
- const unused = this.categories.filter((c) => !usedCats.has(c.key));
10346
+ const unused = flattenCategories(this.categories).filter((c) => !usedCats.has(c.key));
10317
10347
  const canAdd = !atMax && unused.length > 0;
10318
10348
  const addSub = document.createElement(`ixfx-menu-sub-trigger`);
10319
10349
  addSub.setAttribute(`label`, `Add column`);
@@ -10357,7 +10387,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10357
10387
  this.addColumn(categoryKey, position);
10358
10388
  }
10359
10389
  _changeColumnCategory(col, categoryKey) {
10360
- const category = this.categories.find((c) => c.key === categoryKey);
10390
+ const category = findLeafCategory(this.categories, categoryKey);
10361
10391
  if (!category) return;
10362
10392
  col.setCategory(category);
10363
10393
  this._emitColumnsChange(`category`);
@@ -10393,10 +10423,11 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10393
10423
  }
10394
10424
  _syncColumnsWithCategories() {
10395
10425
  let changed = false;
10426
+ const leaves = flattenCategories(this.categories);
10396
10427
  for (const col of this._columns) {
10397
- if (this.categories.some((c) => c.key === col.categoryKey)) continue;
10398
- if (this.categories.length === 0) continue;
10399
- col.setCategory(this.categories[0]);
10428
+ if (leaves.some((c) => c.key === col.categoryKey)) continue;
10429
+ if (leaves.length === 0) continue;
10430
+ col.setCategory(leaves[0]);
10400
10431
  changed = true;
10401
10432
  }
10402
10433
  if (changed) this._emitColumnsChange(`category`);
@@ -2578,6 +2578,10 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2578
2578
  _handleItemClick(node, depth, event) {
2579
2579
  if (this.#drop?.isDragging || this.#nativeDrag?.isDragging || this.#postDragSuppressTimer !== void 0) return;
2580
2580
  if (event && event.detail >= 2) return;
2581
+ if (this.interactionMode === `checked` && event && !(event.target instanceof Element && event.target.closest(`.tree-label`))) {
2582
+ this._handleCheckboxClick(event, node, depth);
2583
+ return;
2584
+ }
2581
2585
  if (this.#pendingNode && this._expandedKeys.has(this.#pendingNode.item.key)) this.#abortPending();
2582
2586
  this.tickled.handlePointerEnter(node.item.key);
2583
2587
  this._focusedDepth = depth;
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-9Hs7VxuX.js";
1
+ import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-BM2CY6uu.js";
2
2
  export { TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeListElement };
@@ -1543,6 +1543,8 @@ let VerticalListElement = class VerticalListElement extends LitElement {
1543
1543
  box-sizing: border-box;
1544
1544
  outline: none;
1545
1545
  --ixfx-list-item-padding: var(--ixfx-list-item-padding, var(--list-item-padding, 4px var(--space-m, 8px)));
1546
+ --_item-padding:
1547
+ var(--ixfx-list-item-padding, var(--list-item-padding, 0 var(--space-xs, 3px)));
1546
1548
  }
1547
1549
 
1548
1550
  :host(:focus-visible) {
@@ -1579,7 +1581,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
1579
1581
  ::slotted(li) {
1580
1582
  display: flex;
1581
1583
  align-items: center;
1582
- padding: var(--ixfx-list-item-padding);
1584
+ padding: var(--_item-padding);
1583
1585
  /* Inset each row from the scroll container's edges (margin, not padding:
1584
1586
  padding is inside the border box, so the tickle/selection outlines
1585
1587
  would still sit flush against the edge and be clipped). */
@@ -1701,6 +1703,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
1701
1703
  background: transparent;
1702
1704
  color: inherit;
1703
1705
  outline: 1px solid transparent;
1706
+ padding: var(--space-s);
1704
1707
  }
1705
1708
 
1706
1709
  :host([interaction-mode="checked"]) ::slotted([data-selected][data-tickled]) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
- "version": "0.14.0",
4
- "generated": "2026-10-09T22:14:37.194Z",
3
+ "version": "0.14.2",
4
+ "generated": "2026-10-13T12:04:47.321Z",
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 `description` (shown as a tooltip on the column header label) and `defaults`: `grouped`, `groupBy`, `allLabel`, `sort`. |
20
+ | `categories` | `readonly MulticolCategory[]` | `[]` | Facet definitions (tree, shown in the header picker menu). Only leaves are selectable as a column's category; a node with `children` is just a menu group. Keys are resolved across the whole tree. 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
@@ -160,7 +160,7 @@ Controls how pointer and keyboard gestures translate to selection changes.
160
160
  | `'implicit'` | Single selection follows every click; click also toggles expand/collapse |
161
161
  | `'standard'` | Plain click selects + toggles expand; Ctrl/Cmd+click toggles item; Shift+click range-selects |
162
162
  | `'manual'` | Component never auto-selects; use `select()` / `deselect()` / `clearSelection()` to drive state |
163
- | `'checked'` | Checkboxes on every row; checkbox click recursively selects/deselects loaded descendants; row click only expands/collapses |
163
+ | `'checked'` | Checkboxes on every row; clicking the checkbox — or anywhere left of the label (indent, caret, icon) — recursively selects/deselects loaded descendants; label click only expands/collapses |
164
164
  | `'vscode'` | Like `standard` for clicks; Shift+Up/Down uses snapshot-based toggle-range (VS Code file-explorer style) |
165
165
 
166
166
  Switching `interactionMode` clears the current selection.
@@ -190,6 +190,8 @@ tree.checkboxRenderer = (state) => state === `unchecked`
190
190
 
191
191
  When `checkboxRenderer` is set, the checkbox becomes a `<span class="tree-checkbox" role="checkbox" part="checkbox">` holding your template, with `aria-checked` set to `"true"`, `"false"`, or `"mixed"` to match the tri-state. The span owns the click (same as the native input), so clicking anywhere in it — including empty space when you render nothing for `unchecked` — toggles the row.
192
192
 
193
+ To keep the checkbox easy to aim at, the entire row region left of the label — indent gutter, checkbox, caret and icon — acts as one checkbox hit target in `checked` mode. Only clicks on the label itself expand/collapse.
194
+
193
195
  Both the native input and the custom span expose the `checkbox` CSS part for external styling:
194
196
 
195
197
  ```css
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.14.2",
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "description": "Web components for ixfx — Lit-based UI toolkit",
7
7
  "author": "Clint Heyer",