@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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { i as ICON_CLOSE, n as ICON_CHECK, r as ICON_CHEVRON_DOWN, t as ICON_CAR
16
16
  import "./icons.js";
17
17
  import { t as prominenceStyles } from "./prominence-DlFShic3.js";
18
18
  import { n as buttonStyles, t as ButtonElement } from "./button-DsFbV4XD.js";
19
- import { n as captionForEntry, r as createDefaultListDragPreview, t as ListNativeDragController } from "./list-native-drag-controller-D97TYjn1.js";
19
+ import { n as captionForEntry, r as createDefaultListDragPreview, t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
20
20
  import { n as isMultiMode, r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-SA4piSBi.js";
21
21
  import { a as prefersReducedMotion, i as listItemFadeKeyframes, n as animateListItemIn, o as startListItemOutAnimation, r as animateListItemOut, t as LIST_ITEM_MOTION_MS } from "./list-item-animation-BUoEKi5s.js";
22
22
  import "./interaction-BskmlA9t.js";
@@ -25,7 +25,7 @@ import { Checkbox } from "./checkbox.js";
25
25
  import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-R73i0QTP.js";
26
26
  import { LabelledRangeInput } from "./labelled-range-input.js";
27
27
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
28
- 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-C25xYUa6.js";
28
+ 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-BuzWn18_.js";
29
29
  import { t as scrollFadeStyles } from "./scroll-fade-CAs1EabA.js";
30
30
  import { NarrowedTextElement } from "./narrowed-text.js";
31
31
  import { CrumbNavigationElement } from "./crumbs.js";
@@ -40,7 +40,7 @@ import { TabListElement } from "./tabs.js";
40
40
  import { EditableLabel } from "./editable-label.js";
41
41
  import { n as EditableNumber, t as LabelledSliderInput } from "./labelled-slider-input-CF2RtzgG.js";
42
42
  import { t as FormElement } from "./form-CsXJX15p.js";
43
- import { t as GridListElement } from "./grid-list-DSnYOGh7.js";
43
+ import { t as GridListElement } from "./grid-list-DLfw5HI4.js";
44
44
  import { PanelElement } from "./panel.js";
45
45
  import { GroupedItemListerElement } from "./grouped-item-lister.js";
46
46
  import { VerticalListElement } from "./vertical-list.js";
@@ -9731,12 +9731,16 @@ function subdivide(box, weights, method, relativeDepth) {
9731
9731
  */
9732
9732
  function computeTreemapLayout(root, options = {}) {
9733
9733
  if (!root?.children?.length) return [];
9734
- const { maxDepth = Number.POSITIVE_INFINITY, valueAccessor = () => 1, zoomKey, tilingMethod = `squarify` } = options;
9734
+ const { maxDepth = Number.POSITIVE_INFINITY, valueAccessor = () => 1, zoomKey, tilingMethod = `squarify`, hideLeaves = false } = options;
9735
9735
  if (maxDepth <= 0) return [];
9736
+ const visibleChildren = (node) => {
9737
+ const children = node.children ?? [];
9738
+ return hideLeaves ? children.filter((n) => n.item.isLeaf !== true) : children;
9739
+ };
9736
9740
  const zoomNode = zoomKey === void 0 ? root : findTreemapNode(root, zoomKey) ?? root;
9737
9741
  const zoomPath = findTreemapPath(root, zoomNode.item.key);
9738
9742
  const zoomDepth = zoomPath ? zoomPath.length - 2 : -1;
9739
- const siblings = zoomNode.children ?? [];
9743
+ const siblings = visibleChildren(zoomNode);
9740
9744
  if (siblings.length === 0) return [];
9741
9745
  const rects = [];
9742
9746
  const layoutLevel = (nodes, box, depth, relativeDepth, parentKey) => {
@@ -9745,7 +9749,7 @@ function computeTreemapLayout(root, options = {}) {
9745
9749
  const setSize = nodes.length;
9746
9750
  nodes.forEach((node, index) => {
9747
9751
  const childBox = boxes[index];
9748
- const children = node.children ?? [];
9752
+ const children = visibleChildren(node);
9749
9753
  rects.push({
9750
9754
  node,
9751
9755
  depth,
@@ -9780,8 +9784,11 @@ let TreemapListElement = class TreemapListElement extends LitElement {
9780
9784
  this.interactionMode = `standard`;
9781
9785
  this.tilingMethod = `squarify`;
9782
9786
  this.maxDepth = 4;
9787
+ this.hideLeaves = false;
9783
9788
  this.minLabelArea = 1600;
9789
+ this.minTileWidth = 24;
9784
9790
  this.padding = 4;
9791
+ this.minChildArea = 400;
9785
9792
  this.valueAccessor = () => 1;
9786
9793
  this._selectedNodes = /* @__PURE__ */ new Set();
9787
9794
  this.disableKeyboardNavigation = false;
@@ -9815,6 +9822,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
9815
9822
  });
9816
9823
  this.#pendingKeys = /* @__PURE__ */ new Set();
9817
9824
  this.#aborters = /* @__PURE__ */ new Map();
9825
+ this.#wheelCooldownUntil = 0;
9818
9826
  }
9819
9827
  get selectedNodes() {
9820
9828
  return this._selectedNodes;
@@ -9831,15 +9839,22 @@ let TreemapListElement = class TreemapListElement extends LitElement {
9831
9839
  #ownedRegistry;
9832
9840
  #unregisterSearchCommands;
9833
9841
  #resizeObserver;
9842
+ #pendingFlip;
9843
+ /** `performance.now()` timestamp before which further Cmd+wheel zoom steps are ignored. */
9844
+ #wheelCooldownUntil;
9834
9845
  get #effectiveRoot() {
9835
9846
  return this.model?.root ?? this.root;
9836
9847
  }
9837
9848
  #rects() {
9849
+ return this.#rectsForZoom(this._zoomKey);
9850
+ }
9851
+ #rectsForZoom(zoomKey) {
9838
9852
  return computeTreemapLayout(this.#effectiveRoot, {
9839
9853
  valueAccessor: this.valueAccessor,
9840
9854
  maxDepth: this.maxDepth,
9841
- zoomKey: this._zoomKey,
9842
- tilingMethod: this.tilingMethod
9855
+ zoomKey,
9856
+ tilingMethod: this.tilingMethod,
9857
+ hideLeaves: this.hideLeaves
9843
9858
  });
9844
9859
  }
9845
9860
  connectedCallback() {
@@ -9915,8 +9930,14 @@ let TreemapListElement = class TreemapListElement extends LitElement {
9915
9930
  if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
9916
9931
  if (this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
9917
9932
  this.#ensureVisibleChildren();
9933
+ if (this.#pendingFlip) {
9934
+ const flip = this.#pendingFlip;
9935
+ this.#pendingFlip = void 0;
9936
+ this.#applyFlip(flip);
9937
+ }
9918
9938
  }
9919
9939
  #resetViewState() {
9940
+ this.#pendingFlip = void 0;
9920
9941
  this.#clearSelectionInternal();
9921
9942
  this.tickled.clearTickled();
9922
9943
  this._zoomKey = void 0;
@@ -10029,6 +10050,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10029
10050
  */
10030
10051
  zoomTo(node) {
10031
10052
  if (this._zoomKey === node.item.key) return;
10053
+ this.#pendingFlip = this.#planFlip(node.item.key);
10032
10054
  this._zoomKey = node.item.key;
10033
10055
  this.tickled.clearTickled();
10034
10056
  const depth = findTreemapPath(this.#effectiveRoot, node.item.key)?.length;
@@ -10057,6 +10079,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10057
10079
  */
10058
10080
  zoomOut() {
10059
10081
  if (this._zoomKey === void 0) return;
10082
+ this.#pendingFlip = this.#planFlip(void 0);
10060
10083
  const previous = this.focusedNode;
10061
10084
  const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
10062
10085
  const absoluteDepth = path === void 0 ? -1 : path.length - 2;
@@ -10080,9 +10103,66 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10080
10103
  }));
10081
10104
  this.requestUpdate();
10082
10105
  }
10106
+ /**
10107
+ * Work out (before `_zoomKey` changes) whether the upcoming zoom should
10108
+ * animate, and how — see `FlipPlan`. Returns `undefined` to skip the
10109
+ * animation: reduced-motion is requested, or the node needed to anchor it
10110
+ * isn't in either the old or new layout's visible window.
10111
+ */
10112
+ #planFlip(targetKey) {
10113
+ if (typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches) return void 0;
10114
+ if (targetKey !== void 0) {
10115
+ const visible = this.#rects().find((r) => r.node.item.key === targetKey);
10116
+ if (visible) return {
10117
+ direction: `in`,
10118
+ rect: {
10119
+ x: visible.x,
10120
+ y: visible.y,
10121
+ width: visible.width,
10122
+ height: visible.height
10123
+ }
10124
+ };
10125
+ }
10126
+ const fromKey = this._zoomKey;
10127
+ if (fromKey === void 0) return void 0;
10128
+ const landing = this.#rectsForZoom(targetKey).find((r) => r.node.item.key === fromKey);
10129
+ if (!landing) return void 0;
10130
+ return {
10131
+ direction: `out`,
10132
+ rect: {
10133
+ x: landing.x,
10134
+ y: landing.y,
10135
+ width: landing.width,
10136
+ height: landing.height
10137
+ }
10138
+ };
10139
+ }
10140
+ /** Apply a planned FLIP animation once the new layout has rendered — see `FlipPlan`. */
10141
+ #applyFlip(flip) {
10142
+ const inner = this.renderRoot.querySelector(`.viewport-inner`);
10143
+ const viewport = this.renderRoot.querySelector(`.viewport`);
10144
+ if (!(inner instanceof HTMLElement) || !(viewport instanceof HTMLElement)) return;
10145
+ const vw = viewport.clientWidth;
10146
+ const vh = viewport.clientHeight;
10147
+ if (vw <= 0 || vh <= 0) return;
10148
+ const { rect, direction } = flip;
10149
+ 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)})` : (() => {
10150
+ const sx = rect.width > 0 ? 1 / rect.width : 1;
10151
+ const sy = rect.height > 0 ? 1 / rect.height : 1;
10152
+ return `translate(${-rect.x * sx * vw}px, ${-rect.y * sy * vh}px) scale(${sx}, ${sy})`;
10153
+ })();
10154
+ inner.classList.remove(`flipping`);
10155
+ inner.style.transform = startTransform;
10156
+ inner.offsetWidth;
10157
+ requestAnimationFrame(() => {
10158
+ inner.classList.add(`flipping`);
10159
+ inner.style.transform = `none`;
10160
+ });
10161
+ inner.addEventListener(`transitionend`, () => inner.classList.remove(`flipping`), { once: true });
10162
+ }
10083
10163
  #canZoom(node) {
10084
10164
  if (node.item.isLeaf === true) return false;
10085
- if (node.children !== void 0) return node.children.length > 0;
10165
+ if (node.children !== void 0) return this.hideLeaves ? node.children.some((n) => n.item.isLeaf !== true) : node.children.length > 0;
10086
10166
  return this.model !== void 0 || this.loadChildren !== void 0;
10087
10167
  }
10088
10168
  /**
@@ -10309,6 +10389,39 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10309
10389
  }
10310
10390
  }
10311
10391
  }
10392
+ /**
10393
+ * Cmd+wheel zooms: scrolling backward (deltaY < 0, towards the user) zooms
10394
+ * into the tile under the pointer — the same as clicking it; scrolling
10395
+ * forward (deltaY > 0, away from the user) zooms out one level at a time
10396
+ * (see `#zoomOutOneStep`), not straight to the root. Plain wheel (no Cmd)
10397
+ * is left alone for normal page scroll. A short cooldown after each step
10398
+ * keeps a single trackpad gesture (which fires many small wheel events)
10399
+ * from firing a cascade of zoom steps.
10400
+ */
10401
+ #handleWheel(event) {
10402
+ if (!event.metaKey || event.deltaY === 0) return;
10403
+ event.preventDefault();
10404
+ if (performance.now() < this.#wheelCooldownUntil) return;
10405
+ if (event.deltaY > 0) {
10406
+ if (this._zoomKey === void 0) return;
10407
+ this.#wheelCooldownUntil = performance.now() + 260;
10408
+ this.#zoomOutOneStep();
10409
+ return;
10410
+ }
10411
+ const key = (event.target?.closest(`.tile`))?.getAttribute(`data-key`) ?? void 0;
10412
+ if (key === void 0) return;
10413
+ const rect = this.#rects().find((r) => r.node.item.key === key);
10414
+ if (!rect || !this.#canZoom(rect.node)) return;
10415
+ this.#wheelCooldownUntil = performance.now() + 260;
10416
+ this.zoomTo(rect.node);
10417
+ }
10418
+ /** Zoom to the current zoom node's parent, or fully out when it's a direct child of the root. */
10419
+ #zoomOutOneStep() {
10420
+ const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
10421
+ const parent = path && path.length > 2 ? path[path.length - 2] : void 0;
10422
+ if (parent) this.zoomTo(parent);
10423
+ else this.zoomOut();
10424
+ }
10312
10425
  #renderBreadcrumb() {
10313
10426
  const zoomNode = this.focusedNode;
10314
10427
  if (zoomNode === void 0) return nothing;
@@ -10338,25 +10451,60 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10338
10451
  if (node.item.key === this.#effectiveRoot?.item.key) this.zoomOut();
10339
10452
  else this.zoomTo(node);
10340
10453
  }
10341
- #hideLabel(rect) {
10454
+ /** On-screen size (px) of `rect`, or `undefined` before the host has been measured. */
10455
+ #pixelSize(rect) {
10342
10456
  const hostWidth = this.clientWidth || 0;
10343
10457
  const hostHeight = this.clientHeight || 0;
10344
- if (hostWidth <= 0 || hostHeight <= 0) return false;
10345
- return rect.width * hostWidth * rect.height * hostHeight < this.minLabelArea;
10458
+ if (hostWidth <= 0 || hostHeight <= 0) return void 0;
10459
+ return {
10460
+ width: rect.width * hostWidth,
10461
+ height: rect.height * hostHeight
10462
+ };
10346
10463
  }
10347
- #renderTile(rect, positionStyle, children, byParent, nested) {
10464
+ /** Hide when too small by area, or too narrow to fit any readable text even truncated. */
10465
+ #hideLabel(rect) {
10466
+ const size = this.#pixelSize(rect);
10467
+ if (size === void 0) return false;
10468
+ return size.width * size.height < this.minLabelArea || size.width < this.minTileWidth;
10469
+ }
10470
+ /** Whether `rect` is large enough on-screen for its own children to be subdivided and drawn. */
10471
+ #canDescend(rect) {
10472
+ const size = this.#pixelSize(rect);
10473
+ if (size === void 0) return true;
10474
+ return size.width * size.height >= this.minChildArea && size.width >= this.minTileWidth;
10475
+ }
10476
+ /** Slugify a category name into a CSS custom-property-safe token. */
10477
+ #categorySlug(category) {
10478
+ return category.replace(/[^\w-]/g, `-`);
10479
+ }
10480
+ /**
10481
+ * Inline colour overrides for `node`'s category, or `undefined` for none.
10482
+ * Set as real `background-color`/`color` (not custom properties) so they
10483
+ * simply win over `.tile`'s CSS-variable-driven defaults by specificity —
10484
+ * no risk of an unresolvable `var()` chain leaving the property invalid.
10485
+ */
10486
+ #categoryStyle(node) {
10487
+ const category = this.categoryAccessor?.(node.item);
10488
+ if (!category) return void 0;
10489
+ const slug = this.#categorySlug(category);
10490
+ return `background-color:var(--treemap-category-${slug}-bg, var(--treemap-tile-bg));color:var(--treemap-category-${slug}-text, var(--treemap-tile-text));`;
10491
+ }
10492
+ #renderTileBox(rect, positionStyle, hasVisibleChildren, childrenContent) {
10348
10493
  const { node } = rect;
