@ixfx/components 0.10.0 → 0.10.1

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 (91) hide show
  1. package/bundle/index.d.ts +60 -1
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +257 -30
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +38 -85
  6. package/dist/ac-text.d.ts +1 -1
  7. package/dist/{ac-token--_vCHXWw.d.ts → ac-token-TCLAZeVg.d.ts} +2 -2
  8. package/dist/{ac-token--_vCHXWw.d.ts.map → ac-token-TCLAZeVg.d.ts.map} +1 -1
  9. package/dist/ac-token.d.ts +1 -1
  10. package/dist/button.d.ts +1 -1
  11. package/dist/button.d.ts.map +1 -1
  12. package/dist/colour-picker.d.ts +1 -1
  13. package/dist/crumbs.d.ts +2 -2
  14. package/dist/{data-grid-C5hk_pzK.d.ts → data-grid-BT1d_nlu.d.ts} +4 -4
  15. package/dist/{data-grid-C5hk_pzK.d.ts.map → data-grid-BT1d_nlu.d.ts.map} +1 -1
  16. package/dist/data-grid.d.ts +2 -2
  17. package/dist/data-grid.js +1 -1
  18. package/dist/{detail-list-CxPpv89T.d.ts → detail-list-BCaIN7nx.d.ts} +5 -5
  19. package/dist/{detail-list-CxPpv89T.d.ts.map → detail-list-BCaIN7nx.d.ts.map} +1 -1
  20. package/dist/detail-list.d.ts +3 -3
  21. package/dist/detail-list.js +1 -1
  22. package/dist/{grid-list-BGvoWbZz.d.ts → grid-list-CiwlNE4c.d.ts} +4 -4
  23. package/dist/{grid-list-BGvoWbZz.d.ts.map → grid-list-CiwlNE4c.d.ts.map} +1 -1
  24. package/dist/{grid-list-DSnYOGh7.js → grid-list-DLfw5HI4.js} +10 -5
  25. package/dist/grid-list-DLfw5HI4.js.map +1 -0
  26. package/dist/grid-list.d.ts +2 -2
  27. package/dist/grid-list.js +1 -1
  28. package/dist/{grouped-item-lister-fdZHVxFM.d.ts → grouped-item-lister-CEZ72VOs.d.ts} +2 -2
  29. package/dist/{grouped-item-lister-fdZHVxFM.d.ts.map → grouped-item-lister-CEZ72VOs.d.ts.map} +1 -1
  30. package/dist/grouped-item-lister.d.ts +1 -1
  31. package/dist/{icon-CDGprSdE.d.ts → icon-DR_3rSyZ.d.ts} +2 -2
  32. package/dist/icon-DR_3rSyZ.d.ts.map +1 -0
  33. package/dist/icons.d.ts +2 -2
  34. package/dist/incr-search.d.ts +1 -1
  35. package/dist/{index-tq6f8007.d.ts → index-BOUaDIW-.d.ts} +2 -2
  36. package/dist/{index-tq6f8007.d.ts.map → index-BOUaDIW-.d.ts.map} +1 -1
  37. package/dist/{index-CoqtMSd9.d.ts → index-BUB5SICW.d.ts} +2 -2
  38. package/dist/{index-CoqtMSd9.d.ts.map → index-BUB5SICW.d.ts.map} +1 -1
  39. package/dist/{index-9AhaHIT2.d.ts → index-C59-JEPl.d.ts} +3 -3
  40. package/dist/{index-9AhaHIT2.d.ts.map → index-C59-JEPl.d.ts.map} +1 -1
  41. package/dist/{index-UvnoZUAo.d.ts → index-CJ5fshD3.d.ts} +2 -2
  42. package/dist/{index-UvnoZUAo.d.ts.map → index-CJ5fshD3.d.ts.map} +1 -1
  43. package/dist/{index--5BSdbnL.d.ts → index-DPFrRhKL.d.ts} +2 -2
  44. package/dist/{index--5BSdbnL.d.ts.map → index-DPFrRhKL.d.ts.map} +1 -1
  45. package/dist/{index-CS9yM8L-.d.ts → index-DsjpDksO.d.ts} +2 -2
  46. package/dist/{index-CS9yM8L-.d.ts.map → index-DsjpDksO.d.ts.map} +1 -1
  47. package/dist/index.d.ts +66 -20
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +247 -27
  50. package/dist/index.js.map +1 -1
  51. package/dist/{labelled-input-base-DiTme1JO.d.ts → labelled-input-base-Dp_9KP0G.d.ts} +2 -2
  52. package/dist/{labelled-input-base-DiTme1JO.d.ts.map → labelled-input-base-Dp_9KP0G.d.ts.map} +1 -1
  53. package/dist/labelled-radial-input.d.ts +2 -2
  54. package/dist/labelled-radial-input.d.ts.map +1 -1
  55. package/dist/labelled-range-input.d.ts +1 -1
  56. package/dist/labelled-slider-input.d.ts +1 -1
  57. package/dist/{list-native-drag-controller-D97TYjn1.js → list-native-drag-controller-B9xO0XWn.js} +6 -4
  58. package/dist/list-native-drag-controller-B9xO0XWn.js.map +1 -0
  59. package/dist/{list-native-drag-controller-DWiXQZou.d.ts → list-native-drag-controller-BAuWAtyQ.d.ts} +2 -2
  60. package/dist/{list-native-drag-controller-DWiXQZou.d.ts.map → list-native-drag-controller-BAuWAtyQ.d.ts.map} +1 -1
  61. package/dist/{menu-item-CZi9vy5O.d.ts → menu-item-ZmkXNjek.d.ts} +2 -2
  62. package/dist/{menu-item-CZi9vy5O.d.ts.map → menu-item-ZmkXNjek.d.ts.map} +1 -1
  63. package/dist/menu.d.ts +2 -2
  64. package/dist/miller.d.ts +2 -2
  65. package/dist/panel.d.ts +1 -1
  66. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-C5SYbDrq.d.ts} +2 -2
  67. package/dist/{split-layout-JK3xWVX2.d.ts.map → split-layout-C5SYbDrq.d.ts.map} +1 -1
  68. package/dist/split-layout.d.ts +1 -1
  69. package/dist/style.css +38 -85
  70. package/dist/{tab-list-CGyPt07Z.d.ts → tab-list-DbMIL520.d.ts} +2 -2
  71. package/dist/{tab-list-CGyPt07Z.d.ts.map → tab-list-DbMIL520.d.ts.map} +1 -1
  72. package/dist/tabs.d.ts +1 -1
  73. package/dist/{titlebar-Tqu-VvMU.d.ts → titlebar-B5zSTZnF.d.ts} +2 -2
  74. package/dist/{titlebar-Tqu-VvMU.d.ts.map → titlebar-B5zSTZnF.d.ts.map} +1 -1
  75. package/dist/titlebar.d.ts +1 -1
  76. package/dist/{tree-C25xYUa6.js → tree-BuzWn18_.js} +2 -2
  77. package/dist/{tree-C25xYUa6.js.map → tree-BuzWn18_.js.map} +1 -1
  78. package/dist/{tree-component-mw8x-n2Y.d.ts → tree-component-DGmKBbpy.d.ts} +14 -1
  79. package/dist/tree-component-DGmKBbpy.d.ts.map +1 -0
  80. package/dist/tree.d.ts +3 -3
  81. package/dist/tree.js +1 -1
  82. package/dist/{vertical-list--tFanQwM.d.ts → vertical-list-BLvJ4-Zf.d.ts} +3 -3
  83. package/dist/{vertical-list--tFanQwM.d.ts.map → vertical-list-BLvJ4-Zf.d.ts.map} +1 -1
  84. package/dist/vertical-list.d.ts +2 -2
  85. package/dist/vertical-list.js +1 -1
  86. package/docs-user/index.json +2 -2
  87. package/package.json +1 -1
  88. package/dist/grid-list-DSnYOGh7.js.map +0 -1
  89. package/dist/icon-CDGprSdE.d.ts.map +0 -1
  90. package/dist/list-native-drag-controller-D97TYjn1.js.map +0 -1
  91. package/dist/tree-component-mw8x-n2Y.d.ts.map +0 -1
