@ixfx/components 0.11.4 → 0.11.6

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 (113) hide show
  1. package/bundle/index.d.ts +451 -120
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2267 -540
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  7. package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  8. package/dist/ac-token.d.ts +1 -1
  9. package/dist/{button-CTL45aBl.d.ts → button-BYuf72aR.d.ts} +2 -2
  10. package/dist/{button-CTL45aBl.d.ts.map → button-BYuf72aR.d.ts.map} +1 -1
  11. package/dist/button.d.ts +1 -1
  12. package/dist/colour-picker.d.ts +1 -1
  13. package/dist/crumbs.d.ts +3 -3
  14. package/dist/crumbs.js +1 -1
  15. package/dist/{data-grid-CfiaBqTA.d.ts → data-grid-C6EjCe5O.d.ts} +48 -6
  16. package/dist/data-grid-C6EjCe5O.d.ts.map +1 -0
  17. package/dist/data-grid.d.ts +3 -3
  18. package/dist/data-grid.js +67 -3
  19. package/dist/data-grid.js.map +1 -1
  20. package/dist/{detail-list-DxQZkK-C.d.ts → detail-list-CmdCxddo.d.ts} +22 -5
  21. package/dist/detail-list-CmdCxddo.d.ts.map +1 -0
  22. package/dist/detail-list.d.ts +3 -3
  23. package/dist/detail-list.js +67 -4
  24. package/dist/detail-list.js.map +1 -1
  25. package/dist/{grid-list-k89JpiNy.js → grid-list-3fGhdaFA.js} +92 -7
  26. package/dist/grid-list-3fGhdaFA.js.map +1 -0
  27. package/dist/{grid-list-ChgYpUUq.d.ts → grid-list-D6AfWfwn.d.ts} +27 -4
  28. package/dist/grid-list-D6AfWfwn.d.ts.map +1 -0
  29. package/dist/grid-list.d.ts +2 -2
  30. package/dist/grid-list.js +1 -1
  31. package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +2 -2
  32. package/dist/{grouped-item-lister-Bhyo-dkH.d.ts.map → grouped-item-lister-DETte0Dh.d.ts.map} +1 -1
  33. package/dist/grouped-item-lister.d.ts +1 -1
  34. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
  35. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  36. package/dist/icons.d.ts +2 -2
  37. package/dist/incr-search.d.ts +1 -1
  38. package/dist/{index-CHbuPyLL.d.ts → index-CMsmZ6Or.d.ts} +4 -4
  39. package/dist/{index-CHbuPyLL.d.ts.map → index-CMsmZ6Or.d.ts.map} +1 -1
  40. package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +2 -2
  41. package/dist/{index-garxxqgV.d.ts.map → index-CYndvTWH.d.ts.map} +1 -1
  42. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
  43. package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
  44. package/dist/{index-CDllxjVG.d.ts → index-DyTSt9OK.d.ts} +4 -4
  45. package/dist/{index-CDllxjVG.d.ts.map → index-DyTSt9OK.d.ts.map} +1 -1
  46. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
  47. package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
  48. package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
  49. package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
  50. package/dist/index.d.ts +337 -140
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2038 -646
  53. package/dist/index.js.map +1 -1
  54. package/dist/item-appearance-BjcMyauw.js +31 -0
  55. package/dist/item-appearance-BjcMyauw.js.map +1 -0
  56. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
  57. package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
  58. package/dist/labelled-radial-input.d.ts +2 -2
  59. package/dist/labelled-radial-input.d.ts.map +1 -1
  60. package/dist/labelled-range-input.d.ts +1 -1
  61. package/dist/labelled-slider-input.d.ts +1 -1
  62. package/dist/{list-native-drag-controller-B9reY4uM.d.ts → list-native-drag-controller-CllsHLxw.d.ts} +2 -2
  63. package/dist/{list-native-drag-controller-B9reY4uM.d.ts.map → list-native-drag-controller-CllsHLxw.d.ts.map} +1 -1
  64. package/dist/{menu-BHT6HRND.js → menu-CsBUyUuS.js} +2 -2
  65. package/dist/{menu-BHT6HRND.js.map → menu-CsBUyUuS.js.map} +1 -1
  66. package/dist/{menu-item-DlxAb0Xr.d.ts → menu-item-BbgdcwBX.d.ts} +11 -2
  67. package/dist/menu-item-BbgdcwBX.d.ts.map +1 -0
  68. package/dist/{menu-item-JcpHCyF0.js → menu-item-CfndBGO8.js} +38 -3
  69. package/dist/menu-item-CfndBGO8.js.map +1 -0
  70. package/dist/menu.d.ts +2 -2
  71. package/dist/menu.js +2 -2
  72. package/dist/miller.d.ts +3 -3
  73. package/dist/miller.d.ts.map +1 -1
  74. package/dist/miller.js +18 -2
  75. package/dist/miller.js.map +1 -1
  76. package/dist/panel.d.ts +1 -1
  77. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
  78. package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
  79. package/dist/split-layout.d.ts +1 -1
  80. package/dist/{tab-list-v8mKX7Fu.d.ts → tab-list-pqfKaVAE.d.ts} +2 -2
  81. package/dist/{tab-list-v8mKX7Fu.d.ts.map → tab-list-pqfKaVAE.d.ts.map} +1 -1
  82. package/dist/tabs.d.ts +1 -1
  83. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +2 -2
  84. package/dist/{titlebar-B5zSTZnF.d.ts.map → titlebar-Tqu-VvMU.d.ts.map} +1 -1
  85. package/dist/titlebar.d.ts +1 -1
  86. package/dist/{tree-component-CMGeqyjZ.d.ts → tree-component-DukLqesg.d.ts} +2 -2
  87. package/dist/{tree-component-CMGeqyjZ.d.ts.map → tree-component-DukLqesg.d.ts.map} +1 -1
  88. package/dist/tree.d.ts +4 -4
  89. package/dist/{types-3Jfcaxvo.d.ts → types-D-kYF16b.d.ts} +21 -1
  90. package/dist/types-D-kYF16b.d.ts.map +1 -0
  91. package/dist/{vertical-list-R25WqAVO.d.ts → vertical-list-DTp-UIUE.d.ts} +26 -3
  92. package/dist/vertical-list-DTp-UIUE.d.ts.map +1 -0
  93. package/dist/vertical-list.d.ts +2 -2
  94. package/dist/vertical-list.js +53 -4
  95. package/dist/vertical-list.js.map +1 -1
  96. package/docs-user/data-grid.md +51 -1
  97. package/docs-user/detail-list.md +46 -0
  98. package/docs-user/grid-list.md +49 -2
  99. package/docs-user/hyperbolic-tree.md +98 -18
  100. package/docs-user/index.json +2 -2
  101. package/docs-user/menu.md +30 -9
  102. package/docs-user/miller.md +52 -7
  103. package/docs-user/vertical-list.md +63 -0
  104. package/package.json +1 -1
  105. package/dist/data-grid-CfiaBqTA.d.ts.map +0 -1
  106. package/dist/detail-list-DxQZkK-C.d.ts.map +0 -1
  107. package/dist/grid-list-ChgYpUUq.d.ts.map +0 -1
  108. package/dist/grid-list-k89JpiNy.js.map +0 -1
  109. package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
  110. package/dist/menu-item-DlxAb0Xr.d.ts.map +0 -1
  111. package/dist/menu-item-JcpHCyF0.js.map +0 -1
  112. package/dist/types-3Jfcaxvo.d.ts.map +0 -1
  113. package/dist/vertical-list-R25WqAVO.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { n as captionForEntry, r as createDefaultListDragPreview, t as ListNativ
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";
23
- import "./menu-item-JcpHCyF0.js";
23
+ import "./menu-item-CfndBGO8.js";
24
24
  import { Checkbox } from "./checkbox.js";
25
25
  import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-WDrPuqoO.js";
26
26
  import { LabelledRangeInput } from "./labelled-range-input.js";
@@ -31,7 +31,7 @@ import { NarrowedTextElement } from "./narrowed-text.js";
31
31
  import { CrumbNavigationElement } from "./crumbs.js";
32
32
  import { DataDisplayComponent } from "./data-display.js";
33
33
  import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
34
- import { a as menuFromTreeToElements, t as menu_exports } from "./menu-BHT6HRND.js";
34
+ import { a as menuFromTreeToElements, t as menu_exports } from "./menu-CsBUyUuS.js";
35
35
  import { t as PanTextElement } from "./pan-text-CqtvjPyK.js";
36
36
  import { DataGridElement } from "./data-grid.js";
37
37
  import { DetailListElement } from "./detail-list.js";
@@ -42,7 +42,7 @@ import { t as resolveAxisStep } from "./keyboard-B5xD7Fz-.js";
42
42
  import { t as WheelNudgeMixin } from "./wheel-nudge-D5rmht3v.js";
43
43
  import { n as EditableNumber, t as LabelledSliderInput } from "./labelled-slider-input-FdCRi6st.js";
44
44
  import { t as FormElement } from "./form-CsXJX15p.js";
45
- import { t as GridListElement } from "./grid-list-k89JpiNy.js";
45
+ import { t as GridListElement } from "./grid-list-3fGhdaFA.js";
46
46
  import { PanelElement } from "./panel.js";
47
47
  import { GroupedItemListerElement } from "./grouped-item-lister.js";
48
48
  import { VerticalListElement } from "./vertical-list.js";
@@ -1497,6 +1497,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1497
1497
  this.path = ``;
1498
1498
  this.separator = `/`;
1499
1499
  this.pathRoot = ``;
1500
+ this.hintPrevious = false;
1500
1501
  this._openIdx = null;
1501
1502
  this._editing = false;
1502
1503
  this._cachedSiblings = /* @__PURE__ */ new Map();
@@ -1508,6 +1509,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1508
1509
  this._openCumPath = null;
1509
1510
  this._dragCumPath = null;
1510
1511
  this._dropCumPath = null;
1512
+ this._hintSegments = null;
1511
1513
  this.#uid = Math.random().toString(36).slice(2, 8);
1512
1514
  this.#clickSuppressUntil = 0;
1513
1515
  this.#onPopoverToggle = (e) => {
@@ -1643,6 +1645,19 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1643
1645
  #clicksSuppressed() {
1644
1646
  return Date.now() < this.#clickSuppressUntil || this._dragCumPath !== null;
1645
1647
  }
1648
+ #isSegmentPrefix(prefix, full) {
1649
+ return prefix.length <= full.length && prefix.every((seg, i) => seg === full[i]);
1650
+ }
1651
+ /**
1652
+ * Called when a real segment click shortens the path. Keeps the deepest
1653
+ * known branch: only overwrites `_hintSegments` when the current hint no
1654
+ * longer covers where we're navigating to.
1655
+ */
1656
+ #recordHintOnShorten(oldSegments, newSegments) {
1657
+ if (!this.hintPrevious) return;
1658
+ const hint = this._hintSegments;
1659
+ if (!(hint !== null && this.#isSegmentPrefix(newSegments, hint))) this._hintSegments = oldSegments;
1660
+ }
1646
1661
  #resolveSegmentIcon(fullPathSegments, index) {
1647
1662
  const direct = this.segmentIconLookup?.(fullPathSegments, index);
1648
1663
  if (typeof direct === `string` && direct.length > 0) return direct;
@@ -1788,12 +1803,63 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1788
1803
  composed: true
1789
1804
  }));
1790
1805
  }
1806
+ /**
1807
+ * The current leaf segment (deepest visible segment), used as the drop target
1808
+ * when a drag is released over blank space rather than a specific `.seg`.
1809
+ */
1810
+ #leafSegment() {
1811
+ const segments = this.#getSegments();
1812
+ if (segments.length > 0) {
1813
+ const index = segments.length - 1;
1814
+ return {
1815
+ cumPath: this.#getCumulativePath(index, false),
1816
+ fullPathSegments: this.#resolveFullPathSegments(index, false)
1817
+ };
1818
+ }
1819
+ const rootSegments = this.#getRootSegments();
1820
+ if (rootSegments.length > 0) {
1821
+ const index = rootSegments.length - 1;
1822
+ return {
1823
+ cumPath: this.#getCumulativePath(index, true),
1824
+ fullPathSegments: this.#resolveFullPathSegments(index, true)
1825
+ };
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Is `event` already being handled by a `.seg`'s own drag handlers? Virtual
1830
+ * (hint) segments don't have drag handlers of their own, so a drag released
1831
+ * over one falls through to the host-level fallback (the real leaf).
1832
+ */
1833
+ #eventTargetsSegment(event) {
1834
+ return Boolean(event.target?.closest?.(`.seg:not(.seg-virtual)`));
1835
+ }
1836
+ #onHostDragOver(event) {
1837
+ if (!this.parseExternalDrop || !event.dataTransfer) return;
1838
+ if (this.#eventTargetsSegment(event)) return;
1839
+ const leaf = this.#leafSegment();
1840
+ if (!leaf) return;
1841
+ event.preventDefault();
1842
+ if (this._dropCumPath !== leaf.cumPath) this._dropCumPath = leaf.cumPath;
1843
+ }
1844
+ #onHostDragLeave(event) {
1845
+ if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
1846
+ const leaf = this.#leafSegment();
1847
+ if (leaf && this._dropCumPath === leaf.cumPath) this._dropCumPath = null;
1848
+ }
1849
+ #onHostDrop(event) {
1850
+ if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
1851
+ const leaf = this.#leafSegment();
1852
+ if (!leaf) return;
1853
+ this.#onSegDrop(leaf.fullPathSegments, event);
1854
+ }
1791
1855
  #onLabelClick(index, event) {
1792
1856
  if (this.#clicksSuppressed()) return;
1793
1857
  event.preventDefault();
1794
1858
  event.stopPropagation();
1795
1859
  this.#closeDropdown();
1796
- const truncatedSegments = this.#getSegments().slice(0, index + 1);
1860
+ const segments = this.#getSegments();
1861
+ const truncatedSegments = segments.slice(0, index + 1);
1862
+ this.#recordHintOnShorten(segments, truncatedSegments);
1797
1863
  const hadLeading = this.path.startsWith(this.separator);
1798
1864
  const newPath = truncatedSegments.join(this.separator);
1799
1865
  this.path = hadLeading ? this.separator + newPath : newPath;
@@ -1811,6 +1877,31 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1811
1877
  composed: true
1812
1878
  }));
1813
1879
  }
1880
+ #onVirtualSegmentClick(hintIndex, event) {
1881
+ if (this.#clicksSuppressed()) return;
1882
+ event.preventDefault();
1883
+ event.stopPropagation();
1884
+ this.#closeDropdown();
1885
+ const hint = this._hintSegments;
1886
+ if (!hint) return;
1887
+ const truncatedSegments = hint.slice(0, hintIndex + 1);
1888
+ const hadLeading = this.path.startsWith(this.separator);
1889
+ const newPath = truncatedSegments.join(this.separator);
1890
+ this.path = hadLeading ? this.separator + newPath : newPath;
1891
+ this._cachedSiblings = /* @__PURE__ */ new Map();
1892
+ this._leafSegmentIndex = null;
1893
+ const parentPath = truncatedSegments.slice(0, -1).join(this.separator);
1894
+ this.dispatchEvent(new CustomEvent(`navigate`, {
1895
+ detail: {
1896
+ index: hintIndex,
1897
+ label: truncatedSegments[hintIndex],
1898
+ parentPath,
1899
+ fullPath: newPath
1900
+ },
1901
+ bubbles: true,
1902
+ composed: true
1903
+ }));
1904
+ }
1814
1905
  #selectSibling(index, segment) {
1815
1906
  const cumPath = this._openCumPath;
1816
1907
  this.#closeDropdown();
@@ -1886,7 +1977,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1886
1977
  @drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
1887
1978
  <label title=${label}
1888
1979
  part="label"