10349
10494
  const selected = this._selectedNodes.has(node);
10350
10495
  const tickled = this.tickled.isTickled(node.item.key);
10351
10496
  const filtered = !this.#matchesFilter(node, rect.depth);
10352
10497
  const pending = this.#pendingKeys.has(node.item.key);
10353
10498
  const hideLabel = this.#hideLabel(rect);
10354
- const showHeader = children.length > 0 && !hideLabel;
10499
+ const showHeader = hasVisibleChildren && !hideLabel;
10500
+ const category = this.categoryAccessor?.(node.item);
10501
+ const categoryStyle = this.#categoryStyle(node);
10355
10502
  return html`
10356
10503
  <div
10357
- class="item tile ${nested ? `nested` : ``} ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${filtered ? `filtered` : ``} ${pending ? `loading` : ``} ${rect.width <= 0 || rect.height <= 0 ? `empty` : ``}"
10504
+ class="item tile ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${filtered ? `filtered` : ``} ${pending ? `loading` : ``} ${rect.width <= 0 || rect.height <= 0 ? `empty` : ``}"
10358
10505
  data-key=${node.item.key}
10359
10506
  data-depth=${rect.relativeDepth}
10507
+ data-category=${category ?? nothing}
10360
10508
  role="treeitem"
10361
10509
  tabindex="-1"
10362
10510
  aria-level=${rect.depth + 1}
@@ -10366,7 +10514,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10366
10514
  aria-label=${node.item.label}
10367
10515
  title=${node.item.label}
10368
10516
  part="tile"
10369
- style=${positionStyle}
10517
+ style=${categoryStyle ? `${positionStyle};${categoryStyle}` : positionStyle}
10370
10518
  @click=${(e) => {
10371
10519
  e.stopPropagation();
10372
10520
  this.#handleItemClick(rect, this.#rects().findIndex((r) => r.node.item.key === node.item.key), e);
@@ -10378,23 +10526,46 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10378
10526
  @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
10379
10527
  >
10380
10528
  ${showHeader ? html`<div class="tile-header" part="tile-header"><span class="treemap-label" part="label">${node.item.label}</span></div>` : html`<span class="treemap-label ${hideLabel ? `hide-label` : ``}" part="label">${node.item.label}</span>`}
10381
- ${children.length > 0 ? html`
10382
- <div class="tile-children" role="group">
10383
- ${repeat(children, (r) => r.node.item.key, (r) => this.#renderNestedTile(r, rect, byParent))}
10384
- </div>
10385
- ` : nothing}
10529
+ ${childrenContent}
10386
10530
  </div>
10387
10531
  `;
10388
10532
  }
10533
+ /**
10534
+ * Render `rect` plus (if it's large enough to descend into — see
10535
+ * `#canDescend`) a `.tile-children` holding its own children, each
10536
+ * positioned relative to `rect` itself and recursing the same way.
10537
+ *
10538
+ * Each level gets its own real `.tile-header` + `.tile-children` box, so a
10539
+ * header reserves its own space via normal box-model layout (flex column:
10540
+ * header, then the remaining space for children) at *every* level — not
10541
+ * just the top one. That matters because layout rects are purely
10542
+ * geometric (no allowance for header height), so a folder whose one child
10543
+ * fills ~100% of its area gets a child rect nearly identical to its own;
10544
+ * positioning that child against its direct parent (not some flattened
10545
+ * shared ancestor) is what keeps the parent's header from being painted
10546
+ * over by an opaque, near-identically-sized child box.
10547
+ */
10548
+ #renderTile(rect, positionStyle, byParent) {
10549
+ const children = rect.hasChildren && this.#canDescend(rect) ? byParent.get(rect.node.item.key) ?? [] : [];
10550
+ const childrenContent = children.length > 0 ? html`
10551
+ <div class="tile-children" role="group">
10552
+ ${repeat(children, (r) => r.node.item.key, (r) => this.#renderNestedTile(r, rect, byParent))}
10553
+ </div>
10554
+ ` : nothing;
10555
+ return this.#renderTileBox(rect, positionStyle, children.length > 0, childrenContent);
10556
+ }
10557
+ /** A child tile, positioned as a percentage of `parent`'s own box. */
10389
10558
  #renderNestedTile(rect, parent, byParent) {