package/bundle/index.js CHANGED
@@ -5707,14 +5707,12 @@ function fontOf(style) {
5707
5707
  } catch {}
5708
5708
  return `12px system-ui, sans-serif`;
5709
5709
  }
5710
- /** Caption for a drag entry — element labels win, then string items, then common object fields, then the key. */
5710
+ /** Caption for a drag entry — explicit element labels win, then data values, then generic element text, then the key. */
5711
5711
  function captionForEntry(entry) {
5712
5712
  const el = entry.element ?? (entry.item instanceof Element ? entry.item : void 0);
5713
5713
  if (el) {
5714
5714
  const labelled = el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? el.getAttribute(`data-search-label`) ?? el.querySelector(`.cell`)?.textContent?.trim();
5715
5715
  if (labelled) return labelled.slice(0, 80);
5716
- const text = el.textContent?.trim();
5717
- if (text) return text.slice(0, 80);
5718
5716
  }
5719
5717
  const item = entry.item;
5720
5718
  if (typeof item === `string` && item.length > 0) return item.slice(0, 80);
@@ -5730,6 +5728,10 @@ function captionForEntry(entry) {
5730
5728
  if (typeof value === `string` && value.length > 0) return value.slice(0, 80);
5731
5729
  }
5732
5730
  }
5731
+ if (el) {
5732
+ const text = el.textContent?.trim();
5733
+ if (text) return text.slice(0, 80);
5734
+ }
5733
5735
  return entry.key;