1889
- ?draggable=${draggable}
1980
+ draggable=${draggable ? `true` : `false`}
1890
1981
  @click=${(e) => this.#onLabelClick(index, e)}
1891
1982
  @dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
1892
1983
  @dragend=${() => this.#onLabelDragEnd()}>
@@ -1903,6 +1994,26 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1903
1994
  </div>
1904
1995
  ` : nothing}
1905
1996
  </div>
1997
+ `;
1998
+ }
1999
+ /**
2000
+ * A translucent, click-to-restore segment beyond the current path, shown
2001
+ * while `hintPrevious` is enabled and the current path is a prefix of the
2002
+ * deepest path visited. Doesn't participate in drag'n'drop and isn't
2003
+ * keyboard-focusable.
2004
+ */
2005
+ #renderVirtualSegment(label, hintIndex) {
2006
+ const fullPathSegments = [...this.#getRootSegments(), ...(this._hintSegments ?? []).slice(0, hintIndex + 1)];
2007
+ const icon = this.#resolveSegmentIcon(fullPathSegments, fullPathSegments.length - 1);
2008
+ return html`
2009
+ <div class="seg seg-virtual" part="segment virtual">
2010
+ <label title=${label}
2011
+ part="label"
2012
+ @click=${(e) => this.#onVirtualSegmentClick(hintIndex, e)}>
2013
+ ${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}
2014
+ ${label}
2015
+ </label>
2016
+ </div>
1906
2017
  `;
1907
2018
  }
1908
2019
  #renderRootSegment(label, index) {
@@ -1924,7 +2035,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
1924
2035
  @drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
1925
2036
  <label title=${label}
1926
2037
  part="label"
1927
- ?draggable=${draggable}
2038
+ draggable=${draggable ? `true` : `false`}
1928
2039
  @click=${(e) => this.#onLabelClick(index, e)}
1929
2040
  @dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
1930
2041
  @dragend=${() => this.#onLabelDragEnd()}>${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}${label}</label>
@@ -2030,6 +2141,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
2030
2141
  #onHostKeyDown;
2031
2142
  render() {
2032
2143
  const segments = this.#getSegments();
2144
+ const hintSegments = this.hintPrevious ? this._hintSegments : null;
2145
+ const virtualSegments = hintSegments !== null && hintSegments.length > segments.length && this.#isSegmentPrefix(segments, hintSegments) ? hintSegments.slice(segments.length) : [];
2033
2146
  if (this._editing) {
2034
2147
  const editValue = this.path;
2035
2148
  return html`
@@ -2052,10 +2165,15 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
2052
2165
  ${this.separator}
2053
2166
  </div>` : nothing;
2054
2167
  return html`
2055
- <div class="crumbs scroll-fade-x" tabindex="0" @keydown=${this.#onHostKeyDown}>
2168
+ <div class="crumbs scroll-fade-x" tabindex="0"
2169
+ @keydown=${this.#onHostKeyDown}
2170
+ @dragover=${(e) => this.#onHostDragOver(e)}
2171
+ @dragleave=${(e) => this.#onHostDragLeave(e)}
2172
+ @drop=${(e) => this.#onHostDrop(e)}>
2056
2173
  ${rootSegments.length > 0 ? rootSegments.map((label, i) => this.#renderRootSegment(label, i)) : nothing}
2057
2174
  ${leadingCaret}
2058
2175
  ${segments.map((label, i) => this.#renderSegment(label, i, i === segments.length - 1))}
2176
+ ${virtualSegments.map((label, i) => this.#renderVirtualSegment(label, segments.length + i))}
2059
2177
  <div class="trailing" @click=${this.#onTrailingClick}></div>
2060
2178
  </div>
2061
2179
  ${this._openIdx !== null ? html`
@@ -2082,6 +2200,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
2082
2200
  --padding-m: var(--space-s);
2083
2201
  --hover-bg: var(--surface-5);
2084
2202
  --hover-fg: var(--surface-5-text);
2203
+ --crumb-hint-opacity: 0.35;
2204
+ --crumb-hint-hover-opacity: 0.6;
2085
2205
 
2086
2206
  display: flex;
2087
2207
  width: 100%;
@@ -2193,6 +2313,14 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
2193
2313
  background: var(--crumb-drop-into-bg, color-mix(in srgb, var(--accent) 14%, transparent));
2194
2314
  }
2195
2315
 
2316
+ .seg-virtual {
2317
+ opacity: var(--crumb-hint-opacity);
2318
+ }
2319
+
2320
+ .seg-virtual:hover {
2321
+ opacity: var(--crumb-hint-hover-opacity);
2322
+ }
2323
+
2196
2324
  .crumb-icon {
2197
2325
  width: 1em;
2198
2326
  height: 1em;
@@ -2277,6 +2405,10 @@ __decorate([property({ type: Object })], CrumbPathElement.prototype, "siblingLoo
2277
2405
  __decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentIconLookup", void 0);
2278
2406
  __decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentDragLookup", void 0);
2279
2407
  __decorate([property({ type: Object })], CrumbPathElement.prototype, "parseExternalDrop", void 0);
2408
+ __decorate([property({
2409
+ type: Boolean,
2410
+ attribute: `hint-previous`
2411
+ })], CrumbPathElement.prototype, "hintPrevious", void 0);
2280
2412
  __decorate([state()], CrumbPathElement.prototype, "_openIdx", void 0);
2281
2413
  __decorate([state()], CrumbPathElement.prototype, "_editing", void 0);
2282
2414
  __decorate([state()], CrumbPathElement.prototype, "_cachedSiblings", void 0);
@@ -2288,6 +2420,7 @@ __decorate([state()], CrumbPathElement.prototype, "_editStartIndex", void 0);
2288
2420
  __decorate([state()], CrumbPathElement.prototype, "_openCumPath", void 0);
2289
2421
  __decorate([state()], CrumbPathElement.prototype, "_dragCumPath", void 0);
2290
2422
  __decorate([state()], CrumbPathElement.prototype, "_dropCumPath", void 0);
2423
+ __decorate([state()], CrumbPathElement.prototype, "_hintSegments", void 0);
2291
2424
  CrumbPathElement = __decorate([safeCustomElement(`ixfx-crumb-path`)], CrumbPathElement);
2292
2425
  //#endregion
2293
2426
  //#region src/data-table/data-table.ts
@@ -5721,6 +5854,90 @@ function createVerticalListAdapter(renderItem, options) {
5721
5854
  });
5722
5855
  return adapter;
5723
5856
  }
5857
+ /**
5858
+ * Whether `n` deserves a preview: it has children and at least one of them
5859
+ * isn't legible right now — laid out without a label, or not laid out at
5860
+ * all (outside the `maxRadius` window).
5861
+ */
5862
+ function needsChildPreview(n, visible, labelIds) {
5863
+ const children = n.node.children ?? [];
5864
+ if (children.length === 0) return false;
5865
+ const shown = visible.filter((v) => v.parentId === n.id);
5866
+ return shown.length < children.length || shown.some((v) => !labelIds.has(v.id));
5867
+ }
5868
+ /** Where node position `p` (disk units) lands on screen, in CSS px from the host's top-left. */
5869
+ function diskToHostPx(p, zoom, width, height) {
5870
+ const pxPerUnit = Math.min(width, height) / 2.1;
5871
+ return {
5872
+ x: width / 2 + (zoom.offset.x + zoom.zoom * p.x) * pxPerUnit,
5873
+ y: height / 2 + (zoom.offset.y + zoom.zoom * p.y) * pxPerUnit
5874
+ };
5875
+ }
5876
+ function renderChildPreview(preview, handlers) {
5877
+ if (!preview) return nothing;
5878
+ const children = preview.node.children ?? [];
5879
+ const listed = children.slice(0, 12);
5880
+ const more = children.length - listed.length;
5881
+ return html`
5882
+ <div
5883
+ class="child-preview ${preview.above ? `above` : `below`}"
5884
+ part="child-preview"
5885
+ role="dialog"
5886
+ aria-label=${`Children of ${preview.node.item.label}`}
5887
+ style="left: ${preview.anchor.x}px; top: ${preview.anchor.y}px"
5888
+ @pointerenter=${handlers.onPointerEnter}
5889
+ @pointerleave=${handlers.onPointerLeave}
5890
+ @pointerdown=${(e) => e.stopPropagation()}
5891
+ >
5892
+ <div class="child-preview-title">${preview.node.item.label}</div>
5893
+ <ul>
5894
+ ${listed.map((child) => html`<li>
5895
+ <button type="button" tabindex="-1" @click=${() => handlers.onPick(child)}>${child.item.label}</button>
5896
+ </li>`)}
5897
+ </ul>
5898
+ ${more > 0 ? html`<div class="child-preview-more">+${more} more</div>` : nothing}
5899
+ </div>
5900
+ `;
5901
+ }
5902
+ /**
5903
+ * Show/hide timing for the preview: shows at once for a qualifying node;
5904
+ * otherwise hides after a short grace period, unless the pointer is over the
5905
+ * popover itself — so it can travel from the node onto the list.
5906
+ */
5907
+ function createChildPreviewTiming(host) {
5908
+ let hovered = false;
5909
+ let timer;
5910
+ const update = (id) => {
5911
+ clearTimeout(timer);
5912
+ const candidate = host.candidate(id);
5913
+ if (candidate !== void 0) {
5914
+ host.setPreviewId(candidate);
5915
+ return;
5916
+ }
5917
+ timer = setTimeout(() => {
5918
+ if (!hovered) host.setPreviewId(void 0);
5919
+ }, 250);
5920
+ };
5921
+ return {
5922
+ update,
5923
+ pointerEnter() {
5924
+ hovered = true;
5925
+ clearTimeout(timer);
5926
+ },
5927
+ pointerLeave(tickledId) {
5928
+ hovered = false;
5929
+ update(tickledId);
5930
+ },
5931
+ hide() {
5932
+ hovered = false;
5933
+ clearTimeout(timer);
5934
+ host.setPreviewId(void 0);
5935
+ },
5936
+ dispose() {
5937
+ clearTimeout(timer);
5938
+ }
5939
+ };
5940
+ }
5724
5941
  //#endregion
5725
5942
  //#region src/hyperbolic-tree/poincare-math.ts
5726
5943
  const EPSILON = 1e-9;
@@ -5825,237 +6042,954 @@ function geodesicInterpolate(a, b, t) {
5825
6042
  if (localNorm < EPSILON) return a;
5826
6043
  return mobiusFromOrigin(complexScale(complexScale(bLocal, 1 / localNorm), euclideanRadiusForHyperbolicDistance(hyperbolicDistanceForEuclideanRadius(localNorm) * t)), a);
5827
6044
  }
5828
- //#endregion
5829
- //#region src/hyperbolic-tree/layout.ts
5830
- /** Depth-first search for the node with `key`. Returns `undefined` when absent. */
5831
- function findHyperbolicNode(root, key) {
5832
- if (!root) return void 0;
5833
- if (root.item.key === key) return root;
5834
- for (const child of root.children ?? []) {
5835
- const hit = findHyperbolicNode(child, key);
5836
- if (hit) return hit;
6045
+ const identityIsometry = {
6046
+ a: {
6047
+ x: 1,
6048
+ y: 0
6049
+ },
6050
+ b: {
6051
+ x: 0,
6052
+ y: 0
5837
6053
  }
6054
+ };
6055
+ /** Apply `m` to `z`. */
6056
+ function applyIsometry(m, z) {
6057
+ return clampToDisk(complexDiv(complexAdd(complexMul(m.a, z), m.b), complexAdd(complexMul(complexConjugate(m.b), z), complexConjugate(m.a))));
6058
+ }
6059
+ /** `outer ∘ inner` — apply `inner` first, then `outer`. */
6060
+ function composeIsometry(outer, inner) {
6061
+ const a = complexAdd(complexMul(outer.a, inner.a), complexMul(outer.b, complexConjugate(inner.b)));
6062
+ const b = complexAdd(complexMul(outer.a, inner.b), complexMul(outer.b, complexConjugate(inner.a)));
6063
+ const det = Math.sqrt(Math.max(EPSILON, a.x * a.x + a.y * a.y - b.x * b.x - b.y * b.y));
6064
+ return {
6065
+ a: complexScale(a, 1 / det),
6066
+ b: complexScale(b, 1 / det)
6067
+ };
5838
6068
  }
5839
- /**
5840
- * Chain of nodes from `root` down to (and including) the node with `key`.
5841
- * Returns `undefined` when the key is absent. The first entry is always `root`.
5842
- */
5843
- function findHyperbolicPath(root, key) {
5844
- if (!root) return void 0;
5845
- if (root.item.key === key) return [root];
5846
- for (const child of root.children ?? []) {
5847
- const hit = findHyperbolicPath(child, key);
5848
- if (hit) return [root, ...hit];
5849
- }
6069
+ function invertIsometry(m) {
6070
+ return {
6071
+ a: complexConjugate(m.a),
6072
+ b: complexScale(m.b, -1)
6073
+ };
5850
6074
  }
5851
- /**
5852
- * Number of nodes in `node`'s subtree, counting `node` itself, up to
5853
- * `remainingDepth` further levels (matching the canonical-position recursion's
5854
- * own `maxDepth` cutoff, so weighting stays consistent with what's actually
5855
- * laid out).
5856
- */
5857
- function subtreeSize(node, remainingDepth) {
5858
- if (remainingDepth <= 0) return 1;
5859
- const children = node.children ?? [];
5860
- if (children.length === 0) return 1;
5861
- let total = 1;
5862
- for (const child of children) total += subtreeSize(child, remainingDepth - 1);
5863
- return total;
6075
+ /** Pure translation carrying the origin to `p` (same map as {@link mobiusFromOrigin}). */
6076
+ function translationFromOrigin(p) {
6077
+ const scale = 1 / Math.sqrt(Math.max(EPSILON, 1 - p.x * p.x - p.y * p.y));
6078
+ return {
6079
+ a: {
6080
+ x: scale,
6081
+ y: 0
6082
+ },
6083
+ b: complexScale(p, scale)
6084
+ };
6085
+ }
6086
+ /** `z ↦ e^{iθ}·z`. */
6087
+ function rotationIsometry(theta) {
6088
+ return {
6089
+ a: {
6090
+ x: Math.cos(theta / 2),
6091
+ y: Math.sin(theta / 2)
6092
+ },
6093
+ b: {
6094
+ x: 0,
6095
+ y: 0
6096
+ }
6097
+ };
5864
6098
  }
5865
6099
  /**
5866
- * Angle (as seen from `localOrigin`, i.e. after translating `localOrigin` to
5867
- * the disk's center) of the disk-boundary point that currently sits at angle
5868
- * `theta` from the true origin. Möbius translation is conformal but not
5869
- * angle-preserving in the naive sense — a wedge boundary doesn't just shift
5870
- * by a constant offset when reparented to a new local frame, its exact
5871
- * transformed angle depends on how far off-center `localOrigin` is. Used to
5872
- * carry a node's *own* angular wedge (inherited from its parent) into the
5873
- * local frame of each of its children, rather than approximating it as
5874
- * "the opposite direction ± half the width" — the same technique used by
5875
- * reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
5876
- * `wedgeTranslate`) to keep the recursive layout accurate at any depth/width.
6100
+ * The view map that sends canonical point `center` to the origin, then
6101
+ * rotates by `rotation` radians: `z ↦ e^{i·rotation}·mobiusToOrigin(z, center)`.
5877
6102
  */
5878
- function boundaryAngleInLocalFrame(theta, localOrigin) {
5879
- const transformed = mobiusToOrigin({
5880
- x: Math.cos(theta),
5881
- y: Math.sin(theta)
5882
- }, localOrigin);
5883
- return Math.atan2(transformed.y, transformed.x);
6103
+ function viewIsometry(center, rotation = 0) {
6104
+ return composeIsometry(rotationIsometry(rotation), invertIsometry(translationFromOrigin(center)));
5884
6105
  }
5885
- /** Deterministic pseudo-random value in `[-1, 1]` derived from `key`, stable across renders. */
5886
- function stableUnitJitter(key) {
5887
- let hash = 0;
5888
- for (let i = 0; i < key.length; i++) hash = Math.imul(hash, 31) + key.charCodeAt(i) | 0;
5889
- return (hash >>> 0) % 1e3 / 1e3 * 2 - 1;
6106
+ /** Decompose `m` into the `{ center, rotation }` accepted by {@link viewIsometry}. */
6107
+ function decomposeViewIsometry(m) {
6108
+ return {
6109
+ center: applyIsometry(invertIsometry(m), {
6110
+ x: 0,
6111
+ y: 0
6112
+ }),
6113
+ rotation: 2 * Math.atan2(m.a.y, m.a.x)
6114
+ };
6115
+ }
6116
+ function geodesicSegment(p, q) {
6117
+ const det = p.x * q.y - p.y * q.x;
6118
+ const chord = Math.hypot(q.x - p.x, q.y - p.y);
6119
+ if (Math.abs(det) < 1e-6 * Math.max(chord, EPSILON)) return { kind: `line` };
6120
+ const rp = (p.x * p.x + p.y * p.y + 1) / 2;
6121
+ const rq = (q.x * q.x + q.y * q.y + 1) / 2;
6122
+ const center = {
6123
+ x: (rp * q.y - rq * p.y) / det,
6124
+ y: (p.x * rq - q.x * rp) / det
6125
+ };
6126
+ return {
6127
+ kind: `arc`,
6128
+ center,
6129
+ radius: Math.sqrt(Math.max(0, center.x * center.x + center.y * center.y - 1)),
6130
+ sweep: (p.x - center.x) * (q.y - center.y) - (p.y - center.y) * (q.x - center.x) > 0 ? 1 : 0
6131
+ };
6132
+ }
6133
+ /** SVG path `d` for the geodesic from `p` to `q`, with coordinates multiplied by `scale`. */
6134
+ function geodesicPath(p, q, scale = 1) {
6135
+ const fmt = (n) => (n * scale).toFixed(2);
6136
+ const segment = geodesicSegment(p, q);
6137
+ const start = `M${fmt(p.x)},${fmt(p.y)}`;
6138
+ if (segment.kind === `line` || segment.radius * scale > 1e6) return `${start} L${fmt(q.x)},${fmt(q.y)}`;
6139
+ return `${start} A${fmt(segment.radius)},${fmt(segment.radius)} 0 0 ${segment.sweep} ${fmt(q.x)},${fmt(q.y)}`;
6140
+ }
6141
+ //#endregion
6142
+ //#region src/hyperbolic-tree/view-motion.ts
6143
+ /** Wrap an angle into `(-π, π]`. */
6144
+ function wrapAngle(theta) {
6145
+ return Math.atan2(Math.sin(theta), Math.cos(theta));
6146
+ }
6147
+ function easeInOutCubic(t) {
6148
+ return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
6149
+ }
6150
+ function prefersReducedMotion$1() {
6151
+ return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
5890
6152
  }
5891
6153
  /**
5892
- * A single unbranching child (no siblings to fan against) would otherwise sit
5893
- * at the exact bisector of its inherited wedge — i.e. dead-center, continuing
5894
- * in a perfectly straight line from its own parent's edge. For a chain of
5895
- * such nodes (a deeply-nested path with no branching), that reads as one
5896
- * unbroken line cutting across the disk rather than a legible sequence of
5897
- * edges. Nudging by a small, key-derived (so stable across re-renders) angle
5898
- * breaks that up — the same purpose served by the small per-node offsets in
5899
- * reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
5900
- * `layoutBergé`, which staggers leaf nodes rather than stacking them exactly
5901
- * radially).
6154
+ * The view after dragging from `start` to `current` (both disk
6155
+ * coordinates), starting from `baseView`. Applies the hyperbolic translation
6156
+ * along the geodesic carrying the grabbed point to the pointer — a true
6157
+ * isometry, so the grabbed point tracks the pointer exactly and the tree
6158
+ * never distorts. (Adding a Euclidean offset to the canonical center
6159
+ * instead is not an isometry: it lurches badly near the disk boundary.)
5902
6160
  */
5903
- const CHAIN_JITTER_RADIANS = .22;
6161
+ function panView(baseView, start, current) {
6162
+ const toStart = translationFromOrigin(start);
6163
+ return composeIsometry(composeIsometry(toStart, composeIsometry(translationFromOrigin(mobiusToOrigin(current, start)), invertIsometry(toStart))), baseView);
6164
+ }
5904
6165
  /**
5905
- * Canonical (true-root-anchored) hyperbolic position of every currently-loaded
5906
- * node, keyed by node key. Positions never depend on the current focus —
5907
- * refocusing is purely a Möbius re-projection of these fixed coordinates, so
5908
- * the tree never needs to be re-laid-out when the focus changes.
5909
- *
5910
- * Each node holds an angular wedge — inherited from its parent, expressed in
5911
- * its *own* local frame (root starts with the full `[-π, π]`) — across which
5912
- * its children are placed at hyperbolic distance `curvature` (default `1`).
5913
- * That wedge is partitioned among the children, weighted by each child's own
5914
- * subtree size rather than split evenly, and each child's own share is
5915
- * carried into its local frame via {@link boundaryAngleInLocalFrame} before
5916
- * recursing. This is a strict partition (slices never overlap and always sum
5917
- * to the parent's own wedge), which is what guarantees sibling subtrees never
5918
- * spatially cross regardless of how deep or bushy they are.
5919
- *
5920
- * `curvature` scales how far apart (in hyperbolic distance) each generation
5921
- * sits. Raising it pushes each generation further out, so the Poincaré
5922
- * metric's own `tanh` compression crowds distant generations toward the
5923
- * boundary faster — a stronger fisheye, which is why it reads as "zooming
5924
- * in" on the focus. Lowering it spreads generations out more evenly,
5925
- * flattening the view toward a Euclidean-feeling layout.
6166
+ * A focus-change animation from view `from` — the old view, re-expressed in
6167
+ * the *new* focus's local frame, so the new focus currently sits somewhere
6168
+ * off-center — to a settled view with the new focus at the center. The
6169
+ * center travels along the hyperbolic geodesic; the rotation is held, so the
6170
+ * tree glides into place without spinning, and the settled view keeps
6171
+ * whatever orientation it arrived with (`settledRotation`).
5926
6172
  */
5927
- function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
5928
- const map = /* @__PURE__ */ new Map();
5929
- if (!root) return map;
6173
+ function focusTransition(from) {
6174
+ const { center, rotation } = decomposeViewIsometry(from);
6175
+ const settledRotation = wrapAngle(rotation);
5930
6176
  const origin = {
5931
6177
  x: 0,
5932
6178
  y: 0
5933
6179
  };
5934
- map.set(root.item.key, {
5935
- node: root,
5936
- depth: 0,
5937
- parentKey: void 0,
5938
- position: origin,
5939
- angularShare: 2 * Math.PI
5940
- });
5941
- const place = (node, position, wedgeStart, wedgeEnd, depth) => {
5942
- if (depth >= maxDepth) return;
5943
- const children = node.children ?? [];
5944
- const n = children.length;
5945
- if (n === 0) return;
5946
- const r0 = euclideanRadiusForHyperbolicDistance(curvature);
5947
- const angularWidth = wedgeEnd - wedgeStart;
5948
- const weights = children.map((child) => subtreeSize(child, maxDepth - depth - 1));
5949
- const totalWeight = weights.reduce((a, b) => a + b, 0);
5950
- let cursor = wedgeStart;
5951
- children.forEach((child, index) => {
5952
- const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
5953
- const childStart = cursor;
5954
- const childEnd = cursor + share;
5955
- cursor = childEnd;
5956
- const theta = (childStart + childEnd) / 2 + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
5957
- const local = {
5958
- x: r0 * Math.cos(theta),
5959
- y: r0 * Math.sin(theta)
5960
- };
5961
- const canonicalPosition = mobiusFromOrigin(local, position);
5962
- map.set(child.item.key, {
5963
- node: child,
5964
- depth: depth + 1,
5965
- parentKey: node.item.key,
5966
- position: canonicalPosition,
5967
- angularShare: share
5968
- });
5969
- const localStart = boundaryAngleInLocalFrame(childStart, local);
5970
- let localWidth = boundaryAngleInLocalFrame(childEnd, local) - localStart;
5971
- if (localWidth <= 0) localWidth += 2 * Math.PI;
5972
- place(child, canonicalPosition, localStart, localStart + localWidth, depth + 1);
5973
- });
6180
+ return {
6181
+ settledRotation,
6182
+ viewAt: (t) => {
6183
+ const eased = easeInOutCubic(Math.min(1, Math.max(0, t)));
6184
+ return viewIsometry(geodesicInterpolate(center, origin, eased), settledRotation);
6185
+ }
5974
6186
  };
5975
- place(root, origin, -Math.PI, Math.PI, 0);
5976
- return map;
5977
6187
  }
5978
- /** Nodes within `maxRadius` tree-graph steps of `focusedKey`, mapped to their step distance. */
5979
- function ringDistances(canonical, focusedKey, maxRadius) {
5980
- const ring = /* @__PURE__ */ new Map();
5981
- if (!canonical.has(focusedKey)) return ring;
5982
- ring.set(focusedKey, 0);
5983
- let frontier = [focusedKey];
5984
- for (let distance = 1; distance <= maxRadius && frontier.length > 0; distance++) {
5985
- const next = [];
5986
- for (const key of frontier) {
5987
- const entry = canonical.get(key);
5988
- if (!entry) continue;
5989
- const neighbours = [];
5990
- if (entry.parentKey !== void 0) neighbours.push(entry.parentKey);
5991
- for (const child of entry.node.children ?? []) neighbours.push(child.item.key);
5992
- for (const neighbour of neighbours) if (!ring.has(neighbour) && canonical.has(neighbour)) {
5993
- ring.set(neighbour, distance);
5994
- next.push(neighbour);
5995
- }
6188
+ //#endregion
6189
+ //#region src/hyperbolic-tree/gestures.ts
6190
+ const MIN_ZOOM = .5;
6191
+ /**
6192
+ * Keep the view center over the disk (so you can't zoom off into empty
6193
+ * space), and re-center entirely once the whole disk fits again.
6194
+ */
6195
+ function clampZoomState(state) {
6196
+ const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom));
6197
+ if (zoom <= 1) return {
6198
+ zoom,
6199
+ offset: {
6200
+ x: 0,
6201
+ y: 0
5996
6202
  }
5997
- frontier = next;
5998
- }
5999
- return ring;
6203
+ };
6204
+ const limit = zoom - 1;
6205
+ const length = Math.hypot(state.offset.x, state.offset.y);
6206
+ return {
6207
+ zoom,
6208
+ offset: length > limit ? {
6209
+ x: state.offset.x * limit / length,
6210
+ y: state.offset.y * limit / length
6211
+ } : state.offset
6212
+ };
6000
6213
  }
6001
6214
  /**
6002
- * Compute the visible nodes/edges of a hyperbolic tree view: every
6003
- * currently-loaded node within `maxRadius` tree-graph steps of the focused
6004
- * node, projected onto the Poincaré disk with the focus at its center.
6005
- *
6006
- * @param root True tree root. `undefined` yields an empty layout.
6007
- * @param options `focusedKey` (default the true root; unknown keys fall back
6008
- * to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
6009
- * cap on canonical-position recursion depth, default 24), `centerOverride`
6010
- * (project around this canonical-space point instead of the focus node's
6011
- * own position — used to animate smoothly between foci).
6215
+ * Zoom by `factor` keeping the disk point under `screenPoint` fixed on
6216
+ * screen — the "zoom where I'm pointing" behaviour of maps and image viewers.
6012
6217
  */
6013
- function computeHyperbolicLayout(root, options = {}) {
6014
- if (!root) return {
6015
- nodes: [],
6016
- edges: []
6218
+ function zoomAt(state, screenPoint, factor) {
6219
+ const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom * factor));
6220
+ const disk = screenToDisk(screenPoint, state);
6221
+ return clampZoomState({
6222
+ zoom,
6223
+ offset: {
6224
+ x: screenPoint.x - zoom * disk.x,
6225
+ y: screenPoint.y - zoom * disk.y
6226
+ }
6227
+ });
6228
+ }
6229
+ /**
6230
+ * A client-space position in screen units: disk radii from the SVG's center,
6231
+ * before zoom. The disk fills the SVG's smaller dimension, with the
6232
+ * viewBox's 5% margin.
6233
+ */
6234
+ function clientToScreen(rect, clientX, clientY) {
6235
+ const unitsPerPixel = 2.1 / (Math.min(rect.width, rect.height) || 1);
6236
+ return {
6237
+ x: (clientX - (rect.left + rect.width / 2)) * unitsPerPixel,
6238
+ y: (clientY - (rect.top + rect.height / 2)) * unitsPerPixel
6017
6239
  };
6018
- const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1 } = options;
6019
- const canonical = computeCanonicalPositions(root, maxDepth, curvature);
6020
- const focusedKey = options.focusedKey !== void 0 && canonical.has(options.focusedKey) ? options.focusedKey : root.item.key;
6021
- const focusEntry = canonical.get(focusedKey);
6022
- if (!focusEntry) return {
6023
- nodes: [],
6024
- edges: []
6240
+ }
6241
+ function screenToDisk(p, state) {
6242
+ const zoom = state.zoom || 1;
6243
+ return {
6244
+ x: (p.x - state.offset.x) / zoom,
6245
+ y: (p.y - state.offset.y) / zoom
6025
6246
  };
6026
- const projectionCenter = options.centerOverride ?? focusEntry.position;
6027
- const ring = ringDistances(canonical, focusedKey, maxRadius);
6028
- const rawWeights = [...ring.keys()].map((key) => canonical.get(key).node.item.weight).filter((w) => typeof w === `number`);
6247
+ }
6248
+ /** Is `event` on empty disk space (the SVG itself or the boundary circle), rather than a node? */
6249
+ function isBackground(event) {
6250
+ const target = event.target;
6251
+ return target === event.currentTarget || target?.classList.contains(`disk-boundary`) === true;
6252
+ }
6253
+ function createGestureHandlers(host) {
6254
+ const pointers = /* @__PURE__ */ new Map();
6255
+ let pan;
6256
+ let pinch;
6257
+ const screenOf = (event) => {
6258
+ const rect = host.svgRect();
6259
+ return rect ? clientToScreen(rect, event.clientX, event.clientY) : void 0;
6260
+ };
6261
+ const diskOf = (screen) => clampToDisk(screenToDisk(screen, host.zoomState()), .98);
6262
+ const midpoint = (a, b) => ({
6263
+ x: (a.x + b.x) / 2,
6264
+ y: (a.y + b.y) / 2
6265
+ });
6266
+ const startPan = (pointerId, screen) => {
6267
+ pan = {
6268
+ pointerId,
6269
+ start: diskOf(screen),
6270
+ baseView: host.takeView()
6271
+ };
6272
+ };
6273
+ const startPinch = () => {
6274
+ const [[idA, a], [idB, b]] = [...pointers.entries()];
6275
+ pan = void 0;
6276
+ const { zoom } = host.zoomState();
6277
+ pinch = {
6278
+ ids: [idA, idB],
6279
+ startDistance: Math.max(1e-6, Math.hypot(a.x - b.x, a.y - b.y)),
6280
+ startZoom: zoom,
6281
+ anchor: screenToDisk(midpoint(a, b), host.zoomState())
6282
+ };
6283
+ };
6284
+ return {
6285
+ onPointerDown(event) {
6286
+ if (!isBackground(event)) return;
6287
+ host.focusTree();
6288
+ const screen = screenOf(event);
6289
+ if (!screen) return;
6290
+ event.currentTarget.setPointerCapture?.(event.pointerId);
6291
+ pointers.set(event.pointerId, screen);
6292
+ if (pointers.size === 2 && host.zoomGesturesEnabled()) startPinch();
6293
+ else if (pointers.size === 1) startPan(event.pointerId, screen);
6294
+ },
6295
+ onPointerMove(event) {
6296
+ if (!pointers.has(event.pointerId)) return;
6297
+ const screen = screenOf(event);
6298
+ if (!screen) return;
6299
+ pointers.set(event.pointerId, screen);
6300
+ if (pinch) {
6301
+ const a = pointers.get(pinch.ids[0]);
6302
+ const b = pointers.get(pinch.ids[1]);
6303
+ if (!a || !b) return;
6304
+ const zoom = pinch.startZoom * Math.hypot(a.x - b.x, a.y - b.y) / pinch.startDistance;
6305
+ const mid = midpoint(a, b);
6306
+ const clampedZoom = Math.min(8, Math.max(MIN_ZOOM, zoom));
6307
+ host.setZoomState(clampZoomState({
6308
+ zoom: clampedZoom,
6309
+ offset: {
6310
+ x: mid.x - clampedZoom * pinch.anchor.x,
6311
+ y: mid.y - clampedZoom * pinch.anchor.y
6312
+ }
6313
+ }));
6314
+ } else if (pan && pan.pointerId === event.pointerId) host.setView(panView(pan.baseView, pan.start, diskOf(screen)));
6315
+ },
6316
+ onPointerUp(event) {
6317
+ if (!pointers.delete(event.pointerId)) return;
6318
+ if (pinch) {
6319
+ pinch = void 0;
6320
+ const [remaining] = [...pointers.entries()];
6321
+ if (remaining) startPan(remaining[0], remaining[1]);
6322
+ } else if (pan?.pointerId === event.pointerId) pan = void 0;
6323
+ },
6324
+ /**
6325
+ * Ctrl/⌘-wheel — which is also what a trackpad pinch produces — always
6326
+ * zooms while the pointer is over the tree. A plain wheel only zooms
6327
+ * once the tree has keyboard focus (the library's focus-gated wheel
6328
+ * convention), so scrolling past it doesn't get hijacked.
6329
+ */
6330
+ onWheel(event) {
6331
+ const modified = event.ctrlKey || event.metaKey;
6332
+ if (!host.zoomGesturesEnabled() || !modified && !host.hasFocus()) return;
6333
+ const screen = screenOf(event);
6334
+ if (!screen) return;
6335
+ event.preventDefault();
6336
+ const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
6337
+ const factor = Math.exp(-delta * (modified ? .01 : .002));
6338
+ host.setZoomState(zoomAt(host.zoomState(), screen, factor));
6339
+ },
6340
+ /** Forget any in-progress pan (e.g. when a focus change takes over the view). */
6341
+ cancel() {
6342
+ pan = void 0;
6343
+ pinch = void 0;
6344
+ pointers.clear();
6345
+ }
6346
+ };
6347
+ }
6348
+ //#endregion
6349
+ //#region src/hyperbolic-tree/hyperbolictree-styles.ts
6350
+ /** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
6351
+ const hyperbolicTreeStyles = css`
6352
+ :host {
6353
+ --hyperbolictree-node-color: var(--surface-4);
6354
+ --hyperbolictree-node-text: var(--surface-3-text);
6355
+ --hyperbolictree-edge-color: var(--surface-6);
6356
+ --hyperbolictree-boundary-color: var(--border-subtle);
6357
+ /* Real screen px — converted to SVG units from the element's rendered size. */
6358
+ --hyperbolictree-font-size: 13px;
6359
+ --hyperbolictree-box-color: var(--hyperbolictree-node-color);
6360
+ --hyperbolictree-box-text: var(--surface-4-text);
6361
+ --hyperbolictree-box-border: var(--surface-2);
6362
+ --hyperbolictree-branch-hue-start: 250;
6363
+ --hyperbolictree-branch-chroma: 0.13;
6364
+ --hyperbolictree-branch-lightness: 62%;
6365
+ --hyperbolictree-branch-mix: 55%;
6366
+
6367
+ display: block;
6368
+ position: relative;
6369
+ overflow: hidden;
6370
+ outline: none;
6371
+ overscroll-behavior: contain;
6372
+ }
6373
+
6374
+ /*
6375
+ * delegatesFocus means the host itself reports as focused whenever
6376
+ * the shadow DOM's focused descendant (the svg) is — without this,
6377
+ * the host also picks up the browser's own default focus ring/fill
6378
+ * sized to the whole box.
6379
+ */
6380
+ :host(:focus),
6381
+ :host(:focus-visible) {
6382
+ outline: none;
6383
+ }
6384
+
6385
+ .hyperbolictree {
6386
+ display: block;
6387
+ width: 100%;
6388
+ height: 100%;
6389
+ outline: none;
6390
+ touch-action: none;
6391
+ overscroll-behavior: contain;
6392
+ }
6393
+
6394
+ .disk-boundary {
6395
+ fill: none;
6396
+ stroke: var(--hyperbolictree-boundary-color);
6397
+ stroke-width: 1;
6398
+ }
6399
+
6400
+ .edge {
6401
+ fill: none;
6402
+ stroke: var(--hyperbolictree-edge-color);
6403
+ /* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
6404
+ stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
6405
+ /* Constant on-screen width, however far the disk is zoomed. */
6406
+ vector-effect: non-scaling-stroke;
6407
+ }
6408
+
6409
+ .node {
6410
+ cursor: pointer;
6411
+ }
6412
+
6413
+ /*
6414
+ * color-by-branch: each top-level branch gets an evenly-spaced hue,
6415
+ * mixed into the normal node/box/edge colour (rather than replacing it)
6416
+ * so it stays in keeping with — and legible in — the current theme.
6417
+ * Only sets the base colour vars, so selected/tickled states still win.
6418
+ */
6419
+ :host([color-by-branch]) .node,
6420
+ :host([color-by-branch]) .edge {
6421
+ --_branch-color: oklch(
6422
+ var(--hyperbolictree-branch-lightness)
6423
+ var(--hyperbolictree-branch-chroma)
6424
+ calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
6425
+ );
6426
+ }
6427
+
6428
+ :host([color-by-branch]) .node {
6429
+ --_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
6430
+ --_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
6431
+ }
6432
+
6433
+ :host([color-by-branch]) .edge {
6434
+ stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
6435
+ }
6436
+
6437
+ .node-hit {
6438
+ fill: transparent;
6439
+ pointer-events: all;
6440
+ }
6441
+
6442
+ .node-dot {
6443
+ /* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
6444
+ fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
6445
+ stroke: var(--surface-2);
6446
+ stroke-width: 1;
6447
+ pointer-events: none;
6448
+ }
6449
+
6450
+ .node-box {
6451
+ fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
6452
+ stroke: var(--hyperbolictree-box-border);
6453
+ stroke-width: 1.5;
6454
+ /* Keep the border crisp however far the box has been scaled down. */
6455
+ vector-effect: non-scaling-stroke;
6456
+ }
6457
+
6458
+ /* Text-less stand-in for a box too small, or too crowded, to label. */
6459
+ .node-box.marker {
6460
+ stroke-width: 1;
6461
+ }
6462
+
6463
+ /* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
6464
+ .hyperbolictree-label {
6465
+ fill: var(--hyperbolictree-node-text);
6466
+ cursor: pointer;
6467
+ user-select: none;
6468
+ }
6469
+
6470
+ .box-label {
6471
+ fill: var(--hyperbolictree-box-text);
6472
+ pointer-events: none;
6473
+ }
6474
+
6475
+ /*
6476
+ * State fills come paired with their theme text tokens: a box label sits
6477
+ * on the state fill itself, so e.g. --item-bg-selected needs
6478
+ * --item-text-selected on top, not the resting box text colour.
6479
+ */
6480
+ .node.tickled .node-dot,
6481
+ .node.tickled .node-box {
6482
+ fill: var(--item-bg-tickled);
6483
+ }
6484
+
6485
+ .node.tickled .box-label {
6486
+ fill: var(--item-text-tickled);
6487
+ }
6488
+
6489
+ .node.selected .node-dot,
6490
+ .node.selected .node-box {
6491
+ fill: var(--item-bg-selected);
6492
+ }
6493
+
6494
+ .node.selected .box-label {
6495
+ fill: var(--item-text-selected);
6496
+ }
6497
+
6498
+ .node.selected.tickled .node-dot,
6499
+ .node.selected.tickled .node-box {
6500
+ fill: var(--item-bg-selected-tickled);
6501
+ }
6502
+
6503
+ .node.focused .node-dot,
6504
+ .node.focused .node-box {
6505
+ stroke: var(--accent);
6506
+ stroke-width: 3;
6507
+ }
6508
+
6509
+ /* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
6510
+ .node.tickled .hyperbolictree-label:not(.box-label) {
6511
+ paint-order: stroke;
6512
+ stroke: var(--surface-2);
6513
+ stroke-width: 4px;
6514
+ stroke-linejoin: round;
6515
+ }
6516
+
6517
+ .node.filtered {
6518
+ opacity: 0.3;
6519
+ }
6520
+
6521
+ .node.loading .node-dot,
6522
+ .node.loading .node-box {
6523
+ opacity: 0.6;
6524
+ }
6525
+
6526
+
6527
+ /* Child preview: a clickable list of a crowded node's children. */
6528
+ .child-preview {
6529
+ position: absolute;
6530
+ z-index: 1;
6531
+ min-width: 8rem;
6532
+ max-width: min(18rem, 60%);
6533
+ max-height: 45%;
6534
+ overflow: auto;
6535
+ padding: var(--space-xs, 4px);
6536
+ background: var(--surface-3);
6537
+ color: var(--surface-3-text);
6538
+ border: var(--border-subtle);
6539
+ border-radius: var(--radius-m, 6px);
6540
+ box-shadow: var(--shadow-m);
6541
+ font-size: var(--hyperbolictree-font-size);
6542
+ transform: translate(-50%, 14px);
6543
+ }
6544
+
6545
+ .child-preview.above {
6546
+ transform: translate(-50%, calc(-100% - 14px));
6547
+ }
6548
+
6549
+ .child-preview-title {
6550
+ padding: 2px 6px 4px;
6551
+ font-weight: 600;
6552
+ overflow: hidden;
6553
+ text-overflow: ellipsis;
6554
+ white-space: nowrap;
6555
+ }
6556
+
6557
+ .child-preview ul {
6558
+ margin: 0;
6559
+ padding: 0;
6560
+ list-style: none;
6561
+ }
6562
+
6563
+ .child-preview button {
6564
+ display: block;
6565
+ width: 100%;
6566
+ padding: 2px 6px;
6567
+ border: none;
6568
+ border-radius: var(--radius-s, 4px);
6569
+ background: none;
6570
+ color: inherit;
6571
+ font: inherit;
6572
+ text-align: start;
6573
+ overflow: hidden;
6574
+ text-overflow: ellipsis;
6575
+ white-space: nowrap;
6576
+ cursor: pointer;
6577
+ }
6578
+
6579
+ .child-preview button:hover {
6580
+ background: var(--item-bg-tickled);
6581
+ color: var(--item-text-tickled);
6582
+ }
6583
+
6584
+ .child-preview-more {
6585
+ padding: 2px 6px;
6586
+ color: var(--surface-muted-text);
6587
+ }
6588
+ `;
6589
+ //#endregion
6590
+ //#region src/hyperbolic-tree/layout.ts
6591
+ const ID_SEPARATOR = `\u001F`;
6592
+ /**
6593
+ * Path-derived ids for `children` of the node with id `parentId`. `TreeItem.key`
6594
+ * is meant to be unique, but real data (e.g. a file system, where `readme.md`
6595
+ * recurs in many folders) often only has keys unique among siblings. Keying
6596
+ * the layout on `item.key` alone would silently merge those nodes — edges get
6597
+ * attached to the wrong parent and clicks focus the wrong node — so the
6598
+ * layout identifies nodes by their key path instead. Repeated keys among
6599
+ * siblings are disambiguated by occurrence count. The root's id is its key.
6600
+ */
6601
+ function hyperbolicChildIds(parentId, children) {
6602
+ const seen = /* @__PURE__ */ new Map();
6603
+ return children.map((child) => {
6604
+ const key = child.item.key;
6605
+ const count = seen.get(key) ?? 0;
6606
+ seen.set(key, count + 1);
6607
+ return `${parentId}${ID_SEPARATOR}${count === 0 ? key : `${key}#${count}`}`;
6608
+ });
6609
+ }
6610
+ /** Depth (root is 0) of the node with layout id `id`. */
6611
+ function hyperbolicIdDepth(id) {
6612
+ return id.split(ID_SEPARATOR).length - 1;
6613
+ }
6614
+ /**
6615
+ * Layout id of the root's child that `id` descends from (or is) — i.e. which
6616
+ * top-level branch it belongs to. `undefined` for the root itself.
6617
+ */
6618
+ function hyperbolicBranchId(id) {
6619
+ const parts = id.split(ID_SEPARATOR);
6620
+ return parts.length < 2 ? void 0 : `${parts[0]}${ID_SEPARATOR}${parts[1]}`;
6621
+ }
6622
+ /** Whether layout id `id` is a strict descendant of `ancestorId`. */
6623
+ function isHyperbolicDescendantId(id, ancestorId) {
6624
+ return id.startsWith(`${ancestorId}${ID_SEPARATOR}`);
6625
+ }
6626
+ /** Chain of nodes from `root` down to the node with layout id `id`, or `undefined` if absent. */
6627
+ function findHyperbolicPathById(root, id) {
6628
+ if (!root) return void 0;
6629
+ const walk = (node, nodeId) => {
6630
+ if (nodeId === id) return [node];
6631
+ if (!isHyperbolicDescendantId(id, nodeId)) return void 0;
6632
+ const children = node.children ?? [];
6633
+ const ids = hyperbolicChildIds(nodeId, children);
6634
+ for (let i = 0; i < children.length; i++) {
6635
+ const hit = walk(children[i], ids[i]);
6636
+ if (hit) return [node, ...hit];
6637
+ }
6638
+ };
6639
+ return walk(root, root.item.key);
6640
+ }
6641
+ /** Layout id of `target` (matched by object identity) within `root`, or `undefined` if absent. */
6642
+ function findHyperbolicIdOf(root, target) {
6643
+ if (!root) return void 0;
6644
+ const walk = (node, nodeId) => {
6645
+ if (node === target) return nodeId;
6646
+ const children = node.children ?? [];
6647
+ const ids = hyperbolicChildIds(nodeId, children);
6648
+ for (let i = 0; i < children.length; i++) {
6649
+ const hit = walk(children[i], ids[i]);
6650
+ if (hit !== void 0) return hit;
6651
+ }
6652
+ };
6653
+ return walk(root, root.item.key);
6654
+ }
6655
+ /** Key path (from the root's children) to the first node, depth-first, satisfying `predicate`. */
6656
+ function findHyperbolicKeyPath(nodes, predicate, prefix = []) {
6657
+ for (const node of nodes) {
6658
+ const path = [...prefix, node.item.key];
6659
+ if (predicate(node)) return path;
6660
+ const hit = node.children?.length ? findHyperbolicKeyPath(node.children, predicate, path) : void 0;
6661
+ if (hit) return hit;
6662
+ }
6663
+ }
6664
+ /** Depth-first search for the node with `key`. Returns `undefined` when absent. */
6665
+ function findHyperbolicNode(root, key) {
6666
+ if (!root) return void 0;
6667
+ if (root.item.key === key) return root;
6668
+ for (const child of root.children ?? []) {
6669
+ const hit = findHyperbolicNode(child, key);
6670
+ if (hit) return hit;
6671
+ }
6672
+ }
6673
+ /**
6674
+ * Chain of nodes from `root` down to (and including) the node with `key`.
6675
+ * Returns `undefined` when the key is absent. The first entry is always `root`.
6676
+ */
6677
+ function findHyperbolicPath(root, key) {
6678
+ if (!root) return void 0;
6679
+ if (root.item.key === key) return [root];
6680
+ for (const child of root.children ?? []) {
6681
+ const hit = findHyperbolicPath(child, key);
6682
+ if (hit) return [root, ...hit];
6683
+ }
6684
+ }
6685
+ /**
6686
+ * Number of nodes in `node`'s subtree, counting `node` itself, up to
6687
+ * `remainingDepth` further levels (matching the canonical-position recursion's
6688
+ * own `maxDepth` cutoff, so weighting stays consistent with what's actually
6689
+ * laid out).
6690
+ */
6691
+ function subtreeSize(node, remainingDepth) {
6692
+ if (remainingDepth <= 0) return 1;
6693
+ const children = node.children ?? [];
6694
+ if (children.length === 0) return 1;
6695
+ let total = 1;
6696
+ for (const child of children) total += subtreeSize(child, remainingDepth - 1);
6697
+ return total;
6698
+ }
6699
+ /**
6700
+ * Angle (as seen from `localOrigin`, i.e. after translating `localOrigin` to
6701
+ * the disk's center) of the disk-boundary point that currently sits at angle
6702
+ * `theta` from the true origin. Möbius translation is conformal but not
6703
+ * angle-preserving in the naive sense — a wedge boundary doesn't just shift
6704
+ * by a constant offset when reparented to a new local frame, its exact
6705
+ * transformed angle depends on how far off-center `localOrigin` is. Used to
6706
+ * carry a node's *own* angular wedge (inherited from its parent) into the
6707
+ * local frame of each of its children, rather than approximating it as
6708
+ * "the opposite direction ± half the width" — the same technique used by
6709
+ * reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
6710
+ * `wedgeTranslate`) to keep the recursive layout accurate at any depth/width.
6711
+ */
6712
+ function boundaryAngleInLocalFrame(theta, localOrigin) {
6713
+ const transformed = mobiusToOrigin({
6714
+ x: Math.cos(theta),
6715
+ y: Math.sin(theta)
6716
+ }, localOrigin);
6717
+ return Math.atan2(transformed.y, transformed.x);
6718
+ }
6719
+ /** Deterministic pseudo-random value in `[-1, 1]` derived from `key`, stable across renders. */
6720
+ function stableUnitJitter(key) {
6721
+ let hash = 0;
6722
+ for (let i = 0; i < key.length; i++) hash = Math.imul(hash, 31) + key.charCodeAt(i) | 0;
6723
+ return (hash >>> 0) % 1e3 / 1e3 * 2 - 1;
6724
+ }
6725
+ /**
6726
+ * A single unbranching child (no siblings to fan against) would otherwise sit
6727
+ * at the exact bisector of its inherited wedge — i.e. dead-center, continuing
6728
+ * in a perfectly straight line from its own parent's edge. For a chain of
6729
+ * such nodes (a deeply-nested path with no branching), that reads as one
6730
+ * unbroken line cutting across the disk rather than a legible sequence of
6731
+ * edges. Nudging by a small, key-derived (so stable across re-renders) angle
6732
+ * breaks that up — the same purpose served by the small per-node offsets in
6733
+ * reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
6734
+ * `layoutBergé`, which staggers leaf nodes rather than stacking them exactly
6735
+ * radially).
6736
+ */
6737
+ const CHAIN_JITTER_RADIANS = .22;
6738
+ /** Default minimum hyperbolic distance between adjacent siblings. See {@link childDistance}. */
6739
+ const DEFAULT_SIBLING_SPACING = .5;
6740
+ /**
6741
+ * Hyperbolic distance from parent to a child whose angular slice (in the
6742
+ * parent's frame) is `share`, such that the child sits at least `spacing`
6743
+ * from a neighbour one slice-width away: from the hyperbolic law of cosines
6744
+ * for an isosceles triangle, `sinh(d/2) = sinh(L)·sin(Δθ/2)`, solved for L.
6745
+ * Never less than `minimum` (the `curvature`), and capped so a sliver-thin
6746
+ * slice can't fling a child off to infinity.
6747
+ *
6748
+ * This is what makes panning *useful* in a crowded fan: panning is an
6749
+ * isometry — it can't change the hyperbolic distance between siblings, only
6750
+ * move them to where the disk magnifies more. If siblings are laid out
6751
+ * nearly on top of each other in hyperbolic terms, no amount of dragging
6752
+ * separates them; guaranteeing a minimum spacing means dragging a crowd
6753
+ * towards the center always fans it out.
6754
+ */
6755
+ function childDistance(share, spacing, minimum) {
6756
+ if (spacing <= 0) return minimum;
6757
+ const halfAngle = Math.min(share, Math.PI) / 2;
6758
+ const needed = Math.asinh(Math.sinh(spacing / 2) / Math.max(Math.sin(halfAngle), 1e-6));
6759
+ return Math.min(Math.max(minimum, needed), minimum + 6);
6760
+ }
6761
+ /**
6762
+ * Canonical (true-root-anchored) hyperbolic position of every currently-loaded
6763
+ * node, keyed by layout id (see {@link hyperbolicChildIds}). Positions never depend on the current focus —
6764
+ * refocusing is purely a Möbius re-projection of these fixed coordinates, so
6765
+ * the tree never needs to be re-laid-out when the focus changes.
6766
+ *
6767
+ * Each node holds an angular wedge — inherited from its parent, expressed in
6768
+ * its *own* local frame (root starts with the full `[-π, π]`) — across which
6769
+ * its children are placed at hyperbolic distance of at least `curvature`
6770
+ * (default `1`) — further for a thin slice, per `siblingSpacing` (see
6771
+ * {@link childDistance}).
6772
+ * That wedge is partitioned among the children, weighted by √ of each child's own
6773
+ * subtree size rather than split evenly, and each child's own share is
6774
+ * carried into its local frame via {@link boundaryAngleInLocalFrame} before
6775
+ * recursing. This is a strict partition, and each child's wedge region
6776
+ * (bounded by geodesic rays to the same two ideal boundary points as its
6777
+ * slice) nests inside its parent's, which is what guarantees sibling
6778
+ * subtrees never spatially cross regardless of how deep or bushy they are.
6779
+ *
6780
+ * Every node's local frame is tracked as a full {@link DiskIsometry}
6781
+ * (local → canonical), built by composing its parent's frame with the
6782
+ * translation to the child. Composing Möbius translations introduces a
6783
+ * rotation (gyration); dropping it — i.e. assuming a node's frame is simply
6784
+ * `mobiusToOrigin(·, position)` — makes every wedge from depth 3 onward
6785
+ * point in a subtly wrong direction, so subtrees cross each other.
6786
+ *
6787
+ * A wedge wider than π is narrowed to π (around the same bisector) before
6788
+ * the child recurses into it: a near-full-circle wedge would otherwise fan
6789
+ * that child's own children back around towards — and across — its parent.
6790
+ *
6791
+ * `curvature` scales how far apart (in hyperbolic distance) each generation
6792
+ * sits. Raising it pushes each generation further out, so the Poincaré
6793
+ * metric's own `tanh` compression crowds distant generations toward the
6794
+ * boundary faster — a stronger fisheye, which is why it reads as "zooming
6795
+ * in" on the focus. Lowering it spreads generations out more evenly,
6796
+ * flattening the view toward a Euclidean-feeling layout.
6797
+ */
6798
+ function computeCanonicalPositions(root, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING) {
6799
+ const map = /* @__PURE__ */ new Map();
6800
+ if (!root) return map;
6801
+ const origin = {
6802
+ x: 0,
6803
+ y: 0
6804
+ };
6805
+ map.set(root.item.key, {
6806
+ node: root,
6807
+ id: root.item.key,
6808
+ depth: 0,
6809
+ parentKey: void 0,
6810
+ parentId: void 0,
6811
+ position: origin,
6812
+ toParent: identityIsometry,
6813
+ angularShare: 2 * Math.PI
6814
+ });
6815
+ const place = (node, id, frame, wedgeStart, wedgeEnd, depth) => {
6816
+ if (depth >= maxDepth) return;
6817
+ const children = node.children ?? [];
6818
+ const n = children.length;
6819
+ if (n === 0) return;
6820
+ const angularWidth = wedgeEnd - wedgeStart;
6821
+ const weights = children.map((child) => Math.sqrt(subtreeSize(child, maxDepth - depth - 1)));
6822
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
6823
+ const ids = hyperbolicChildIds(id, children);
6824
+ let cursor = wedgeStart;
6825
+ children.forEach((child, index) => {
6826
+ const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
6827
+ const bisector = cursor + share / 2;
6828
+ cursor += share;
6829
+ const theta = bisector + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
6830
+ const r = euclideanRadiusForHyperbolicDistance(n === 1 ? curvature : childDistance(share, siblingSpacing, curvature));
6831
+ const local = {
6832
+ x: r * Math.cos(theta),
6833
+ y: r * Math.sin(theta)
6834
+ };
6835
+ const toParent = translationFromOrigin(local);
6836
+ const childFrame = composeIsometry(frame, toParent);
6837
+ map.set(ids[index], {
6838
+ node: child,
6839
+ id: ids[index],
6840
+ depth: depth + 1,
6841
+ parentKey: node.item.key,
6842
+ parentId: id,
6843
+ position: applyIsometry(childFrame, origin),
6844
+ toParent,
6845
+ angularShare: share
6846
+ });
6847
+ const halfWedge = Math.min(share, Math.PI) / 2;
6848
+ const localStart = boundaryAngleInLocalFrame(bisector - halfWedge, local);
6849
+ let localWidth = boundaryAngleInLocalFrame(bisector + halfWedge, local) - localStart;
6850
+ if (localWidth <= 0) localWidth += 2 * Math.PI;
6851
+ place(child, ids[index], childFrame, localStart, localStart + localWidth, depth + 1);
6852
+ });
6853
+ };
6854
+ place(root, root.item.key, identityIsometry, -Math.PI, Math.PI, 0);
6855
+ return map;
6856
+ }
6857
+ /**
6858
+ * Every node within `maxRadius` tree-graph steps of `focusedId`, with its
6859
+ * frame relative to the focus. Built by walking outward from the focus and
6860
+ * composing one short {@link CanonicalEntry.toParent} hop per edge — so
6861
+ * precision depends only on distance *from the focus*, never on how far the
6862
+ * focus is from the root.
6863
+ */
6864
+ function focusFrames(entries, focusedId, maxRadius) {
6865
+ const frames = /* @__PURE__ */ new Map();
6866
+ if (!entries.has(focusedId)) return frames;
6867
+ frames.set(focusedId, {
6868
+ ring: 0,
6869
+ frame: identityIsometry
6870
+ });
6871
+ let frontier = [focusedId];
6872
+ for (let ring = 1; ring <= maxRadius && frontier.length > 0; ring++) {
6873
+ const next = [];
6874
+ for (const id of frontier) {
6875
+ const entry = entries.get(id);
6876
+ const { frame } = frames.get(id);
6877
+ const visit = (neighbourId, neighbourFrame) => {
6878
+ if (frames.has(neighbourId) || !entries.has(neighbourId)) return;
6879
+ frames.set(neighbourId, {
6880
+ ring,
6881
+ frame: neighbourFrame()
6882
+ });
6883
+ next.push(neighbourId);
6884
+ };
6885
+ for (const childId of hyperbolicChildIds(id, entry.node.children ?? [])) visit(childId, () => composeIsometry(frame, entries.get(childId).toParent));
6886
+ if (entry.parentId !== void 0) visit(entry.parentId, () => composeIsometry(frame, invertIsometry(entry.toParent)));
6887
+ }
6888
+ frontier = next;
6889
+ }
6890
+ return frames;
6891
+ }
6892
+ /** `toParent` hops composed from `id` up to (not including) ancestor `ancestorId`: `id`-local → `ancestorId`-local. */
6893
+ function frameUpTo(entries, id, ancestorId) {
6894
+ let frame = identityIsometry;
6895
+ for (let cursor = id; cursor !== void 0 && cursor !== ancestorId; cursor = entries.get(cursor)?.parentId) frame = composeIsometry(entries.get(cursor).toParent, frame);
6896
+ return frame;
6897
+ }
6898
+ /**
6899
+ * The frame of node `toId` expressed in node `fromId`'s local frame (maps
6900
+ * `toId`-local coordinates to `fromId`-local ones), composed along the tree
6901
+ * path between them via their lowest common ancestor. Used to hand a view
6902
+ * over from one focus to another without passing through root-anchored
6903
+ * coordinates. `undefined` if either id is absent.
6904
+ */
6905
+ function hyperbolicRelativeFrame(entries, fromId, toId) {
6906
+ if (!entries.has(fromId) || !entries.has(toId)) return void 0;
6907
+ const ancestors = /* @__PURE__ */ new Set();
6908
+ for (let cursor = fromId; cursor !== void 0; cursor = entries.get(cursor)?.parentId) ancestors.add(cursor);
6909
+ let lca = toId;
6910
+ while (lca !== void 0 && !ancestors.has(lca)) lca = entries.get(lca)?.parentId;
6911
+ if (lca === void 0) return void 0;
6912
+ return composeIsometry(invertIsometry(frameUpTo(entries, fromId, lca)), frameUpTo(entries, toId, lca));
6913
+ }
6914
+ /**
6915
+ * Resolve `focusedKey` — either a layout id, or (as a convenience, and for
6916
+ * data with globally-unique keys) a plain `item.key`, matching the first
6917
+ * node in depth-first order carrying it.
6918
+ */
6919
+ function resolveFocusId(canonical, focusedKey) {
6920
+ if (focusedKey === void 0) return void 0;
6921
+ if (canonical.has(focusedKey)) return focusedKey;
6922
+ for (const entry of canonical.values()) if (entry.node.item.key === focusedKey) return entry.id;
6923
+ }
6924
+ /**
6925
+ * Compute the visible nodes/edges of a hyperbolic tree view: every
6926
+ * currently-loaded node within `maxRadius` tree-graph steps of the focused
6927
+ * node, projected onto the Poincaré disk with the focus at its center.
6928
+ *
6929
+ * @param root True tree root. `undefined` yields an empty layout.
6930
+ * @param options `focusedKey` (a layout id or plain `item.key`; default the
6931
+ * true root; unknown keys fall back to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
6932
+ * cap on layout recursion depth, default 24), `curvature`,
6933
+ * `siblingSpacing`, `centerOverride` (put this point of the focus's local
6934
+ * frame at the center instead of the focus itself), `view` (a full
6935
+ * focus-local → screen isometry, taking precedence over `centerOverride` —
6936
+ * used for panning and animation, which also rotate).
6937
+ */
6938
+ function computeHyperbolicLayout(root, options = {}) {
6939
+ if (!root) return {
6940
+ nodes: [],
6941
+ edges: []
6942
+ };
6943
+ const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING } = options;
6944
+ const canonical = computeCanonicalPositions(root, maxDepth, curvature, siblingSpacing);
6945
+ const focusedId = resolveFocusId(canonical, options.focusedKey) ?? root.item.key;
6946
+ if (!canonical.has(focusedId)) return {
6947
+ nodes: [],
6948
+ edges: []
6949
+ };
6950
+ const view = options.view ?? viewIsometry(options.centerOverride ?? {
6951
+ x: 0,
6952
+ y: 0
6953
+ });
6954
+ const frames = focusFrames(canonical, focusedId, maxRadius);
6955
+ const rawWeights = [...frames.keys()].map((key) => canonical.get(key).node.item.weight).filter((w) => typeof w === `number`);
6029
6956
  const minWeight = rawWeights.length > 0 ? Math.min(...rawWeights) : void 0;
6030
6957
  const maxWeight = rawWeights.length > 0 ? Math.max(...rawWeights) : void 0;
6031
6958
  const normalizeWeight = (weight) => {
6032
6959
  if (weight === void 0 || minWeight === void 0 || maxWeight === void 0 || maxWeight === minWeight) return .5;
6033
6960
  return (weight - minWeight) / (maxWeight - minWeight);
6034
6961
  };
6035
- const nodes = [...ring.entries()].map(([key, distance]) => {
6036
- const entry = canonical.get(key);
6962
+ const nodes = [...frames.entries()].map(([id, { ring, frame }]) => {
6963
+ const entry = canonical.get(id);
6037
6964
  const weight = entry.node.item.weight;
6038
6965
  return {
6039
6966
  node: entry.node,
6040
- key,
6967
+ key: entry.node.item.key,
6968
+ id,
6041
6969
  depth: entry.depth,
6042
- ring: distance,
6970
+ ring,
6043
6971
  parentKey: entry.parentKey,
6044
- position: mobiusToOrigin(entry.position, projectionCenter),
6972
+ parentId: entry.parentId,
6973
+ position: applyIsometry(composeIsometry(view, frame), {
6974
+ x: 0,
6975
+ y: 0
6976
+ }),
6045
6977
  angularShare: entry.angularShare,
6046
6978
  weight,
6047
6979
  weightNormalized: normalizeWeight(weight)
6048
6980
  };
6049
6981
  }).sort((a, b) => a.ring - b.ring);
6050
- const byKey = new Map(nodes.map((n) => [n.key, n]));
6982
+ const byId = new Map(nodes.map((n) => [n.id, n]));
6051
6983
  const edges = [];
6052
6984
  for (const n of nodes) {
6053
- if (n.parentKey === void 0) continue;
6054
- const parent = byKey.get(n.parentKey);
6985
+ if (n.parentId === void 0) continue;
6986
+ const parent = byId.get(n.parentId);
6055
6987
  if (!parent) continue;
6056
6988
  edges.push({
6057
6989
  fromKey: parent.key,
6058
6990
  toKey: n.key,
6991
+ fromId: parent.id,
6992
+ toId: n.id,
6059
6993
  from: parent.position,
6060
6994
  to: n.position,
6061
6995
  weightNormalized: (parent.weightNormalized + n.weightNormalized) / 2
@@ -6066,18 +7000,477 @@ function computeHyperbolicLayout(root, options = {}) {
6066
7000
  edges
6067
7001
  };
6068
7002
  }
6069
- //#endregion
6070
- //#region src/hyperbolic-tree/hyperbolictree-list.ts
7003
+ //#endregion
7004
+ //#region src/hyperbolic-tree/keyboard.ts
7005
+ const unhandled = {
7006
+ action: { kind: `none` },
7007
+ handled: false
7008
+ };
7009
+ /** Angular distance (radians, 0–π) of a layout node from screen-"up" (−π/2). */
7010
+ function angleDistanceFromUp(n) {
7011
+ const angle = Math.atan2(n.position.y, n.position.x);
7012
+ let diff = Math.abs(angle - -Math.PI / 2);
7013
+ if (diff > Math.PI) diff = 2 * Math.PI - diff;
7014
+ return diff;
7015
+ }
7016
+ /**
7017
+ * Resolve `key` against the focused node (layout id `focusId`):
7018
+ * - `ArrowUp` — the visible child closest to screen-"up"
7019
+ * - `ArrowDown` — the parent
7020
+ * - `ArrowLeft` / `ArrowRight` — previous / next sibling, wrapping
7021
+ * - `Enter` — activate the focus
7022
+ * - `Escape` — back to the root (unhandled when already there)
7023
+ */
7024
+ function resolveKeyboardAction(key, root, focusId, visibleNodes) {
7025
+ const path = findHyperbolicPathById(root, focusId);
7026
+ const focusNode = path?.at(-1);
7027
+ if (!path || !focusNode) return unhandled;
7028
+ const parent = path.length >= 2 ? path[path.length - 2] : void 0;
7029
+ const none = {
7030
+ action: { kind: `none` },
7031
+ handled: true
7032
+ };
7033
+ const focus = (node) => node ? {
7034
+ action: {
7035
+ kind: `focus`,
7036
+ node
7037
+ },
7038
+ handled: true
7039
+ } : none;
7040
+ switch (key) {
7041
+ case `ArrowUp`: {
7042
+ const children = visibleNodes.filter((n) => n.parentId === focusId);
7043
+ if (children.length === 0) return none;
7044
+ return focus(children.reduce((a, b) => angleDistanceFromUp(a) < angleDistanceFromUp(b) ? a : b).node);
7045
+ }
7046
+ case `ArrowDown`: return focus(parent);
7047
+ case `ArrowLeft`:
7048
+ case `ArrowRight`: {
7049
+ const siblings = parent?.children ?? [];
7050
+ const index = siblings.indexOf(focusNode);
7051
+ if (index === -1 || siblings.length <= 1) return none;
7052
+ return focus(siblings[(index + (key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length]);
7053
+ }
7054
+ case `Enter`: return {
7055
+ action: {
7056
+ kind: `activate`,
7057
+ node: focusNode,
7058
+ depth: path.length - 1
7059
+ },
7060
+ handled: true
7061
+ };
7062
+ case `Escape`: return focusNode === root ? unhandled : focus(root);
7063
+ default: return unhandled;
7064
+ }
7065
+ }
7066
+ //#endregion
7067
+ //#region src/hyperbolic-tree/lazy-children.ts
7068
+ function createLazyChildLoader(source, onChange) {
7069
+ const pending = /* @__PURE__ */ new Set();
7070
+ const aborters = /* @__PURE__ */ new Map();
7071
+ const fetchChildren = async (node, depth, signal) => {
7072
+ const { model, loadChildren } = source();
7073
+ if (model) return model.getChildren(node, depth, signal);
7074
+ return [...await collectDataResult(loadChildren({
7075
+ node,
7076
+ depth
7077
+ }, signal), signal)];
7078
+ };
7079
+ const load = async (node, depth) => {
7080
+ pending.add(node);
7081
+ const controller = new AbortController();
7082
+ aborters.set(node, controller);
7083
+ onChange();
7084
+ try {
7085
+ const children = await fetchChildren(node, depth, controller.signal);
7086
+ if (!controller.signal.aborted) node.children = children;
7087
+ } catch {
7088
+ if (!controller.signal.aborted) node.children = [];
7089
+ } finally {
7090
+ pending.delete(node);
7091
+ aborters.delete(node);
7092
+ if (!controller.signal.aborted) onChange();
7093
+ }
7094
+ };
7095
+ return {
7096
+ /** Start loading children for every node in `visible` that has none yet. */
7097
+ ensure(visible) {
7098
+ const { model, loadChildren } = source();
7099
+ if (!model && !loadChildren) return;
7100
+ for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
7101
+ },
7102
+ isPending: (node) => pending.has(node),
7103
+ abortAll() {
7104
+ for (const aborter of aborters.values()) aborter.abort();
7105
+ aborters.clear();
7106
+ pending.clear();
7107
+ }
7108
+ };
7109
+ }
7110
+ //#endregion
7111
+ //#region src/hyperbolic-tree/render-utils.ts
7112
+ /**
7113
+ * The Poincaré disk's conformal factor relative to the origin, `1 − |p|²`:
7114
+ * how much a shape of fixed hyperbolic size shrinks (in Euclidean terms) when
7115
+ * moved from the center to `p`. Sizing nodes by this makes them shrink
7116
+ * exactly as the space does, rather than by an ad-hoc falloff.
7117
+ */
7118
+ function conformalScale(p) {
7119
+ return Math.max(0, 1 - (p.x * p.x + p.y * p.y));
7120
+ }
7121
+ /**
7122
+ * Euclidean distance from each node to its nearest visible neighbour, keyed
7123
+ * by layout id (`Infinity` for a lone node). Used to cap on-screen node size
7124
+ * so crowded nodes never overlap — measured in the *projected* view, so it
7125
+ * stays right whichever node is focused. O(n²), fine for the few hundred
7126
+ * nodes a `maxRadius` window typically holds.
7127
+ */
7128
+ function nearestNeighbourDistances(nodes) {
7129
+ const out = /* @__PURE__ */ new Map();
7130
+ for (const n of nodes) out.set(n.id, Number.POSITIVE_INFINITY);
7131
+ for (let i = 0; i < nodes.length; i++) {
7132
+ const a = nodes[i].position;
7133
+ for (let j = i + 1; j < nodes.length; j++) {
7134
+ const b = nodes[j].position;
7135
+ const d = Math.hypot(a.x - b.x, a.y - b.y);
7136
+ if (d < out.get(nodes[i].id)) out.set(nodes[i].id, d);
7137
+ if (d < out.get(nodes[j].id)) out.set(nodes[j].id, d);
7138
+ }
7139
+ }
7140
+ return out;
7141
+ }
7142
+ /** Weight multiplier for `n`, interpolating between the min/max weight scales. */
7143
+ function weightScale(n, options) {
7144
+ return options.minWeightScale + (options.maxWeightScale - options.minWeightScale) * n.weightNormalized;
7145
+ }
7146
+ /**
7147
+ * A dot's rendered radius: base size × weight × conformal shrink, capped to
7148
+ * under half the distance to its nearest neighbour so crowded siblings keep
7149
+ * a visible gap instead of merging into a blob.
7150
+ */
7151
+ function dotRadius(n, nearest, options) {
7152
+ const radius = options.nodeRadius * conformalScale(n.position) * weightScale(n, options);
7153
+ return Math.max(Math.min(radius, nearest * .45), .003);
7154
+ }
7155
+ /**
7156
+ * Footprint of a `2·halfWidth × 2·halfHeight` rectangle centered on
7157
+ * `center` and rotated by `angle` radians.
7158
+ */
7159
+ function orientedRect(id, center, halfWidth, halfHeight, angle = 0) {
7160
+ const cos = Math.cos(angle);
7161
+ const sin = Math.sin(angle);
7162
+ const corner = (u, v) => ({
7163
+ x: center.x + u * cos - v * sin,
7164
+ y: center.y + u * sin + v * cos
7165
+ });
7166
+ return {
7167
+ id,
7168
+ corners: [
7169
+ corner(-halfWidth, -halfHeight),
7170
+ corner(halfWidth, -halfHeight),
7171
+ corner(halfWidth, halfHeight),
7172
+ corner(-halfWidth, halfHeight)
7173
+ ]
7174
+ };
7175
+ }
7176
+ function polygonArea(points) {
7177
+ let area = 0;
7178
+ for (let i = 0; i < points.length; i++) {
7179
+ const a = points[i];
7180
+ const b = points[(i + 1) % points.length];
7181
+ area += a.x * b.y - b.x * a.y;
7182
+ }
7183
+ return Math.abs(area) / 2;
7184
+ }
7185
+ /** Sutherland–Hodgman: `subject` clipped to convex `clip` (either winding). */
7186
+ function clipPolygon(subject, clip) {
7187
+ const orientation = Math.sign((clip[1].x - clip[0].x) * (clip[2].y - clip[0].y) - (clip[1].y - clip[0].y) * (clip[2].x - clip[0].x)) || 1;
7188
+ let output = [...subject];
7189
+ for (let i = 0; i < clip.length && output.length > 0; i++) {
7190
+ const a = clip[i];
7191
+ const b = clip[(i + 1) % clip.length];
7192
+ const side = (p) => orientation * ((b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x));
7193
+ const input = output;
7194
+ output = [];
7195
+ for (let j = 0; j < input.length; j++) {
7196
+ const current = input[j];
7197
+ const previous = input[(j + input.length - 1) % input.length];
7198
+ const sc = side(current);
7199
+ const sp = side(previous);
7200
+ if (sc >= 0 !== sp >= 0) {
7201
+ const t = sp / (sp - sc);
7202
+ output.push({
7203
+ x: previous.x + (current.x - previous.x) * t,
7204
+ y: previous.y + (current.y - previous.y) * t
7205
+ });
7206
+ }
7207
+ if (sc >= 0) output.push(current);
7208
+ }
7209
+ }
7210
+ return output;
7211
+ }
7212
+ function bounds(points) {
7213
+ const xs = points.map((p) => p.x);
7214
+ const ys = points.map((p) => p.y);
7215
+ return {
7216
+ x0: Math.min(...xs),
7217
+ y0: Math.min(...ys),
7218
+ x1: Math.max(...xs),
7219
+ y1: Math.max(...ys)
7220
+ };
7221
+ }
7222
+ /** Fraction (0–1) of `candidate`'s area covered by `other`. */
7223
+ function overlapFraction(candidate, other) {
7224
+ const a = bounds(candidate.corners);
7225
+ const b = bounds(other.corners);
7226
+ if (a.x1 <= b.x0 || b.x1 <= a.x0 || a.y1 <= b.y0 || b.y1 <= a.y0) return 0;
7227
+ const area = polygonArea(candidate.corners);
7228
+ if (area <= 0) return 1;
7229
+ return polygonArea(clipPolygon(candidate.corners, other.corners)) / area;
7230
+ }
7231
+ /**
7232
+ * Greedy label placement: walk `shapes` in priority order and keep each one
7233
+ * whose overlap with every already-kept shape is at most `maxOverlap` of its
7234
+ * own area. Returns the ids of the kept labels. Checking actual (possibly
7235
+ * rotated) label footprints, not just node positions, is what stops a fan of
7236
+ * same-radius siblings from stacking their text into an unreadable pile.
7237
+ */
7238
+ function cullOverlappingLabels(shapes, maxOverlap = .03) {
7239
+ const kept = [];
7240
+ for (const shape of shapes) if (kept.every((k) => overlapFraction(shape, k) <= maxOverlap)) kept.push(shape);
7241
+ return new Set(kept.map((r) => r.id));
7242
+ }
7243
+ /** `label` cut to at most `maxChars` characters, ending in `…` when cut. `0` or less disables truncation. */
7244
+ function truncateLabel(label, maxChars) {
7245
+ if (maxChars <= 0 || label.length <= maxChars) return label;
7246
+ return `${label.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
7247
+ }
7248
+ let measureContext;
7249
+ const widthCache = /* @__PURE__ */ new Map();
7250
+ /**
7251
+ * Rendered width of `text` at `fontSize` px in `fontFamily`, via an offscreen
7252
+ * canvas (cached). Falls back to a per-character estimate where canvas text
7253
+ * measurement is unavailable (e.g. non-browser test environments).
7254
+ */
7255
+ function measureTextWidth(text, fontSize, fontFamily) {
7256
+ const cacheKey = `${fontFamily}\u0000${text}`;
7257
+ let atUnitSize = widthCache.get(cacheKey);
7258
+ if (atUnitSize === void 0) {
7259
+ if (measureContext === void 0) try {
7260
+ measureContext = typeof document === `undefined` ? null : document.createElement(`canvas`).getContext(`2d`);
7261
+ } catch {
7262
+ measureContext = null;
7263
+ }
7264
+ const reference = 100;
7265
+ let measured;
7266
+ if (measureContext) {
7267
+ measureContext.font = `${reference}px ${fontFamily || `sans-serif`}`;
7268
+ const width = measureContext.measureText(text).width;
7269
+ if (Number.isFinite(width) && width > 0) measured = width / reference;
7270
+ }
7271
+ atUnitSize = measured ?? text.length * .6;
7272
+ widthCache.set(cacheKey, atUnitSize);
7273
+ }
7274
+ return atUnitSize * fontSize;
7275
+ }
7276
+ /**
7277
+ * Smallest scale a box shrinks to, however far from the focus. Pure conformal
7278
+ * scaling (`1 − |z|²`) is geometrically exact but shrinks the second ring to
7279
+ * well under half size, taking labels below legibility long before the
7280
+ * boundary — flooring it keeps nearby rings readable while still reading as
7281
+ * "further away".
7282
+ */
7283
+ const BOX_SCALE_FLOOR = .45;
7284
+ /**
7285
+ * Geometry for a `box`-shaped node. The box is laid out at its focus size and
7286
+ * then uniformly scaled — by the conformal factor, floored at
7287
+ * {@link BOX_SCALE_FLOOR} — so the label is measured just once. Weight
7288
+ * nudges the scale gently (square-rooted), so heavy nodes read as larger
7289
+ * without dwarfing their neighbours' labels.
7290
+ */
7291
+ function boxGeometry(n, options) {
7292
+ const conformal = BOX_SCALE_FLOOR + .55 * conformalScale(n.position);
7293
+ const scale = Math.min(1, conformal * (options.zoom ?? 1)) * Math.sqrt(weightScale(n, options));
7294
+ const text = truncateLabel(n.node.item.label, options.maxChars);
7295
+ const padX = options.fontSize * .5;
7296
+ const height = options.fontSize * 1.55;
7297
+ return {
7298
+ scale,
7299
+ width: measureTextWidth(text, options.fontSize, options.fontFamily) + padX * 2,
7300
+ height,
7301
+ markerSize: options.fontSize * .55,
7302
+ text
7303
+ };
7304
+ }
7305
+ /** Label footprint of a box of `geometry` centered at `center` and rotated by `angle` (render units). */
7306
+ function boxLabelShape(id, center, geometry, angle = 0) {
7307
+ return orientedRect(id, center, geometry.width * geometry.scale / 2, geometry.height * geometry.scale / 2, angle);
7308
+ }
7309
+ //#endregion
7310
+ //#region src/hyperbolic-tree/render-context.ts
7311
+ const HORIZONTAL = {
7312
+ direction: 0,
7313
+ textAngle: 0,
7314
+ flipped: false
7315
+ };
7316
+ /**
7317
+ * Label font metrics. `--hyperbolictree-font-size` is in real screen px;
7318
+ * the SVG's viewBox is fixed, so it's converted to render units using the
7319
+ * on-screen disk size (`extentPx`) and zoom — keeping text legible whatever
7320
+ * size the element is laid out at, and constant-size while zooming
7321
+ * (zooming in spreads crowded labels apart rather than just magnifying the
7322
+ * pile).
7323
+ */
7324
+ function fontMetrics(host, extentPx, zoom) {
7325
+ const style = typeof getComputedStyle === `function` ? getComputedStyle(host) : void 0;
7326
+ const parsed = Number.parseFloat(style?.getPropertyValue(`--hyperbolictree-font-size`) ?? ``);
7327
+ const px = Number.isFinite(parsed) && parsed > 0 ? parsed : 13;
7328
+ return {
7329
+ px,
7330
+ units: px * (1050 / (extentPx > 0 ? extentPx : 420) / (zoom || 1)),
7331
+ family: style?.fontFamily ?? ``
7332
+ };
7333
+ }
7334
+ /** Map of top-level-branch layout id → index among the root's children. */
7335
+ function branchIndices(root) {
7336
+ if (!root) return /* @__PURE__ */ new Map();
7337
+ return new Map(hyperbolicChildIds(root.item.key, root.children ?? []).map((id, i) => [id, i]));
7338
+ }
7339
+ /** Inline `--branch-index`/`--branch-count` declarations for the node/edge with layout id `id` (empty for the root). */
7340
+ function branchStyle(id, branches) {
7341
+ const branch = hyperbolicBranchId(id);
7342
+ const index = branch === void 0 ? void 0 : branches.get(branch);
7343
+ return index === void 0 ? `` : ` --branch-index: ${index}; --branch-count: ${branches.size};`;
7344
+ }
7345
+ function groupSiblings(nodes) {
7346
+ const groups = /* @__PURE__ */ new Map();
7347
+ for (const n of nodes) {
7348
+ const group = groups.get(n.parentId) ?? [];
7349
+ group.push(n);
7350
+ groups.set(n.parentId, group);
7351
+ }
7352
+ return groups;
7353
+ }
7354
+ /**
7355
+ * Label priority: the focus, then selected nodes, then larger (nearer the
7356
+ * center) first.
7357
+ */
7358
+ function byLabelPriority(nodes, sizeOf, isSelected) {
7359
+ const rank = (n) => n.ring === 0 ? 0 : isSelected(n.node) ? 1 : 2;
7360
+ return [...nodes].sort((a, b) => rank(a) - rank(b) || sizeOf(b) - sizeOf(a) || a.ring - b.ring);
7361
+ }
7362
+ /**
7363
+ * Each node's label orientation. The reference neighbour is the one a step
7364
+ * nearer the focus: usually the parent, but for the focus's own ancestors
7365
+ * (which sit *behind* it) the child on the path back to the focus. The focus
7366
+ * itself stays horizontal.
7367
+ */
7368
+ function labelOrientations(nodes, radial) {
7369
+ const out = /* @__PURE__ */ new Map();
7370
+ if (!radial) {
7371
+ for (const n of nodes) out.set(n.id, HORIZONTAL);
7372
+ return out;
7373
+ }
7374
+ const byId = new Map(nodes.map((n) => [n.id, n]));
7375
+ const nearer = /* @__PURE__ */ new Map();
7376
+ for (const n of nodes) {
7377
+ const parent = n.parentId === void 0 ? void 0 : byId.get(n.parentId);
7378
+ if (!parent) continue;
7379
+ if (parent.ring < n.ring) nearer.set(n.id, parent);
7380
+ else if (n.ring < parent.ring) nearer.set(parent.id, n);
7381
+ }
7382
+ for (const n of nodes) {
7383
+ const from = nearer.get(n.id);
7384
+ if (!from || n.ring === 0) {
7385
+ out.set(n.id, HORIZONTAL);
7386
+ continue;
7387
+ }
7388
+ const direction = Math.atan2(n.position.y - from.position.y, n.position.x - from.position.x);
7389
+ const flipped = Math.cos(direction) < 0;
7390
+ out.set(n.id, {
7391
+ direction,
7392
+ textAngle: flipped ? direction + Math.PI : direction,
7393
+ flipped
7394
+ });
7395
+ }
7396
+ return out;
7397
+ }
7398
+ const renderCenter = (n) => ({
7399
+ x: n.position.x * 500,
7400
+ y: n.position.y * 500
7401
+ });
7402
+ function boxLabels(nodes, options, orientations) {
7403
+ const { font } = options;
7404
+ const boxes = new Map(nodes.map((n) => [n.id, boxGeometry(n, {
7405
+ fontSize: font.units,
7406
+ fontFamily: font.family,
7407
+ maxChars: options.boxMaxChars,
7408
+ minWeightScale: options.minWeightScale,
7409
+ maxWeightScale: options.maxWeightScale,
7410
+ zoom: options.zoom
7411
+ })]));
7412
+ return {
7413
+ boxes,
7414
+ labelIds: cullOverlappingLabels(byLabelPriority(nodes.filter((n) => !options.isTickled(n.id) && font.px * boxes.get(n.id).scale >= options.minLabelSize), (n) => boxes.get(n.id).scale, options.isSelected).map((n) => boxLabelShape(n.id, renderCenter(n), boxes.get(n.id), orientations.get(n.id).textAngle)))
7415
+ };
7416
+ }
7417
+ function dotLabels(nodes, options, orientations) {
7418
+ const { font } = options;
7419
+ const nearest = nearestNeighbourDistances(nodes);
7420
+ const radii = new Map(nodes.map((n) => [n.id, dotRadius(n, nearest.get(n.id), options)]));
7421
+ return {
7422
+ radii,
7423
+ labelIds: cullOverlappingLabels(byLabelPriority(nodes.filter((n) => !options.isTickled(n.id) && radii.get(n.id) * options.zoom >= options.minNodeRadius), (n) => radii.get(n.id), options.isSelected).map((n) => {
7424
+ const { direction } = orientations.get(n.id);
7425
+ const halfWidth = measureTextWidth(n.node.item.label, font.units, font.family) / 2;
7426
+ const along = (radii.get(n.id) + .01) * 500 + halfWidth;
7427
+ const c = renderCenter(n);
7428
+ const center = {
7429
+ x: c.x + Math.cos(direction) * along,
7430
+ y: c.y + Math.sin(direction) * along
7431
+ };
7432
+ return orientedRect(n.id, center, halfWidth + font.units * .6, font.units * .6, direction);
7433
+ }))
7434
+ };
7435
+ }
7436
+ /**
7437
+ * Everything the node templates need for one render. Labels are placed
7438
+ * greedily by priority and dropped where they'd overlap; hovered/cursor
7439
+ * nodes are exempt from culling entirely — always labelled — so hovering a
7440
+ * marker reveals it without reshuffling everyone else's labels.
7441
+ */
7442
+ function buildNodeRenderContext(nodes, options) {
7443
+ const orientations = labelOrientations(nodes, options.radialLabels);
7444
+ const shapeData = options.nodeShape === `box` ? {
7445
+ ...boxLabels(nodes, options, orientations),
7446
+ radii: /* @__PURE__ */ new Map()
7447
+ } : {
7448
+ ...dotLabels(nodes, options, orientations),
7449
+ boxes: /* @__PURE__ */ new Map()
7450
+ };
7451
+ const labelIds = new Set(shapeData.labelIds);
7452
+ for (const n of nodes) if (options.isTickled(n.id)) labelIds.add(n.id);
7453
+ return {
7454
+ siblingGroups: groupSiblings(nodes),
7455
+ font: options.font,
7456
+ branches: options.branches,
7457
+ boxes: shapeData.boxes,
7458
+ radii: shapeData.radii,
7459
+ labelIds,
7460
+ orientations
7461
+ };
7462
+ }
6071
7463
  /**
6072
- * The Poincaré disk's math operates in a `-1..1` unit radius, but rendering
6073
- * an SVG `viewBox` at that literal scale (then letting CSS stretch it to a
6074
- * few hundred device pixels) is an extreme, unusual upscale factor that has
6075
- * been observed to trigger a Chromium GPU-rasterization glitch — a `fill:
6076
- * none` shape painting as solid-filled, matching its computed style exactly
6077
- * but not what's on screen. Rendering in a "normal"-magnitude SVG coordinate
6078
- * system (viewBox radius {@link RENDER_SCALE} instead of `1`) avoids it.
7464
+ * Paint order: far-from-focus first so nearer (larger) nodes sit on top;
7465
+ * then labelled over unlabelled; the hovered node above everything.
6079
7466
  */
6080
- const RENDER_SCALE = 500;
7467
+ function paintOrder(nodes, ctx, isTickled) {
7468
+ const layer = (n) => isTickled(n.id) ? 2 : ctx.labelIds.has(n.id) ? 1 : 0;
7469
+ const distance = (n) => n.position.x ** 2 + n.position.y ** 2;
7470
+ return [...nodes].sort((a, b) => layer(a) - layer(b) || distance(b) - distance(a));
7471
+ }
7472
+ //#endregion
7473
+ //#region src/hyperbolic-tree/render-templates.ts
6081
7474
  /**
6082
7475
  * Sanitizes a node key into a token safe to use inside a `part` attribute /
6083
7476
  * `::part()` selector — arbitrary key characters (spaces, quotes, etc.)
@@ -6086,13 +7479,121 @@ const RENDER_SCALE = 500;
6086
7479
  function partToken(key) {
6087
7480
  return key.replace(/[^\w-]/g, `-`);
6088
7481
  }
6089
- /** Angular distance (radians, 0–π) of a layout node from screen-"up" (−π/2). */
6090
- function angleDistanceFromUp(n) {
6091
- const angle = Math.atan2(n.position.y, n.position.x);
6092
- let diff = Math.abs(angle - -Math.PI / 2);
6093
- if (diff > Math.PI) diff = 2 * Math.PI - diff;
6094
- return diff;
7482
+ const preventMouseDownFocus = (e) => e.preventDefault();
7483
+ /** The shared `<g part="node">` wrapper — ARIA, parts, weight/branch vars, tooltip, and pointer handling. */
7484
+ function renderNodeGroup(n, args, transform, content) {
7485
+ const { node, key, id } = n;
7486
+ const { ctx, state, handlers } = args;
7487
+ const selected = state.isSelected(node);
7488
+ const classes = [
7489
+ `node`,
7490
+ selected ? `selected` : ``,
7491
+ state.isTickled(id) ? `tickled` : ``,
7492
+ state.isFiltered(n) ? `filtered` : ``,
7493
+ state.isPending(node) ? `loading` : ``,
7494
+ n.ring === 0 ? `focused` : ``
7495
+ ].filter(Boolean).join(` `);
7496
+ const siblings = ctx.siblingGroups.get(n.parentId) ?? [n];
7497
+ const weightStyle = `--node-weight: ${n.weightNormalized};${n.weight === void 0 ? `` : ` --node-weight-raw: ${n.weight};`}`;
7498
+ return svg`
7499
+ <g
7500
+ class=${classes}
7501
+ data-key=${key}
7502
+ role="treeitem"
7503
+ tabindex="-1"
7504
+ aria-level=${n.depth + 1}
7505
+ aria-selected=${selected}
7506
+ aria-setsize=${siblings.length}
7507
+ aria-posinset=${siblings.findIndex((sib) => sib.id === id) + 1}
7508
+ aria-label=${node.item.label}
7509
+ part="node node-${partToken(key)}"
7510
+ style=${weightStyle + branchStyle(id, ctx.branches)}
7511
+ transform=${transform}
7512
+ @pointerdown=${handlers.onPointerDown}
7513
+ @mousedown=${preventMouseDownFocus}
7514
+ @click=${(e) => handlers.onClick(n, e)}
7515
+ @dblclick=${(e) => handlers.onDblClick(n, e)}
7516
+ @pointerenter=${() => handlers.onPointerEnter(id)}
7517
+ >${state.showTitle(n) ? svg`<title>${node.item.label}</title>` : nothing}${content}</g>
7518
+ `;
7519
+ }
7520
+ const degrees = (radians) => radians * 180 / Math.PI;
7521
+ /** A dot's label: just past the dot, running along its orientation — flipped labels anchor at their end so they still read outward. */
7522
+ function renderDotLabel(n, ctx, scaledRadius) {
7523
+ const { textAngle, flipped } = ctx.orientations.get(n.id);
7524
+ const gap = scaledRadius + 5;
7525
+ return svg`<text
7526
+ class="hyperbolictree-label"
7527
+ part="label"
7528
+ x=${flipped ? -gap : gap}
7529
+ y="0"
7530
+ transform=${textAngle === 0 ? nothing : `rotate(${degrees(textAngle)})`}
7531
+ text-anchor=${flipped ? `end` : `start`}
7532
+ font-size=${ctx.font.units}
7533
+ dominant-baseline="middle"
7534
+ >${n.node.item.label}</text>`;
7535
+ }
7536
+ function renderDotNode(n, args) {
7537
+ const { ctx } = args;
7538
+ const scaledRadius = ctx.radii.get(n.id) * 500;
7539
+ return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})`, svg`
7540
+ <circle class="node-hit" r=${Math.max(scaledRadius, 17.5)} part="hit" />
7541
+ <circle class="node-dot" r=${scaledRadius} part="dot" />
7542
+ ${ctx.labelIds.has(n.id) ? renderDotLabel(n, ctx, scaledRadius) : nothing}
7543
+ `);
7544
+ }
7545
+ function renderBoxNode(n, args) {
7546
+ const { ctx } = args;
7547
+ const box = ctx.boxes.get(n.id);
7548
+ const showText = ctx.labelIds.has(n.id);
7549
+ const width = showText ? box.width : box.markerSize;
7550
+ const height = showText ? box.height : box.markerSize;
7551
+ const { textAngle } = ctx.orientations.get(n.id);
7552
+ const rotate = showText && textAngle !== 0 ? ` rotate(${degrees(textAngle)})` : ``;
7553
+ return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})${rotate} scale(${box.scale})`, svg`
7554
+ <rect
7555
+ class="node-box ${showText ? `` : `marker`}"
7556
+ part="box"
7557
+ x=${-width / 2}
7558
+ y=${-height / 2}
7559
+ width=${width}
7560
+ height=${height}
7561
+ rx=${ctx.font.units * .2}
7562
+ />
7563
+ ${showText ? svg`<text class="hyperbolictree-label box-label" part="label" x="0" y="0" font-size=${ctx.font.units} text-anchor="middle" dominant-baseline="central">${box.text}</text>` : nothing}
7564
+ `);
7565
+ }
7566
+ /** Every visible node, in paint order, in the shape `ctx` was built for. */
7567
+ function renderNodes(nodes, shape, args) {
7568
+ const ordered = paintOrder(nodes, args.ctx, args.state.isTickled);
7569
+ const renderOne = shape === `box` ? renderBoxNode : renderDotNode;
7570
+ return repeat(ordered, (n) => n.id, (n) => renderOne(n, args));
7571
+ }
7572
+ function renderDefaultEdge(edge, branches) {
7573
+ return svg`<path
7574
+ class="edge"
7575
+ d=${geodesicPath(edge.from, edge.to, 500)}
7576
+ part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
7577
+ data-from=${edge.fromKey}
7578
+ data-to=${edge.toKey}
7579
+ style="--edge-weight: ${edge.weightNormalized};${branchStyle(edge.toId, branches)}"
7580
+ />`;
7581
+ }
7582
+ /** Every visible edge — via `edgeRenderer` when given, else the default geodesic arc. */
7583
+ function renderEdges(edges, byId, branches, edgeRenderer) {
7584
+ return repeat(edges, (e) => `${e.fromId}->${e.toId}`, (edge) => {
7585
+ const fromNode = byId.get(edge.fromId)?.node;
7586
+ const toNode = byId.get(edge.toId)?.node;
7587
+ if (edgeRenderer && fromNode && toNode) return svg`<g transform="scale(${500})">${edgeRenderer({
7588
+ edge,
7589
+ fromNode,
7590
+ toNode
7591
+ })}</g>`;
7592
+ return renderDefaultEdge(edge, branches);
7593
+ });
6095
7594
  }
7595
+ //#endregion
7596
+ //#region src/hyperbolic-tree/hyperbolictree-list.ts
6096
7597
  let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitElement {
6097
7598
  constructor(..._args) {
6098
7599
  super(..._args);
@@ -6100,37 +7601,63 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6100
7601
  this.selectionFilter = `all`;
6101
7602
  this.interactionMode = `standard`;
6102
7603
  this.maxRadius = 4;
7604
+ this.nodeShape = `dot`;
7605
+ this.boxMaxChars = 24;
7606
+ this.minLabelSize = 9;
7607
+ this.colorByBranch = false;
6103
7608
  this.nodeRadius = .05;
6104
7609
  this.minNodeRadius = .015;
6105
7610
  this.minWeightScale = .6;
6106
7611
  this.maxWeightScale = 1.6;
6107
7612
  this.zoom = 1;
7613
+ this.disableZoomGestures = false;
7614
+ this.disableRadialLabels = false;
7615
+ this.disableChildPreview = false;
6108
7616
  this.curvature = 1;
7617
+ this.siblingSpacing = DEFAULT_SIBLING_SPACING;
6109
7618
  this.focusAnimationDuration = 400;
6110
7619
  this._selectedNodes = /* @__PURE__ */ new Set();
6111
7620
  this.disableKeyboardNavigation = false;
6112
7621
  this.registry = void 0;
6113
- this._focusKey = void 0;
6114
- this._animCenter = void 0;
6115
- this._panOffset = {
7622
+ this._focusId = void 0;
7623
+ this._viewOverride = void 0;
7624
+ this._focusRotation = 0;
7625
+ this._zoomOffset = {
6116
7626
  x: 0,
6117
7627
  y: 0
6118
7628
  };
7629
+ this._previewId = void 0;
7630
+ this.#preview = createChildPreviewTiming({
7631
+ candidate: (id) => {
7632
+ const nodes = this.#layout().nodes;
7633
+ const n = id === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === id);
7634
+ return n && needsChildPreview(n, nodes, this.#lastLabelIds) ? n.id : void 0;
7635
+ },
7636
+ setPreviewId: (id) => {
7637
+ this._previewId = id;
7638
+ }
7639
+ });
7640
+ this.#lazy = createLazyChildLoader(() => ({
7641
+ model: this.model,
7642
+ loadChildren: this.loadChildren
7643
+ }), () => this.requestUpdate());
7644
+ this.#lastLabelIds = /* @__PURE__ */ new Set();
6119
7645
  this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
6120
7646
  labelSelector: `.hyperbolictree-label`,
6121
7647
  highlightKey: `incr-search-highlight`
6122
7648
  }) });
6123
7649
  this.tickled = new TickledController(this, {
6124
7650
  getItems: () => this.#layout().nodes.map((n) => ({
6125
- id: n.key,
7651
+ id: n.id,
6126
7652
  enabled: true
6127
7653
  })),
6128
7654
  onStateChange: () => {
6129
7655
  this.setAttribute(`data-input-mode`, this.tickled.inputMode);
6130
7656
  },
6131
7657
  onTickle: (id) => {
7658
+ this.#preview.update(id);
6132
7659
  if (id === void 0) return;
6133
- const n = this.#layout().nodes.find((n) => n.key === id);
7660
+ const n = this.#layout().nodes.find((n) => n.id === id);
6134
7661
  if (!n) return;
6135
7662
  this.dispatchEvent(new CustomEvent(`tickle`, {
6136
7663
  detail: {
@@ -6142,8 +7669,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6142
7669
  }));
6143
7670
  }
6144
7671
  });