10390
10559
  const relX = parent.width > 0 ? (rect.x - parent.x) / parent.width : 0;
10391
10560
  const relY = parent.height > 0 ? (rect.y - parent.y) / parent.height : 0;
10392
10561
  const relW = parent.width > 0 ? rect.width / parent.width : 0;
10393
10562
  const relH = parent.height > 0 ? rect.height / parent.height : 0;
10394
- 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);
10563
+ const style = `left:${(relX * 100).toFixed(4)}%;top:${(relY * 100).toFixed(4)}%;width:${(relW * 100).toFixed(4)}%;height:${(relH * 100).toFixed(4)}%`;
10564
+ return this.#renderTile(rect, style, byParent);
10395
10565
  }
10396
10566
  #renderTopTile(rect, byParent) {
10397
- 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);
10567
+ 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)}%`;
10568
+ return this.#renderTile(rect, style, byParent);
10398
10569
  }
10399
10570
  render() {
10400
10571
  const rects = this.#rects();
@@ -10416,11 +10587,14 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10416
10587
  style=${`--treemap-padding:${this.padding}px`}
10417
10588
  @keydown=${(e) => this.#handleKeyDown(e)}
10418
10589
  @pointerleave=${() => this.tickled.clearTickled()}
10590
+ @wheel=${(e) => this.#handleWheel(e)}
10419
10591
  >
10420
10592
  ${this.#renderBreadcrumb()}
10421
10593
  ${rects.length === 0 ? html`<div class="empty-state">No data</div>` : html`