5734
5736
  }
5735
5737
  function glyphFor(caption) {
@@ -23784,6 +23786,10 @@ let GridListElement = class GridListElement extends i {
23784
23786
  #postDragSuppressTimer;
23785
23787
  #nativeDrag;
23786
23788
  /** Origin rule + display-ordered entry resolution for native drag-out (both modes). */
23789
+ #renderedCellForKey(key) {
23790
+ const selector = `.cell[data-key="${CSS.escape(key)}"]`;
23791
+ return this.renderRoot.querySelector(selector) ?? void 0;
23792
+ }
23787
23793
  #prepareNativeDrag(originKey) {
23788
23794
  const index = this.#source.indexOfKey(originKey);
23789
23795
  if (index < 0 || !this.#source.isSelectable(index)) return void 0;
@@ -23794,14 +23800,15 @@ let GridListElement = class GridListElement extends i {
23794
23800
  const entryIndex = this.#source.indexOfKey(key);
23795
23801
  if (entryIndex < 0) continue;
23796
23802
  const data = this.#source.resolveKeyToData(key);
23797
- const element = this.#source.resolveKeyToElement(key);
23798
- const item = data ?? element;
23803
+ const sourceElement = this.#source.resolveKeyToElement(key);
23804
+ const renderedElement = sourceElement ?? (this.#source instanceof DataGridListSource ? this.#renderedCellForKey(key) : void 0);
23805
+ const item = data ?? sourceElement;
23799
23806
  if (item === void 0) continue;
23800
23807
  selected.push({
23801
23808
  key,
23802
23809
  index: entryIndex,
23803
23810
  item,
23804
- ...element ? { element } : {}
23811
+ ...renderedElement ? { element: renderedElement } : {}
23805
23812
  });
23806
23813
  }
23807
23814
  const origin = selected.find((entry) => entry.key === originKey);
@@ -45932,12 +45939,16 @@ function subdivide(box, weights, method, relativeDepth) {
45932
45939
  */
45933
45940
  function computeTreemapLayout(root, options = {}) {
45934
45941
  if (!root?.children?.length) return [];
45935
- const { maxDepth = Number.POSITIVE_INFINITY, valueAccessor = () => 1, zoomKey, tilingMethod = `squarify` } = options;
45942
+ const { maxDepth = Number.POSITIVE_INFINITY, valueAccessor = () => 1, zoomKey, tilingMethod = `squarify`, hideLeaves = false } = options;
45936
45943
  if (maxDepth <= 0) return [];
45944
+ const visibleChildren = (node) => {
45945
+ const children = node.children ?? [];
45946
+ return hideLeaves ? children.filter((n) => n.item.isLeaf !== true) : children;
45947
+ };
45937
45948
  const zoomNode = zoomKey === void 0 ? root : findTreemapNode(root, zoomKey) ?? root;
45938
45949
  const zoomPath = findTreemapPath(root, zoomNode.item.key);
45939
45950
  const zoomDepth = zoomPath ? zoomPath.length - 2 : -1;
45940
- const siblings = zoomNode.children ?? [];
45951
+ const siblings = visibleChildren(zoomNode);
45941
45952
  if (siblings.length === 0) return [];
45942
45953
  const rects = [];
45943
45954
  const layoutLevel = (nodes, box, depth, relativeDepth, parentKey) => {
@@ -45946,7 +45957,7 @@ function computeTreemapLayout(root, options = {}) {
45946
45957
  const setSize = nodes.length;
45947
45958
  nodes.forEach((node, index) => {
45948
45959
  const childBox = boxes[index];
45949
- const children = node.children ?? [];
45960
+ const children = visibleChildren(node);
45950
45961
  rects.push({
45951
45962
  node,
45952
45963
  depth,
@@ -45981,8 +45992,11 @@ let TreemapListElement = class TreemapListElement extends i {
45981
45992
  this.interactionMode = `standard`;
45982
45993
  this.tilingMethod = `squarify`;
45983
45994
  this.maxDepth = 4;
45995
+ this.hideLeaves = false;
45984
45996
  this.minLabelArea = 1600;
45997
+ this.minTileWidth = 24;
45985
45998
  this.padding = 4;
45999
+ this.minChildArea = 400;
45986
46000
  this.valueAccessor = () => 1;
45987
46001
  this._selectedNodes = /* @__PURE__ */ new Set();
45988
46002
  this.disableKeyboardNavigation = false;
@@ -46016,6 +46030,7 @@ let TreemapListElement = class TreemapListElement extends i {
46016
46030
  });
46017
46031
  this.#pendingKeys = /* @__PURE__ */ new Set();
46018
46032
  this.#aborters = /* @__PURE__ */ new Map();
46033
+ this.#wheelCooldownUntil = 0;
46019
46034
  }
46020
46035
  get selectedNodes() {
46021
46036
  return this._selectedNodes;
@@ -46032,15 +46047,22 @@ let TreemapListElement = class TreemapListElement extends i {
46032
46047
  #ownedRegistry;
46033
46048
  #unregisterSearchCommands;
46034
46049
  #resizeObserver;
46050
+ #pendingFlip;
46051
+ /** `performance.now()` timestamp before which further Cmd+wheel zoom steps are ignored. */
46052
+ #wheelCooldownUntil;
46035
46053
  get #effectiveRoot() {
46036
46054
  return this.model?.root ?? this.root;
46037
46055
  }
46038
46056
  #rects() {
46057
+ return this.#rectsForZoom(this._zoomKey);
46058
+ }
46059
+ #rectsForZoom(zoomKey) {
46039
46060
  return computeTreemapLayout(this.#effectiveRoot, {
46040
46061
  valueAccessor: this.valueAccessor,
46041
46062
  maxDepth: this.maxDepth,
46042
- zoomKey: this._zoomKey,
46043
- tilingMethod: this.tilingMethod
46063
+ zoomKey,
46064
+ tilingMethod: this.tilingMethod,
46065
+ hideLeaves: this.hideLeaves
46044
46066
  });
46045
46067
  }
46046
46068
  connectedCallback() {
@@ -46116,8 +46138,14 @@ let TreemapListElement = class TreemapListElement extends i {
46116
46138
  if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
46117
46139
  if (this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
46118
46140
  this.#ensureVisibleChildren();
46141
+ if (this.#pendingFlip) {
46142
+ const flip = this.#pendingFlip;
46143
+ this.#pendingFlip = void 0;
46144
+ this.#applyFlip(flip);
46145
+ }
46119
46146
  }
46120
46147
  #resetViewState() {
46148
+ this.#pendingFlip = void 0;
46121
46149
  this.#clearSelectionInternal();
46122
46150
  this.tickled.clearTickled();
46123
46151
  this._zoomKey = void 0;
@@ -46230,6 +46258,7 @@ let TreemapListElement = class TreemapListElement extends i {
46230
46258
  */
46231
46259
  zoomTo(node) {
46232
46260
  if (this._zoomKey === node.item.key) return;
46261
+ this.#pendingFlip = this.#planFlip(node.item.key);
46233
46262
  this._zoomKey = node.item.key;
46234
46263
  this.tickled.clearTickled();
46235
46264
  const depth = findTreemapPath(this.#effectiveRoot, node.item.key)?.length;
@@ -46258,6 +46287,7 @@ let TreemapListElement = class TreemapListElement extends i {
46258
46287
  */
46259
46288
  zoomOut() {
46260
46289
  if (this._zoomKey === void 0) return;
46290
+ this.#pendingFlip = this.#planFlip(void 0);
46261
46291
  const previous = this.focusedNode;
46262
46292
  const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
46263
46293
  const absoluteDepth = path === void 0 ? -1 : path.length - 2;
@@ -46281,9 +46311,66 @@ let TreemapListElement = class TreemapListElement extends i {
46281
46311
  }));
46282
46312
  this.requestUpdate();
46283
46313
  }
46314
+ /**
46315
+ * Work out (before `_zoomKey` changes) whether the upcoming zoom should
46316
+ * animate, and how — see `FlipPlan`. Returns `undefined` to skip the
46317
+ * animation: reduced-motion is requested, or the node needed to anchor it
46318
+ * isn't in either the old or new layout's visible window.
46319
+ */
46320
+ #planFlip(targetKey) {
46321
+ if (typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches) return void 0;
46322
+ if (targetKey !== void 0) {
46323
+ const visible = this.#rects().find((r) => r.node.item.key === targetKey);
46324
+ if (visible) return {
46325
+ direction: `in`,
46326
+ rect: {
46327
+ x: visible.x,
46328
+ y: visible.y,
46329
+ width: visible.width,
46330
+ height: visible.height
46331
+ }
46332
+ };
46333
+ }
46334
+ const fromKey = this._zoomKey;
46335
+ if (fromKey === void 0) return void 0;
46336
+ const landing = this.#rectsForZoom(targetKey).find((r) => r.node.item.key === fromKey);
46337
+ if (!landing) return void 0;
46338
+ return {
46339
+ direction: `out`,
46340
+ rect: {
46341
+ x: landing.x,
46342
+ y: landing.y,
46343
+ width: landing.width,
46344
+ height: landing.height
46345
+ }
46346
+ };
46347
+ }
46348
+ /** Apply a planned FLIP animation once the new layout has rendered — see `FlipPlan`. */
46349
+ #applyFlip(flip) {
46350
+ const inner = this.renderRoot.querySelector(`.viewport-inner`);
46351
+ const viewport = this.renderRoot.querySelector(`.viewport`);
46352
+ if (!(inner instanceof HTMLElement) || !(viewport instanceof HTMLElement)) return;
46353
+ const vw = viewport.clientWidth;
46354
+ const vh = viewport.clientHeight;
46355
+ if (vw <= 0 || vh <= 0) return;
46356
+ const { rect, direction } = flip;
46357
+ const startTransform = direction === `in` ? `translate(${rect.x * vw}px, ${rect.y * vh}px) scale(${Math.max(rect.width, 1e-4)}, ${Math.max(rect.height, 1e-4)})` : (() => {
46358
+ const sx = rect.width > 0 ? 1 / rect.width : 1;
46359
+ const sy = rect.height > 0 ? 1 / rect.height : 1;
46360
+ return `translate(${-rect.x * sx * vw}px, ${-rect.y * sy * vh}px) scale(${sx}, ${sy})`;
46361
+ })();
46362
+ inner.classList.remove(`flipping`);
46363
+ inner.style.transform = startTransform;
46364
+ inner.offsetWidth;
46365
+ requestAnimationFrame(() => {
46366
+ inner.classList.add(`flipping`);
46367
+ inner.style.transform = `none`;
46368
+ });
46369
+ inner.addEventListener(`transitionend`, () => inner.classList.remove(`flipping`), { once: true });
46370
+ }
46284
46371
  #canZoom(node) {
46285
46372
  if (node.item.isLeaf === true) return false;
46286
- if (node.children !== void 0) return node.children.length > 0;
46373
+ if (node.children !== void 0) return this.hideLeaves ? node.children.some((n) => n.item.isLeaf !== true) : node.children.length > 0;
46287
46374
  return this.model !== void 0 || this.loadChildren !== void 0;
46288
46375
  }
46289
46376
  /**
@@ -46510,6 +46597,39 @@ let TreemapListElement = class TreemapListElement extends i {
46510
46597
  }
46511
46598
  }
46512
46599
  }
46600
+ /**
46601
+ * Cmd+wheel zooms: scrolling backward (deltaY < 0, towards the user) zooms
46602
+ * into the tile under the pointer — the same as clicking it; scrolling
46603
+ * forward (deltaY > 0, away from the user) zooms out one level at a time
46604
+ * (see `#zoomOutOneStep`), not straight to the root. Plain wheel (no Cmd)
46605
+ * is left alone for normal page scroll. A short cooldown after each step
46606
+ * keeps a single trackpad gesture (which fires many small wheel events)
46607
+ * from firing a cascade of zoom steps.
46608
+ */
46609
+ #handleWheel(event) {
46610
+ if (!event.metaKey || event.deltaY === 0) return;
46611
+ event.preventDefault();
46612
+ if (performance.now() < this.#wheelCooldownUntil) return;
46613
+ if (event.deltaY > 0) {
46614
+ if (this._zoomKey === void 0) return;
46615
+ this.#wheelCooldownUntil = performance.now() + 260;
46616
+ this.#zoomOutOneStep();
46617
+ return;
46618
+ }
46619
+ const key = (event.target?.closest(`.tile`))?.getAttribute(`data-key`) ?? void 0;
46620
+ if (key === void 0) return;
46621
+ const rect = this.#rects().find((r) => r.node.item.key === key);
46622
+ if (!rect || !this.#canZoom(rect.node)) return;
46623
+ this.#wheelCooldownUntil = performance.now() + 260;
46624
+ this.zoomTo(rect.node);
46625
+ }
46626
+ /** Zoom to the current zoom node's parent, or fully out when it's a direct child of the root. */
46627
+ #zoomOutOneStep() {
46628
+ const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
46629
+ const parent = path && path.length > 2 ? path[path.length - 2] : void 0;
46630
+ if (parent) this.zoomTo(parent);
46631
+ else this.zoomOut();
46632
+ }
46513
46633
  #renderBreadcrumb() {
46514
46634
  const zoomNode = this.focusedNode;
46515
46635
  if (zoomNode === void 0) return A;
@@ -46539,25 +46659,60 @@ let TreemapListElement = class TreemapListElement extends i {
46539
46659
  if (node.item.key === this.#effectiveRoot?.item.key) this.zoomOut();
46540
46660
  else this.zoomTo(node);
46541
46661
  }
46542
- #hideLabel(rect) {
46662
+ /** On-screen size (px) of `rect`, or `undefined` before the host has been measured. */
46663
+ #pixelSize(rect) {
46543
46664
  const hostWidth = this.clientWidth || 0;
46544
46665
  const hostHeight = this.clientHeight || 0;
46545
- if (hostWidth <= 0 || hostHeight <= 0) return false;
46546
- return rect.width * hostWidth * rect.height * hostHeight < this.minLabelArea;
46666
+ if (hostWidth <= 0 || hostHeight <= 0) return void 0;
46667
+ return {
46668
+ width: rect.width * hostWidth,
46669
+ height: rect.height * hostHeight
46670
+ };
46671
+ }
46672
+ /** Hide when too small by area, or too narrow to fit any readable text even truncated. */
46673
+ #hideLabel(rect) {
46674
+ const size = this.#pixelSize(rect);
46675
+ if (size === void 0) return false;
46676
+ return size.width * size.height < this.minLabelArea || size.width < this.minTileWidth;
46677
+ }
46678
+ /** Whether `rect` is large enough on-screen for its own children to be subdivided and drawn. */
46679
+ #canDescend(rect) {
46680
+ const size = this.#pixelSize(rect);
46681
+ if (size === void 0) return true;
46682
+ return size.width * size.height >= this.minChildArea && size.width >= this.minTileWidth;
46683
+ }
46684
+ /** Slugify a category name into a CSS custom-property-safe token. */
46685
+ #categorySlug(category) {
46686
+ return category.replace(/[^\w-]/g, `-`);
46547
46687
  }
46548
- #renderTile(rect, positionStyle, children, byParent, nested) {
46688
+ /**
46689
+ * Inline colour overrides for `node`'s category, or `undefined` for none.
46690
+ * Set as real `background-color`/`color` (not custom properties) so they
46691
+ * simply win over `.tile`'s CSS-variable-driven defaults by specificity —
46692
+ * no risk of an unresolvable `var()` chain leaving the property invalid.
46693
+ */
46694
+ #categoryStyle(node) {
46695
+ const category = this.categoryAccessor?.(node.item);
46696
+ if (!category) return void 0;
46697
+ const slug = this.#categorySlug(category);
46698
+ return `background-color:var(--treemap-category-${slug}-bg, var(--treemap-tile-bg));color:var(--treemap-category-${slug}-text, var(--treemap-tile-text));`;
46699
+ }
46700
+ #renderTileBox(rect, positionStyle, hasVisibleChildren, childrenContent) {
46549
46701
  const { node } = rect;
46550
46702
  const selected = this._selectedNodes.has(node);
46551
46703
  const tickled = this.tickled.isTickled(node.item.key);
46552
46704
  const filtered = !this.#matchesFilter(node, rect.depth);
46553
46705
  const pending = this.#pendingKeys.has(node.item.key);
46554
46706
  const hideLabel = this.#hideLabel(rect);
46555
- const showHeader = children.length > 0 && !hideLabel;
46707
+ const showHeader = hasVisibleChildren && !hideLabel;
46708
+ const category = this.categoryAccessor?.(node.item);
46709
+ const categoryStyle = this.#categoryStyle(node);
46556
46710
  return b`
46557
46711
  <div
46558
- class="item tile ${nested ? `nested` : ``} ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${filtered ? `filtered` : ``} ${pending ? `loading` : ``} ${rect.width <= 0 || rect.height <= 0 ? `empty` : ``}"
46712
+ class="item tile ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${filtered ? `filtered` : ``} ${pending ? `loading` : ``} ${rect.width <= 0 || rect.height <= 0 ? `empty` : ``}"
46559
46713
  data-key=${node.item.key}
46560
46714
  data-depth=${rect.relativeDepth}
46715
+ data-category=${category ?? A}
46561
46716
  role="treeitem"
46562
46717
  tabindex="-1"
46563
46718
  aria-level=${rect.depth + 1}
@@ -46567,7 +46722,7 @@ let TreemapListElement = class TreemapListElement extends i {
46567
46722
  aria-label=${node.item.label}
46568
46723
  title=${node.item.label}
46569
46724
  part="tile"
46570
- style=${positionStyle}
46725
+ style=${categoryStyle ? `${positionStyle};${categoryStyle}` : positionStyle}
46571
46726
  @click=${(e) => {
46572
46727
  e.stopPropagation();
46573
46728
  this.#handleItemClick(rect, this.#rects().findIndex((r) => r.node.item.key === node.item.key), e);
@@ -46579,23 +46734,46 @@ let TreemapListElement = class TreemapListElement extends i {
46579
46734
  @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
46580
46735
  >
46581
46736
  ${showHeader ? b`<div class="tile-header" part="tile-header"><span class="treemap-label" part="label">${node.item.label}</span></div>` : b`<span class="treemap-label ${hideLabel ? `hide-label` : ``}" part="label">${node.item.label}</span>`}
46582
- ${children.length > 0 ? b`
46583
- <div class="tile-children" role="group">
46584
- ${repeat(children, (r) => r.node.item.key, (r) => this.#renderNestedTile(r, rect, byParent))}
46585
- </div>
46586
- ` : A}
46737
+ ${childrenContent}
46587
46738
  </div>
46588
46739
  `;
46589
46740
  }
46741
+ /**
46742
+ * Render `rect` plus (if it's large enough to descend into — see
46743
+ * `#canDescend`) a `.tile-children` holding its own children, each
46744
+ * positioned relative to `rect` itself and recursing the same way.
46745
+ *
46746
+ * Each level gets its own real `.tile-header` + `.tile-children` box, so a
46747
+ * header reserves its own space via normal box-model layout (flex column:
46748
+ * header, then the remaining space for children) at *every* level — not
46749
+ * just the top one. That matters because layout rects are purely
46750
+ * geometric (no allowance for header height), so a folder whose one child
46751
+ * fills ~100% of its area gets a child rect nearly identical to its own;
46752
+ * positioning that child against its direct parent (not some flattened
46753
+ * shared ancestor) is what keeps the parent's header from being painted
46754
+ * over by an opaque, near-identically-sized child box.
46755
+ */
46756
+ #renderTile(rect, positionStyle, byParent) {
46757
+ const children = rect.hasChildren && this.#canDescend(rect) ? byParent.get(rect.node.item.key) ?? [] : [];
46758
+ const childrenContent = children.length > 0 ? b`
46759
+ <div class="tile-children" role="group">
46760
+ ${repeat(children, (r) => r.node.item.key, (r) => this.#renderNestedTile(r, rect, byParent))}
46761
+ </div>
46762
+ ` : A;
46763
+ return this.#renderTileBox(rect, positionStyle, children.length > 0, childrenContent);
46764
+ }
46765
+ /** A child tile, positioned as a percentage of `parent`'s own box. */
46590
46766
  #renderNestedTile(rect, parent, byParent) {
46591
46767
  const relX = parent.width > 0 ? (rect.x - parent.x) / parent.width : 0;
46592
46768
  const relY = parent.height > 0 ? (rect.y - parent.y) / parent.height : 0;
46593
46769
  const relW = parent.width > 0 ? rect.width / parent.width : 0;
46594
46770
  const relH = parent.height > 0 ? rect.height / parent.height : 0;
46595
- return this.#renderTile(rect, `left:${(relX * 100).toFixed(4)}%;top:${(relY * 100).toFixed(4)}%;width:${(relW * 100).toFixed(4)}%;height:${(relH * 100).toFixed(4)}%`, byParent.get(rect.node.item.key) ?? [], byParent, true);
46771
+ const style = `left:${(relX * 100).toFixed(4)}%;top:${(relY * 100).toFixed(4)}%;width:${(relW * 100).toFixed(4)}%;height:${(relH * 100).toFixed(4)}%`;
46772
+ return this.#renderTile(rect, style, byParent);
46596
46773
  }
46597
46774
  #renderTopTile(rect, byParent) {
46598
- return this.#renderTile(rect, `left:${(rect.x * 100).toFixed(4)}%;top:${(rect.y * 100).toFixed(4)}%;width:${(rect.width * 100).toFixed(4)}%;height:${(rect.height * 100).toFixed(4)}%`, byParent.get(rect.node.item.key) ?? [], byParent, false);
46775
+ const style = `left:${(rect.x * 100).toFixed(4)}%;top:${(rect.y * 100).toFixed(4)}%;width:${(rect.width * 100).toFixed(4)}%;height:${(rect.height * 100).toFixed(4)}%`;
46776
+ return this.#renderTile(rect, style, byParent);
46599
46777
  }
46600
46778
  render() {
46601
46779
  const rects = this.#rects();
@@ -46617,11 +46795,14 @@ let TreemapListElement = class TreemapListElement extends i {
46617
46795
  style=${`--treemap-padding:${this.padding}px`}
46618
46796
  @keydown=${(e) => this.#handleKeyDown(e)}
46619
46797
  @pointerleave=${() => this.tickled.clearTickled()}
46798
+ @wheel=${(e) => this.#handleWheel(e)}
46620
46799
  >
46621
46800
  ${this.#renderBreadcrumb()}
46622
46801
  ${rects.length === 0 ? b`<div class="empty-state">No data</div>` : b`
46623
46802
  <div class="viewport" part="viewport">
46624
- ${repeat(topLevel, (r) => r.node.item.key, (r) => this.#renderTopTile(r, byParent))}
46803
+ <div class="viewport-inner">
46804
+ ${repeat(topLevel, (r) => r.node.item.key, (r) => this.#renderTopTile(r, byParent))}
46805
+ </div>
46625
46806
  </div>
46626
46807
  `}
46627
46808
  </div>
@@ -46644,11 +46825,18 @@ let TreemapListElement = class TreemapListElement extends i {
46644
46825
  --treemap-tile-border: var(--border-subtle);
46645
46826
  --treemap-header-bg: rgb(0 0 0 / 0.12);
46646
46827
  --treemap-font-size: var(--text-s);
46828
+ --treemap-zoom-duration: 220ms;
46647
46829
 
46648
46830
  display: block;
46649
- overflow: auto;
46831
+ overflow: hidden;
46650
46832
  }
46651
46833
 
46834
+ /* Per-category tile colours (set categoryAccessor to opt in): define
46835
+ --treemap-category-SLUG-bg / -text at whatever scope suits (:host
46836
+ here, or globally) for each category your accessor can return —
46837
+ e.g. --treemap-category-urgent-bg: oklch(55% 0.18 25). A category
46838
+ with no matching pair falls back to --treemap-tile-bg/-text. */
46839
+
46652
46840
  .treemap {
46653
46841
  display: flex;
46654
46842
  flex-direction: column;
@@ -46677,18 +46865,44 @@ let TreemapListElement = class TreemapListElement extends i {
46677
46865
  .viewport {
46678
46866
  position: relative;
46679
46867
  width: 100%;
46680
- height: var(--treemap-height);
46681
- flex-shrink: 0;
46868
+ /* flex-basis of --treemap-height (its size when nothing forces a
46869
+ different one — the common case of an auto-height host) but
46870
+ allowed to grow/shrink like any flex item, so it shares the
46871
+ .treemap column's height with the breadcrumb instead of a plain
46872
+ fixed height always claiming that full amount regardless of what
46873
+ else needs space — which is what let the breadcrumb push the
46874
+ host past its own height and scroll once a zoom made it appear. */
46875
+ flex: 1 1 var(--treemap-height);
46876
+ min-height: 0;
46682
46877
  overflow: hidden;
46683
46878
  border-radius: var(--radius-m);
46684
46879
  }
46685
46880
 
46881
+ /* Transform target for the zoom-in/zoom-out FLIP animation (see
46882
+ #planFlip/#applyFlip) — kept separate from .viewport so the
46883
+ transform never fights that element's own sizing/overflow. */
46884
+ .viewport-inner {
46885
+ position: absolute;
46886
+ inset: 0;
46887
+ transform-origin: 0 0;
46888
+ }
46889
+
46890
+ .viewport-inner.flipping {
46891
+ transition: transform var(--treemap-zoom-duration, 220ms) ease-out;
46892
+ }
46893
+
46686
46894
  .tile {
46687
46895
  position: absolute;
46688
46896
  box-sizing: border-box;
46689
46897
  overflow: hidden;
46690
46898
  display: flex;
46691
46899
  flex-direction: column;
46900
+ /* Override the shared .item class's align-items:center (built for
46901
+ icon+label rows) — tiles need their flex children to stretch to
46902
+ full width, or .tile-children collapses to its (empty) content
46903
+ width and every nested tile collapses with it. */
46904
+ align-items: stretch;
46905
+ white-space: normal;
46692
46906
  background: var(--treemap-tile-bg);
46693
46907
  color: var(--treemap-tile-text);
46694
46908
  border: var(--treemap-tile-border);
@@ -46781,12 +46995,25 @@ __decorate([property({
46781
46995
  type: Number,
46782
46996
  attribute: `max-depth`
46783
46997
  })], TreemapListElement.prototype, "maxDepth", void 0);
46998
+ __decorate([property({
46999
+ type: Boolean,
47000
+ attribute: `hide-leaves`
47001
+ })], TreemapListElement.prototype, "hideLeaves", void 0);
46784
47002
  __decorate([property({
46785
47003
  type: Number,
46786
47004
  attribute: `min-label-area`
46787
47005
  })], TreemapListElement.prototype, "minLabelArea", void 0);
47006
+ __decorate([property({
47007
+ type: Number,
47008
+ attribute: `min-tile-width`
47009
+ })], TreemapListElement.prototype, "minTileWidth", void 0);
46788
47010
  __decorate([property({ type: Number })], TreemapListElement.prototype, "padding", void 0);
47011
+ __decorate([property({
47012
+ type: Number,
47013
+ attribute: `min-child-area`
47014
+ })], TreemapListElement.prototype, "minChildArea", void 0);
46789
47015
  __decorate([property({ attribute: false })], TreemapListElement.prototype, "valueAccessor", void 0);
47016
+ __decorate([property({ attribute: false })], TreemapListElement.prototype, "categoryAccessor", void 0);
46790
47017
  __decorate([property({ attribute: false })], TreemapListElement.prototype, "selectedNode", void 0);
46791
47018
  __decorate([state()], TreemapListElement.prototype, "_selectedNodes", void 0);
46792
47019
  __decorate([property({ attribute: false })], TreemapListElement.prototype, "filterPredicate", void 0);