6145
- this.#pendingKeys = /* @__PURE__ */ new Set();
6146
- this.#aborters = /* @__PURE__ */ new Map();
7672
+ this._hostSize = {
7673
+ width: 0,
7674
+ height: 0
7675
+ };
7676
+ this.#gestures = createGestureHandlers({
7677
+ svgRect: () => this.renderRoot.querySelector(`svg.hyperbolictree`)?.getBoundingClientRect(),
7678
+ takeView: () => {
7679
+ const view = this.#currentView();
7680
+ this.#cancelAnimation();
7681
+ this._viewOverride = view;
7682
+ return view;
7683
+ },
7684
+ setView: (view) => {
7685
+ this._viewOverride = view;
7686
+ },
7687
+ zoomState: () => this.#zoomState(),
7688
+ setZoomState: (state) => {
7689
+ this.zoom = state.zoom;
7690
+ this._zoomOffset = state.offset;
7691
+ },
7692
+ zoomGesturesEnabled: () => !this.disableZoomGestures,
7693
+ hasFocus: () => this.renderRoot.activeElement !== null,
7694
+ focusTree: () => this.#focusTree()
7695
+ });
6147
7696
  }
6148
7697
  static {
6149
7698
  this.shadowRootOptions = {
@@ -6156,37 +7705,55 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6156
7705
  }
6157
7706
  /** The node currently centered on the disk. Pure view-state — `root` is never mutated by focus changes. */
6158
7707
  get focusedNode() {
6159
- if (this._focusKey === void 0) return this.#effectiveRoot;
6160
- return findHyperbolicNode(this.#effectiveRoot, this._focusKey) ?? this.#effectiveRoot;
7708
+ if (this._focusId === void 0) return this.#effectiveRoot;
7709
+ return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
6161
7710
  }
7711
+ #preview;
7712
+ #lazy;
7713
+ /** Labels shown in the last render — what the child preview judges legibility by. */
7714
+ #lastLabelIds;
6162
7715
  #modelUnsubscribe;
6163
- #pendingKeys;
6164
- #aborters;
6165
7716
  #ownedRegistry;
6166
7717
  #unregisterSearchCommands;
6167
7718
  #animFrame;
6168
- #panStart;
7719
+ #resizeObserver;
7720
+ #gestures;
6169
7721
  get #effectiveRoot() {
6170
7722
  return this.model?.root ?? this.root;
6171
7723
  }
6172
7724
  #layout() {
6173
7725
  return computeHyperbolicLayout(this.#effectiveRoot, {
6174
- focusedKey: this._focusKey,
7726
+ focusedKey: this._focusId,
6175
7727
  maxRadius: this.maxRadius,
6176
7728
  curvature: this.curvature,
6177
- centerOverride: this._panOffset.x === 0 && this._panOffset.y === 0 && this._animCenter === void 0 ? void 0 : this.#effectiveCenter()
7729
+ siblingSpacing: this.siblingSpacing,
7730
+ view: this.#currentView()
6178
7731
  });