10422
10594
  <div class="viewport" part="viewport">
10423
- ${repeat(topLevel, (r) => r.node.item.key, (r) => this.#renderTopTile(r, byParent))}
10595
+ <div class="viewport-inner">
10596
+ ${repeat(topLevel, (r) => r.node.item.key, (r) => this.#renderTopTile(r, byParent))}
10597
+ </div>
10424
10598
  </div>
10425
10599
  `}
10426
10600
  </div>
@@ -10443,11 +10617,18 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10443
10617
  --treemap-tile-border: var(--border-subtle);
10444
10618
  --treemap-header-bg: rgb(0 0 0 / 0.12);
10445
10619
  --treemap-font-size: var(--text-s);
10620
+ --treemap-zoom-duration: 220ms;
10446
10621
 
10447
10622
  display: block;
10448
- overflow: auto;
10623
+ overflow: hidden;
10449
10624
  }
10450
10625
 
10626
+ /* Per-category tile colours (set categoryAccessor to opt in): define
10627
+ --treemap-category-SLUG-bg / -text at whatever scope suits (:host
10628
+ here, or globally) for each category your accessor can return —
10629
+ e.g. --treemap-category-urgent-bg: oklch(55% 0.18 25). A category
10630
+ with no matching pair falls back to --treemap-tile-bg/-text. */
10631
+
10451
10632
  .treemap {
10452
10633
  display: flex;
10453
10634
  flex-direction: column;
@@ -10476,18 +10657,44 @@ let TreemapListElement = class TreemapListElement extends LitElement {
10476
10657
  .viewport {
10477
10658
  position: relative;
10478
10659
  width: 100%;
10479
- height: var(--treemap-height);
10480
- flex-shrink: 0;
10660
+ /* flex-basis of --treemap-height (its size when nothing forces a
10661
+ different one — the common case of an auto-height host) but
10662
+ allowed to grow/shrink like any flex item, so it shares the
10663
+ .treemap column's height with the breadcrumb instead of a plain
10664
+ fixed height always claiming that full amount regardless of what
10665
+ else needs space — which is what let the breadcrumb push the
10666
+ host past its own height and scroll once a zoom made it appear. */
10667
+ flex: 1 1 var(--treemap-height);
10668
+ min-height: 0;
10481
10669
  overflow: hidden;
10482
10670
  border-radius: var(--radius-m);
10483
10671
  }
10484
10672
 
10673
+ /* Transform target for the zoom-in/zoom-out FLIP animation (see
10674
+ #planFlip/#applyFlip) — kept separate from .viewport so the
10675
+ transform never fights that element's own sizing/overflow. */
10676
+ .viewport-inner {
10677
+ position: absolute;
10678
+ inset: 0;
10679
+ transform-origin: 0 0;
10680
+ }
10681
+
10682
+ .viewport-inner.flipping {
10683
+ transition: transform var(--treemap-zoom-duration, 220ms) ease-out;
10684
+ }
10685
+
10485
10686
  .tile {
10486
10687
  position: absolute;
10487
10688
  box-sizing: border-box;
10488
10689
  overflow: hidden;
10489
10690
  display: flex;
10490
10691
  flex-direction: column;
10692
+ /* Override the shared .item class's align-items:center (built for
10693
+ icon+label rows) — tiles need their flex children to stretch to
10694
+ full width, or .tile-children collapses to its (empty) content
10695
+ width and every nested tile collapses with it. */
10696
+ align-items: stretch;
10697
+ white-space: normal;
10491
10698
  background: var(--treemap-tile-bg);
10492
10699
  color: var(--treemap-tile-text);
10493
10700
  border: var(--treemap-tile-border);
@@ -10580,12 +10787,25 @@ __decorate([property({
10580
10787
  type: Number,
10581
10788
  attribute: `max-depth`
10582
10789
  })], TreemapListElement.prototype, "maxDepth", void 0);
10790
+ __decorate([property({
10791
+ type: Boolean,
10792
+ attribute: `hide-leaves`
10793
+ })], TreemapListElement.prototype, "hideLeaves", void 0);
10583
10794
  __decorate([property({
10584
10795
  type: Number,
10585
10796
  attribute: `min-label-area`
10586
10797
  })], TreemapListElement.prototype, "minLabelArea", void 0);
10798
+ __decorate([property({
10799
+ type: Number,
10800
+ attribute: `min-tile-width`
10801
+ })], TreemapListElement.prototype, "minTileWidth", void 0);
10587
10802
  __decorate([property({ type: Number })], TreemapListElement.prototype, "padding", void 0);
10803
+ __decorate([property({
10804
+ type: Number,
10805
+ attribute: `min-child-area`
10806
+ })], TreemapListElement.prototype, "minChildArea", void 0);
10588
10807
  __decorate([property({ attribute: false })], TreemapListElement.prototype, "valueAccessor", void 0);
10808
+ __decorate([property({ attribute: false })], TreemapListElement.prototype, "categoryAccessor", void 0);
10589
10809
  __decorate([property({ attribute: false })], TreemapListElement.prototype, "selectedNode", void 0);
10590
10810
  __decorate([state()], TreemapListElement.prototype, "_selectedNodes", void 0);
10591
10811
  __decorate([property({ attribute: false })], TreemapListElement.prototype, "filterPredicate", void 0);