6179
7732
  }
6180
- #effectiveCenter() {
6181
- const canonical = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature);
6182
- const focusKey = this._focusKey ?? this.#effectiveRoot?.item.key;
6183
- const base = this._animCenter ?? (focusKey === void 0 ? void 0 : canonical.get(focusKey)?.position);
6184
- if (base === void 0) return void 0;
6185
- if (this._panOffset.x === 0 && this._panOffset.y === 0) return base;
6186
- return clampToDisk({
6187
- x: base.x + this._panOffset.x,
6188
- y: base.y + this._panOffset.y
6189
- }, .95);
7733
+ get #rootId() {
7734
+ return this.#effectiveRoot?.item.key;
7735
+ }
7736
+ /** The projection currently on screen, from the focus's local frame. */
7737
+ #currentView() {
7738
+ return this._viewOverride ?? rotationIsometry(this._focusRotation);
7739
+ }
7740
+ /**
7741
+ * The current view re-expressed in `targetId`'s local frame — i.e. the same
7742
+ * picture, but with `targetId` as the reference node. Where the view hands
7743
+ * over on a focus change.
7744
+ */
7745
+ #viewFromFrameOf(targetId, currentId) {
7746
+ const entries = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature, this.siblingSpacing);
7747
+ const relative = currentId === void 0 ? void 0 : hyperbolicRelativeFrame(entries, currentId, targetId);
7748
+ return relative ? composeIsometry(this.#currentView(), relative) : rotationIsometry(this._focusRotation);
7749
+ }
7750
+ /** Layout id of `node` — by identity, falling back to the first node sharing its key. */
7751
+ #idOf(node) {
7752
+ const root = this.#effectiveRoot;
7753
+ const direct = findHyperbolicIdOf(root, node);
7754
+ if (direct !== void 0) return direct;
7755
+ const byKey = findHyperbolicNode(root, node.item.key);
7756
+ return byKey ? findHyperbolicIdOf(root, byKey) : void 0;
6190
7757
  }
6191
7758
  connectedCallback() {
6192
7759
  super.connectedCallback();
@@ -6206,14 +7773,25 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6206
7773
  });
6207
7774
  this.registry = registry;
6208
7775
  this.#syncSearchCommands();
7776
+ if (typeof ResizeObserver === `function`) {
7777
+ this.#resizeObserver = new ResizeObserver(([entry]) => {
7778
+ const { width, height } = entry.contentRect;
7779
+ this._hostSize = {
7780
+ width,
7781
+ height
7782
+ };
7783
+ });
7784
+ this.#resizeObserver.observe(this);
7785
+ }
6209
7786
  }
6210
7787
  disconnectedCallback() {
6211
7788
  super.disconnectedCallback();
7789
+ this.#resizeObserver?.disconnect();
7790
+ this.#resizeObserver = void 0;
6212
7791
  this.#modelUnsubscribe?.();
6213
7792
  this.#modelUnsubscribe = void 0;
6214
- for (const aborter of this.#aborters.values()) aborter.abort();
6215
- this.#aborters.clear();
6216
- this.#pendingKeys.clear();
7793
+ this.#preview.dispose();
7794
+ this.#lazy.abortAll();
6217
7795
  if (this.#animFrame !== void 0) {
6218
7796
  cancelAnimationFrame(this.#animFrame);
6219
7797
  this.#animFrame = void 0;
@@ -6254,58 +7832,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6254
7832
  }
6255
7833
  if (changedProps.has(`interactionMode`)) this.clearSelection();
6256
7834
  if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
6257
- if (this._focusKey !== void 0 && !findHyperbolicNode(this.#effectiveRoot, this._focusKey)) {
6258
- this._focusKey = void 0;
6259
- this._animCenter = void 0;
7835
+ if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
7836
+ this._focusId = void 0;
7837
+ this._viewOverride = void 0;
7838
+ this._focusRotation = 0;
6260
7839
  }
6261
- this.#ensureVisibleChildren();
7840
+ this.#lazy.ensure(this.#layout().nodes);
6262
7841
  }
6263
7842
  #resetViewState() {
6264
7843
  this.#clearSelectionInternal();
6265
7844
  this.tickled.clearTickled();
6266
- this._focusKey = void 0;
6267
- this._animCenter = void 0;
6268
- this._panOffset = {
7845
+ this._focusId = void 0;
7846
+ this._viewOverride = void 0;
7847
+ this._focusRotation = 0;
7848
+ this._zoomOffset = {
6269
7849
  x: 0,
6270
7850
  y: 0
6271
7851
  };
7852
+ this._previewId = void 0;
7853
+ this.#gestures.cancel();
6272
7854
  if (this.#animFrame !== void 0) {
6273
7855
  cancelAnimationFrame(this.#animFrame);
6274
7856
  this.#animFrame = void 0;
6275
7857
  }
6276
7858
  }
6277
- #ensureVisibleChildren() {
6278
- if (!this.model && !this.loadChildren) return;
6279
- for (const n of this.#layout().nodes) {
6280
- const { node } = n;
6281
- if (node.children !== void 0 || this.#pendingKeys.has(node.item.key)) continue;
6282
- this.#loadChildren(node, n.depth);
6283
- }
6284
- }
6285
- async #loadChildren(node, depth) {
6286
- const key = node.item.key;
6287
- this.#pendingKeys.add(key);
6288
- const controller = new AbortController();
6289
- this.#aborters.set(key, controller);
6290
- this.requestUpdate();
6291
- try {
6292
- let children;
6293
- if (this.model) children = await this.model.getChildren(node, depth, controller.signal);
6294
- else children = [...await collectDataResult(this.loadChildren({
6295
- node,
6296
- depth
6297
- }, controller.signal), controller.signal)];
6298
- if (controller.signal.aborted) return;
6299
- node.children = children;
6300
- } catch {
6301
- if (controller.signal.aborted) return;
6302
- node.children = [];
6303
- } finally {
6304
- this.#pendingKeys.delete(key);
6305
- this.#aborters.delete(key);
6306
- if (!controller.signal.aborted) this.requestUpdate();
6307
- }
6308
- }
6309
7859
  #canSelect(node) {
6310
7860
  if (this.selectionFilter === `none`) return false;
6311
7861
  if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
@@ -6381,83 +7931,83 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6381
7931
  * the previous one, for interop with generic `TreeComponent` listeners.
6382
7932
  */
6383
7933
  focusOn(node) {
6384
- const targetKey = node.item.key;
6385
- const currentFocusKey = this._focusKey ?? this.#effectiveRoot?.item.key;
6386
- if (currentFocusKey === targetKey) return;
6387
- const previousNode = currentFocusKey === void 0 ? void 0 : findHyperbolicNode(this.#effectiveRoot, currentFocusKey);
6388
- const canonical = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature);
6389
- const fromPosition = (currentFocusKey === void 0 ? void 0 : canonical.get(currentFocusKey)?.position) ?? {
6390
- x: 0,
6391
- y: 0
6392
- };
6393
- const toPosition = canonical.get(targetKey)?.position ?? {
6394
- x: 0,
6395
- y: 0
6396
- };
6397
- this._focusKey = targetKey;
6398
- this._panOffset = {
6399
- x: 0,
6400
- y: 0
6401
- };
6402
- this.#animateTo(fromPosition, toPosition);
7934
+ const root = this.#effectiveRoot;
7935
+ const targetId = this.#idOf(node);
7936
+ const currentId = this._focusId ?? this.#rootId;
7937
+ if (targetId === void 0 || currentId === targetId) return;
7938
+ const targetNode = findHyperbolicPathById(root, targetId)?.at(-1) ?? node;
7939
+ const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
7940
+ const fromView = this.#viewFromFrameOf(targetId, currentId);
7941
+ this._focusId = targetId;
7942
+ this.#gestures.cancel();
7943
+ this._previewId = void 0;
7944
+ this.#animateTo(fromView);
6403
7945
  this.dispatchEvent(new CustomEvent(`focus`, {
6404
7946
  detail: {
6405
- node,
7947
+ node: targetNode,
6406
7948
  previous: previousNode
6407
7949
  },
6408
7950
  bubbles: true,
6409
7951
  composed: true
6410
7952
  }));
6411
- if (previousNode) {
6412
- if (findHyperbolicNode(previousNode, targetKey)) {
6413
- const depth = (findHyperbolicPath(this.#effectiveRoot, targetKey)?.length ?? 1) - 1;
6414
- this.dispatchEvent(new CustomEvent(`expand`, {
6415
- detail: {
6416
- node,
6417
- depth
6418
- },
6419
- bubbles: true,
6420
- composed: true
6421
- }));
6422
- } else if (findHyperbolicNode(node, previousNode.item.key)) {
6423
- const depth = (findHyperbolicPath(this.#effectiveRoot, previousNode.item.key)?.length ?? 1) - 1;
6424
- this.dispatchEvent(new CustomEvent(`collapse`, {
6425
- detail: {
6426
- node: previousNode,
6427
- depth
6428
- },
6429
- bubbles: true,
6430
- composed: true
6431
- }));
6432
- }
7953
+ if (previousNode && currentId !== void 0) {
7954
+ if (isHyperbolicDescendantId(targetId, currentId)) this.dispatchEvent(new CustomEvent(`expand`, {
7955
+ detail: {
7956
+ node: targetNode,
7957
+ depth: hyperbolicIdDepth(targetId)
7958
+ },
7959
+ bubbles: true,
7960
+ composed: true
7961
+ }));
7962
+ else if (isHyperbolicDescendantId(currentId, targetId)) this.dispatchEvent(new CustomEvent(`collapse`, {
7963
+ detail: {
7964
+ node: previousNode,
7965
+ depth: hyperbolicIdDepth(currentId)
7966
+ },
7967
+ bubbles: true,
7968
+ composed: true
7969
+ }));
6433
7970
  }
6434
7971
  this.requestUpdate();
6435
7972
  }
6436
- #prefersReducedMotion() {
6437
- return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
6438
- }
6439
- #animateTo(from, to) {
7973
+ #cancelAnimation() {
6440
7974
  if (this.#animFrame !== void 0) {
6441
7975
  cancelAnimationFrame(this.#animFrame);
6442
7976
  this.#animFrame = void 0;
6443
7977
  }
6444
- if (this.focusAnimationDuration <= 0 || this.#prefersReducedMotion() || typeof requestAnimationFrame !== `function`) {
6445
- this._animCenter = void 0;
7978
+ }
7979
+ /** Animate from view `from` (in the new focus's frame) to the settled, centered view — see {@link focusTransition}. */
7980
+ #animateTo(from) {
7981
+ this.#cancelAnimation();
7982
+ const { viewAt, settledRotation } = focusTransition(from);
7983
+ this._focusRotation = settledRotation;
7984
+ const fromOffset = this._zoomOffset;
7985
+ if (this.focusAnimationDuration <= 0 || prefersReducedMotion$1() || typeof requestAnimationFrame !== `function`) {
7986
+ this._viewOverride = void 0;
7987
+ this._zoomOffset = {
7988
+ x: 0,
7989
+ y: 0
7990
+ };
6446
7991
  return;
6447
7992
  }
6448
7993
  const start = performance.now();
6449
7994
  const duration = this.focusAnimationDuration;
6450
7995
  const step = (now) => {
6451
7996
  const t = Math.min(1, (now - start) / duration);
6452
- this._animCenter = t >= 1 ? void 0 : geodesicInterpolate(from, to, t);
6453
- this.requestUpdate();
7997
+ const remaining = 1 - easeInOutCubic(t);
7998
+ this._zoomOffset = {
7999
+ x: fromOffset.x * remaining,
8000
+ y: fromOffset.y * remaining
8001
+ };
8002
+ this._viewOverride = t >= 1 ? void 0 : viewAt(t);
6454
8003
  this.#animFrame = t < 1 ? requestAnimationFrame(step) : void 0;
6455
8004
  };
6456
8005
  this.#animFrame = requestAnimationFrame(step);
6457
8006
  }
6458
8007
  #focusAndMaybeSelect(node) {
6459
8008
  this.focusOn(node);
6460
- this.tickled.setTickled(node.item.key);
8009
+ const id = this.#idOf(node);
8010
+ if (id !== void 0) this.tickled.setTickled(id);
6461
8011
  if (this.interactionMode === `implicit`) this.select(node);
6462
8012
  }
6463
8013
  /** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
@@ -6472,7 +8022,8 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6472
8022
  }
6473
8023
  this.focusOn(current);
6474
8024
  this.select(current);
6475
- this.tickled.setTickled(current.item.key);
8025
+ const id = this.#idOf(current);
8026
+ if (id !== void 0) this.tickled.setTickled(id);
6476
8027
  this.requestUpdate();
6477
8028
  return true;
6478
8029
  }
@@ -6480,7 +8031,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6480
8031
  navigateToNode(predicate) {
6481
8032
  const root = this.#effectiveRoot;
6482
8033
  if (!root?.children) return false;
6483
- const keyPath = this.#findKeyPath(root.children, predicate);
8034
+ const keyPath = findHyperbolicKeyPath(root.children, predicate);
6484
8035
  if (!keyPath) return false;
6485
8036
  return this.navigateTo(keyPath);
6486
8037
  }
@@ -6491,22 +8042,13 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6491
8042
  */
6492
8043
  revealKey(key) {
6493
8044
  const node = findHyperbolicNode(this.#effectiveRoot, key);
6494
- if (!node) return false;
6495
- if (!this.#layout().nodes.some((n) => n.key === key)) this.focusOn(node);
6496
- this.tickled.setTickled(key);
8045
+ const id = node ? this.#idOf(node) : void 0;
8046
+ if (!node || id === void 0) return false;
8047
+ if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
8048
+ this.tickled.setTickled(id);
6497
8049
  this.requestUpdate();
6498
8050
  return true;
6499
8051
  }
6500
- #findKeyPath(nodes, predicate, prefix = []) {
6501
- for (const node of nodes) {
6502
- const path = [...prefix, node.item.key];
6503
- if (predicate(node)) return path;
6504
- if (node.children?.length) {
6505
- const hit = this.#findKeyPath(node.children, predicate, path);
6506
- if (hit) return hit;
6507
- }
6508
- }
6509
- }
6510
8052
  /**
6511
8053
  * All nodes currently laid out on the disk (within `maxRadius` of the
6512
8054
  * focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
@@ -6543,7 +8085,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6543
8085
  this.requestUpdate();
6544
8086
  }
6545
8087
  } else if (this.interactionMode !== `manual`) this.select(n.node);
6546
- this.tickled.setTickled(n.key);
8088
+ this.tickled.setTickled(n.id);
6547
8089
  this.focusOn(n.node);
6548
8090
  }
6549
8091
  #handleItemDblClick(n) {
@@ -6565,239 +8107,147 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6565
8107
  }));
6566
8108
  }
6567
8109
  #handleKeyDown(event) {
6568
- if (this.disableKeyboardNavigation) return;
6569
8110
  const root = this.#effectiveRoot;
6570
- const { nodes } = this.#layout();
6571
- if (!root || nodes.length === 0) return;
6572
- const focusNode = findHyperbolicNode(root, this._focusKey ?? root.item.key);
6573
- if (!focusNode) return;
6574
- switch (event.key) {
6575
- case `ArrowUp`: {
6576
- event.preventDefault();
6577
- const children = nodes.filter((n) => n.parentKey === focusNode.item.key);
6578
- if (children.length === 0) return;
6579
- const best = children.reduce((a, b) => angleDistanceFromUp(a) < angleDistanceFromUp(b) ? a : b);
6580
- this.#focusAndMaybeSelect(best.node);
6581
- return;
6582
- }
6583
- case `ArrowDown`: {
6584
- event.preventDefault();
6585
- const path = findHyperbolicPath(root, focusNode.item.key);
6586
- const parent = path && path.length >= 2 ? path[path.length - 2] : void 0;
6587
- if (parent) this.#focusAndMaybeSelect(parent);
6588
- return;
6589
- }
6590
- case `ArrowLeft`:
6591
- case `ArrowRight`: {
6592
- event.preventDefault();
6593
- const path = findHyperbolicPath(root, focusNode.item.key);
6594
- const siblings = (path && path.length >= 2 ? path[path.length - 2] : void 0)?.children ?? (focusNode === root ? [focusNode] : []);
6595
- const index = siblings.findIndex((n) => n.item.key === focusNode.item.key);
6596
- if (index === -1 || siblings.length <= 1) return;
6597
- const next = siblings[(index + (event.key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length];
6598
- this.#focusAndMaybeSelect(next);
6599
- return;
6600
- }
6601
- case `Enter`: {
6602
- event.preventDefault();
6603
- if (this.interactionMode !== `manual`) this.select(focusNode);
6604
- const depth = (findHyperbolicPath(root, focusNode.item.key)?.length ?? 1) - 1;
6605
- this.dispatchEvent(new CustomEvent(`activate`, {
6606
- detail: {
6607
- node: focusNode,
6608
- depth
6609
- },
6610
- bubbles: true,
6611
- composed: true
6612
- }));
6613
- return;
6614
- }
6615
- case `Escape`: if (this._focusKey !== void 0 && this._focusKey !== root.item.key) {
6616
- event.preventDefault();
6617
- this.focusOn(root);
6618
- }
8111
+ if (this.disableKeyboardNavigation || !root) return;
8112
+ if (event.key === `Escape` && this._previewId !== void 0) {
8113
+ event.preventDefault();
8114
+ this._previewId = void 0;
8115
+ return;
8116
+ }
8117
+ const { action, handled } = resolveKeyboardAction(event.key, root, this._focusId ?? root.item.key, this.#layout().nodes);
8118
+ if (handled) event.preventDefault();
8119
+ if (action.kind === `focus`) this.#focusAndMaybeSelect(action.node);
8120
+ else if (action.kind === `activate`) {
8121
+ if (this.interactionMode !== `manual`) this.select(action.node);
8122
+ this.dispatchEvent(new CustomEvent(`activate`, {
8123
+ detail: {
8124
+ node: action.node,
8125
+ depth: action.depth
8126
+ },
8127
+ bubbles: true,
8128
+ composed: true
8129
+ }));
6619
8130
  }
6620
8131
  }
6621
- #isBackgroundTarget(event) {
6622
- const target = event.target;
6623
- return target === event.currentTarget || target?.classList.contains(`disk-boundary`) === true;
6624
- }
6625
- #handleBackgroundPointerDown(event) {
6626
- if (!this.#isBackgroundTarget(event)) return;
6627
- event.currentTarget.setPointerCapture(event.pointerId);
6628
- this.#panStart = {
6629
- pointerId: event.pointerId,
6630
- startClient: {
6631
- x: event.clientX,
6632
- y: event.clientY
6633
- },
6634
- baseOffset: this._panOffset
6635
- };
6636
- }
6637
- #handleBackgroundPointerMove(event) {
6638
- if (!this.#panStart || event.pointerId !== this.#panStart.pointerId) return;
6639
- const rect = event.currentTarget.getBoundingClientRect();
6640
- const scale = 2.1 / (Math.min(rect.width, rect.height) || 1);
6641
- const dx = (event.clientX - this.#panStart.startClient.x) * scale;
6642
- const dy = (event.clientY - this.#panStart.startClient.y) * scale;
6643
- this._panOffset = clampToDisk({
6644
- x: this.#panStart.baseOffset.x - dx,
6645
- y: this.#panStart.baseOffset.y - dy
6646
- }, .9);
6647
- this.requestUpdate();
6648
- }
6649
- #handleBackgroundPointerUp(event) {
6650
- if (this.#panStart?.pointerId === event.pointerId) this.#panStart = void 0;
6651
- }
6652
- #defaultEdgeRenderer(edge) {
6653
- const steps = 8;
6654
- let d = ``;
6655
- for (let i = 0; i <= steps; i++) {
6656
- const p = geodesicInterpolate(edge.from, edge.to, i / steps);
6657
- d += `${i === 0 ? `M` : `L`}${(p.x * RENDER_SCALE).toFixed(2)},${(p.y * RENDER_SCALE).toFixed(2)} `;
6658
- }
6659
- return svg`<path
6660
- class="edge"
6661
- d=${d}
6662
- part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
6663
- data-from=${edge.fromKey}
6664
- data-to=${edge.toKey}
6665
- style="--edge-weight: ${edge.weightNormalized}"
6666
- />`;
6667
- }
6668
- #renderEdge(edge, byKey) {
6669
- if (this.edgeRenderer) {
6670
- const fromNode = byKey.get(edge.fromKey)?.node;
6671
- const toNode = byKey.get(edge.toKey)?.node;
6672
- if (fromNode && toNode) return svg`<g transform="scale(${RENDER_SCALE})">${this.edgeRenderer({
6673
- edge,
6674
- fromNode,
6675
- toNode
6676
- })}</g>`;
6677
- }
6678
- return this.#defaultEdgeRenderer(edge);
8132
+ #zoomState() {
8133
+ return clampZoomState({
8134
+ zoom: this.zoom,
8135
+ offset: this._zoomOffset
8136
+ });
6679
8137
  }
6680
8138
  /**
6681
- * A node's rendered dot radius: shrinks with distance from focus (existing
6682
- * behaviour), but also caps itself to the angular room {@link
6683
- * HyperbolicLayoutNode.angularShare} actually gives it at its radius —
6684
- * without this, a cluster of siblings squeezed into a narrow wedge (a
6685
- * bushy ancestor elsewhere in the tree, or many siblings at this level)
6686
- * all render at the same distance-only size and physically overlap into
6687
- * an unreadable blob, however small {@link minNodeRadius} suppresses their
6688
- * labels. `angularShare * distance` approximates the Euclidean arc length
6689
- * available to the node; capping the diameter to a fraction of that keeps
6690
- * a visible gap between crowded neighbours.
8139
+ * Any pointer interaction (a node press, or a pan) moves keyboard focus to
8140
+ * the tree, so the user can carry straight on with the arrow keys.
8141
+ * Focuses the stable `<svg role="tree">` — not the node, which paint-order
8142
+ * re-sorting may move in the DOM (dropping its focus) — without scrolling.
6691
8143
  */
6692
- #nodeRenderRadius(n) {
6693
- const distance = Math.hypot(n.position.x, n.position.y);
6694
- const scale = Math.max(.2, 1 - distance);
6695
- const weightScale = this.minWeightScale + (this.maxWeightScale - this.minWeightScale) * n.weightNormalized;
6696
- const radius = this.nodeRadius * scale * weightScale;
6697
- if (distance < 1e-6) return radius;
6698
- const arcLength = n.angularShare * distance;
6699
- return Math.min(radius, Math.max(arcLength * .35, .003));
8144
+ #focusTree() {
8145
+ this.renderRoot.querySelector(`svg.hyperbolictree`)?.focus({ preventScroll: true });
6700
8146
  }
6701
- /**
6702
- * Which nodes get a rendered label, greedily thinned to avoid overlap: a
6703
- * per-node radius threshold alone ({@link minNodeRadius}) still lets many
6704
- * same-sized nodes crowded into a small angular wedge all show labels that
6705
- * visually collide. Nodes are considered in priority order (closer ring
6706
- * first, then larger on-screen radius) and a label is kept only if its
6707
- * position isn't too close to an already-kept one.
6708
- */
6709
- #computeLabelKeys(nodes) {
6710
- const minSpacing = .07;
6711
- const prioritised = [...nodes.filter((n) => this.#nodeRenderRadius(n) >= this.minNodeRadius)].sort((a, b) => {
6712
- if (a.ring !== b.ring) return a.ring - b.ring;
6713
- return Math.hypot(a.position.x, a.position.y) - Math.hypot(b.position.x, b.position.y);
8147
+ #handleNodePointerDown(event) {
8148
+ event.stopPropagation();
8149
+ this.#focusTree();
8150
+ }
8151
+ #pickPreviewChild(child) {
8152
+ this.#preview.hide();
8153
+ if (this.interactionMode !== `manual`) this.select(child);
8154
+ const id = this.#idOf(child);
8155
+ if (id !== void 0) this.tickled.setTickled(id);
8156
+ this.focusOn(child);
8157
+ this.#focusTree();
8158
+ }
8159
+ #previewFor(nodes) {
8160
+ const n = this._previewId === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === this._previewId);
8161
+ if (!n) return void 0;
8162
+ const { width, height } = this._hostSize;
8163
+ const anchor = diskToHostPx(n.position, this.#zoomState(), width || 420, height || 420);
8164
+ return {
8165
+ node: n.node,
8166
+ anchor,
8167
+ above: anchor.y > (height || 420) / 2
8168
+ };
8169
+ }
8170
+ #nodeContext(nodes, branches) {
8171
+ return buildNodeRenderContext(nodes, {
8172
+ nodeShape: this.nodeShape,
8173
+ radialLabels: !this.disableRadialLabels,
8174
+ zoom: this.#zoomState().zoom,
8175
+ font: fontMetrics(this, Math.min(this._hostSize.width, this._hostSize.height), this.zoom),
8176
+ branches,
8177
+ boxMaxChars: this.boxMaxChars,
8178
+ minLabelSize: this.minLabelSize,
8179
+ nodeRadius: this.nodeRadius,
8180
+ minNodeRadius: this.minNodeRadius,
8181
+ minWeightScale: this.minWeightScale,
8182
+ maxWeightScale: this.maxWeightScale,
8183
+ isSelected: (node) => this._selectedNodes.has(node),
8184
+ isTickled: (id) => this.tickled.isTickled(id)
6714
8185
  });
6715
- const kept = [];
6716
- const keys = /* @__PURE__ */ new Set();
6717
- for (const n of prioritised) if (!kept.some((k) => Math.hypot(k.position.x - n.position.x, k.position.y - n.position.y) < minSpacing)) {
6718
- kept.push(n);
6719
- keys.add(n.key);
6720
- }
6721
- return keys;
6722
8186
  }
6723
- #renderNode(n, siblingGroups, labelKeys) {
6724
- const { node, key, position } = n;
6725
- const selected = this._selectedNodes.has(node);
6726
- const tickled = this.tickled.isTickled(key);
6727
- const filtered = !this.#matchesFilter(node, n.depth);
6728
- const pending = this.#pendingKeys.has(key);
6729
- const isFocused = n.ring === 0;
6730
- const radius = this.#nodeRenderRadius(n);
6731
- const showLabel = labelKeys.has(key);
6732
- const siblings = siblingGroups.get(n.parentKey) ?? [n];
6733
- const index = siblings.findIndex((s) => s.key === key);
6734
- const scaledRadius = radius * RENDER_SCALE;
6735
- const weightStyle = `--node-weight: ${n.weightNormalized};${n.weight === void 0 ? `` : ` --node-weight-raw: ${n.weight};`}`;
6736
- return svg`
6737
- <g
6738
- class="node ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${filtered ? `filtered` : ``} ${pending ? `loading` : ``} ${isFocused ? `focused` : ``}"
6739
- data-key=${key}
6740
- role="treeitem"
6741
- tabindex="-1"
6742
- aria-level=${n.depth + 1}
6743
- aria-selected=${selected}
6744
- aria-setsize=${siblings.length}
6745
- aria-posinset=${index + 1}
6746
- aria-label=${node.item.label}
6747
- part="node node-${partToken(key)}"
6748
- style=${weightStyle}
6749
- transform="translate(${position.x * RENDER_SCALE} ${position.y * RENDER_SCALE})"
6750
- @pointerdown=${(e) => e.stopPropagation()}
6751
- @click=${(e) => {
6752
- e.stopPropagation();
6753
- this.#handleItemClick(n, e);
6754
- }}
6755
- @dblclick=${(e) => {
6756
- e.stopPropagation();
6757
- this.#handleItemDblClick(n);
6758
- }}
6759
- @pointerenter=${() => this.tickled.handlePointerEnter(key)}
6760
- >
6761
- <circle class="node-hit" r=${Math.max(scaledRadius, .035 * RENDER_SCALE)} part="hit" />
6762
- <circle class="node-dot" r=${scaledRadius} part="dot" />
6763
- ${showLabel ? svg`<text class="hyperbolictree-label" part="label" x=${scaledRadius + .01 * RENDER_SCALE} y="0" dominant-baseline="middle">${node.item.label}</text>` : nothing}
6764
- </g>
6765
- `;
8187
+ #renderNodes(nodes, ctx) {
8188
+ return renderNodes(nodes, this.nodeShape, {
8189
+ ctx,
8190
+ state: {
8191
+ isSelected: (node) => this._selectedNodes.has(node),
8192
+ isTickled: (id) => this.tickled.isTickled(id),
8193
+ showTitle: (n) => this.disableChildPreview || !n.node.children?.length,
8194
+ isFiltered: (n) => !this.#matchesFilter(n.node, n.depth),
8195
+ isPending: (node) => this.#lazy.isPending(node)
8196
+ },
8197
+ handlers: {
8198
+ onPointerDown: (e) => this.#handleNodePointerDown(e),
8199
+ onClick: (n, e) => {
8200
+ e.stopPropagation();
8201
+ this.#handleItemClick(n, e);
8202
+ },
8203
+ onDblClick: (n, e) => {
8204
+ e.stopPropagation();
8205
+ this.#handleItemDblClick(n);
8206
+ },
8207
+ onPointerEnter: (id) => this.tickled.handlePointerEnter(id)
8208
+ }
8209
+ });
6766
8210
  }
6767
8211
  render() {
6768
8212
  const { nodes, edges } = this.#layout();
6769
- const byKey = new Map(nodes.map((n) => [n.key, n]));
6770
- const siblingGroups = /* @__PURE__ */ new Map();
6771
- for (const n of nodes) {
6772
- const group = siblingGroups.get(n.parentKey) ?? [];
6773
- group.push(n);
6774
- siblingGroups.set(n.parentKey, group);
6775
- }
6776
- const labelKeys = this.#computeLabelKeys(nodes);
8213
+ const byId = new Map(nodes.map((n) => [n.id, n]));
8214
+ const branches = branchIndices(this.#effectiveRoot);
8215
+ const ctx = this.#nodeContext(nodes, branches);
8216
+ this.#lastLabelIds = ctx.labelIds;
8217
+ const { zoom, offset } = this.#zoomState();
8218
+ const pan = offset.x === 0 && offset.y === 0 ? `` : `translate(${offset.x * 500} ${offset.y * 500}) `;
6777
8219
  return html`
6778
8220
  <svg
6779
8221
  class="hyperbolictree"
6780
- viewBox="${-1.05 * RENDER_SCALE} ${-1.05 * RENDER_SCALE} ${2.1 * RENDER_SCALE} ${2.1 * RENDER_SCALE}"
8222
+ viewBox="${-525} ${-525} ${1050} ${1050}"
6781
8223
  preserveAspectRatio="xMidYMid meet"
6782
8224
  role="tree"
6783
8225
  tabindex="0"
6784
8226
  data-input-mode=${this.tickled.inputMode}
8227
+ data-node-shape=${this.nodeShape}
6785
8228
  @keydown=${(e) => this.#handleKeyDown(e)}
6786
- @pointerdown=${(e) => this.#handleBackgroundPointerDown(e)}
6787
- @pointermove=${(e) => this.#handleBackgroundPointerMove(e)}
6788
- @pointerup=${(e) => this.#handleBackgroundPointerUp(e)}
8229
+ @pointerdown=${(e) => this.#gestures.onPointerDown(e)}
8230
+ @pointermove=${(e) => this.#gestures.onPointerMove(e)}
8231
+ @pointerup=${(e) => this.#gestures.onPointerUp(e)}
8232
+ @pointercancel=${(e) => this.#gestures.onPointerUp(e)}
8233
+ @wheel=${(e) => this.#gestures.onWheel(e)}
6789
8234
  @pointerleave=${() => this.tickled.clearTickled()}
6790
8235
  >
6791
- <g class="zoom" transform="scale(${this.zoom})">
6792
- <circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${RENDER_SCALE} />
8236
+ <g class="zoom" transform="${pan}scale(${zoom})">
8237
+ <circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${500} />
6793
8238
  <g class="edges" part="edges">
6794
- ${repeat(edges, (e) => `${e.fromKey}->${e.toKey}`, (e) => this.#renderEdge(e, byKey))}
8239
+ ${renderEdges(edges, byId, branches, this.edgeRenderer)}
6795
8240
  </g>
6796
8241
  <g class="nodes" part="nodes">
6797
- ${repeat(nodes, (n) => n.key, (n) => this.#renderNode(n, siblingGroups, labelKeys))}
8242
+ ${this.#renderNodes(nodes, ctx)}
6798
8243
  </g>
6799
8244
  </g>
6800
8245
  </svg>
8246
+ ${renderChildPreview(this.#previewFor(nodes), {
8247
+ onPointerEnter: () => this.#preview.pointerEnter(),
8248
+ onPointerLeave: () => this.#preview.pointerLeave(this.tickled.tickledId),
8249
+ onPick: (child) => this.#pickPreviewChild(child)
8250
+ })}
6801
8251
  ${this.search.renderField()}
6802
8252
  `;
6803
8253
  }
@@ -6805,102 +8255,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
6805
8255
  this.styles = [
6806
8256
  themeFallbacks,
6807
8257
  listSearchStyles,
6808
- css`
6809
- :host {
6810
- --hyperbolictree-node-color: var(--surface-4);
6811
- --hyperbolictree-node-text: var(--surface-3-text);
6812
- --hyperbolictree-edge-color: var(--surface-6);
6813
- --hyperbolictree-boundary-color: var(--border-subtle);
6814
- --hyperbolictree-font-size: 25px;
6815
-
6816
- display: block;
6817
- overflow: hidden;
6818
- outline: none;
6819
- overscroll-behavior: contain;
6820
- }
6821
-
6822
- /*
6823
- * delegatesFocus means the host itself reports as focused whenever
6824
- * the shadow DOM's focused descendant (the svg) is — without this,
6825
- * the host also picks up the browser's own default focus ring/fill
6826
- * sized to the whole box.
6827
- */
6828
- :host(:focus),
6829
- :host(:focus-visible) {
6830
- outline: none;
6831
- }
6832
-
6833
- .hyperbolictree {
6834
- display: block;
6835
- width: 100%;
6836
- height: 100%;
6837
- outline: none;
6838
- touch-action: none;
6839
- overscroll-behavior: contain;
6840
- }
6841
-
6842
- .disk-boundary {
6843
- fill: none;
6844
- stroke: var(--hyperbolictree-boundary-color);
6845
- stroke-width: 1;
6846
- }
6847
-
6848
- .edge {
6849
- fill: none;
6850
- stroke: var(--hyperbolictree-edge-color);
6851
- /* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
6852
- stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
6853
- }
6854
-
6855
- .node {
6856
- cursor: pointer;
6857
- }
6858
-
6859
- .node-hit {
6860
- fill: transparent;
6861
- pointer-events: all;
6862
- }
6863
-
6864
- .node-dot {
6865
- /* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
6866
- fill: color-mix(in oklab, var(--hyperbolictree-node-color), var(--accent) calc(var(--node-weight, 0.5) * 50%));
6867
- stroke: var(--surface-2);
6868
- stroke-width: 1;
6869
- pointer-events: none;
6870
- }
6871
-
6872
- .node.tickled .node-dot {
6873
- fill: var(--item-bg-tickled);
6874
- }
6875
-
6876
- .node.selected .node-dot {
6877
- fill: var(--item-bg-selected);
6878
- }
6879
-
6880
- .node.selected.tickled .node-dot {
6881
- fill: var(--item-bg-selected-tickled);
6882
- }
6883
-
6884
- .node.focused .node-dot {
6885
- stroke: var(--accent);
6886
- stroke-width: 3;
6887
- }
6888
-
6889
- .node.filtered {
6890
- opacity: 0.3;
6891
- }
6892
-
6893
- .node.loading .node-dot {
6894
- opacity: 0.6;
6895
- }
6896
-
6897
- .hyperbolictree-label {
6898
- font-size: var(--hyperbolictree-font-size);
6899
- fill: var(--hyperbolictree-node-text);
6900
- cursor: pointer;
6901
- user-select: none;
6902
- }
6903
- `
8258
+ hyperbolicTreeStyles
6904
8259
  ];
6905
8260
  }
6906
8261
  };
@@ -6923,6 +8278,24 @@ __decorate([property({
6923
8278
  type: Number,
6924
8279
  attribute: `max-radius`
6925
8280
  })], HyperbolicTreeListElement.prototype, "maxRadius", void 0);
8281
+ __decorate([property({
8282
+ type: String,
8283
+ attribute: `node-shape`,
8284
+ reflect: true
8285
+ })], HyperbolicTreeListElement.prototype, "nodeShape", void 0);
8286
+ __decorate([property({
8287
+ type: Number,
8288
+ attribute: `box-max-chars`
8289
+ })], HyperbolicTreeListElement.prototype, "boxMaxChars", void 0);
8290
+ __decorate([property({
8291
+ type: Number,
8292
+ attribute: `min-label-size`
8293
+ })], HyperbolicTreeListElement.prototype, "minLabelSize", void 0);
8294
+ __decorate([property({
8295
+ type: Boolean,
8296
+ attribute: `color-by-branch`,
8297
+ reflect: true
8298
+ })], HyperbolicTreeListElement.prototype, "colorByBranch", void 0);
6926
8299
  __decorate([property({
6927
8300
  type: Number,
6928
8301
  attribute: `node-radius`
@@ -6940,7 +8313,23 @@ __decorate([property({
6940
8313
  attribute: `max-weight-scale`
6941
8314
  })], HyperbolicTreeListElement.prototype, "maxWeightScale", void 0);
6942
8315
  __decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "zoom", void 0);
8316
+ __decorate([property({
8317
+ type: Boolean,
8318
+ attribute: `disable-zoom-gestures`
8319
+ })], HyperbolicTreeListElement.prototype, "disableZoomGestures", void 0);
8320
+ __decorate([property({
8321
+ type: Boolean,
8322
+ attribute: `disable-radial-labels`
8323
+ })], HyperbolicTreeListElement.prototype, "disableRadialLabels", void 0);
8324
+ __decorate([property({
8325
+ type: Boolean,
8326
+ attribute: `disable-child-preview`
8327
+ })], HyperbolicTreeListElement.prototype, "disableChildPreview", void 0);
6943
8328
  __decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "curvature", void 0);
8329
+ __decorate([property({
8330
+ type: Number,
8331
+ attribute: `sibling-spacing`
8332
+ })], HyperbolicTreeListElement.prototype, "siblingSpacing", void 0);
6944
8333
  __decorate([property({
6945
8334
  type: Number,
6946
8335
  attribute: `focus-animation-duration`
@@ -6959,9 +8348,12 @@ __decorate([property({
6959
8348
  attribute: `search-modes`,
6960
8349
  reflect: true
6961
8350
  })], HyperbolicTreeListElement.prototype, "searchModes", void 0);
6962
- __decorate([state()], HyperbolicTreeListElement.prototype, "_focusKey", void 0);
6963
- __decorate([state()], HyperbolicTreeListElement.prototype, "_animCenter", void 0);
6964
- __decorate([state()], HyperbolicTreeListElement.prototype, "_panOffset", void 0);
8351
+ __decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
8352
+ __decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
8353
+ __decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
8354
+ __decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
8355
+ __decorate([state()], HyperbolicTreeListElement.prototype, "_previewId", void 0);
8356
+ __decorate([state()], HyperbolicTreeListElement.prototype, "_hostSize", void 0);
6965
8357
  HyperbolicTreeListElement = __decorate([safeCustomElement(`ixfx-hyperbolictree-list`)], HyperbolicTreeListElement);
6966
8358
  //#endregion
6967
8359
  //#region src/icicle-tree/layout.ts
@@ -14356,6 +15748,6 @@ function init() {
14356
15748
  console.log(`Init`);
14357
15749
  }
14358
15750
  //#endregion
14359
- export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicNode, findHyperbolicPath, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicDistanceForEuclideanRadius, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, walk, wrapRoot };
15751
+ export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, decomposeViewIsometry, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, rotationIsometry, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, wrapRoot };
14360
15752
 
14361
15753
  //# sourceMappingURL=index.js.map