@ixfx/components 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.d.ts +444 -120
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2293 -553
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.d.ts +1 -1
- package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
- package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/{button-CTL45aBl.d.ts → button-BYuf72aR.d.ts} +2 -2
- package/dist/{button-CTL45aBl.d.ts.map → button-BYuf72aR.d.ts.map} +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/crumbs.d.ts +3 -3
- package/dist/crumbs.js +1 -1
- package/dist/{data-grid-BWrtE8H9.d.ts → data-grid-CPsJBnL4.d.ts} +48 -6
- package/dist/data-grid-CPsJBnL4.d.ts.map +1 -0
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +67 -3
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-DxQZkK-C.d.ts → detail-list-CmdCxddo.d.ts} +22 -5
- package/dist/detail-list-CmdCxddo.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -3
- package/dist/detail-list.js +67 -4
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-k89JpiNy.js → grid-list-3fGhdaFA.js} +92 -7
- package/dist/grid-list-3fGhdaFA.js.map +1 -0
- package/dist/{grid-list-ChgYpUUq.d.ts → grid-list-D6AfWfwn.d.ts} +27 -4
- package/dist/grid-list-D6AfWfwn.d.ts.map +1 -0
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +2 -2
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts.map → grouped-item-lister-DETte0Dh.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
- package/dist/icon-CDGprSdE.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -2
- package/dist/incr-search.d.ts +1 -1
- package/dist/{index-CHbuPyLL.d.ts → index-CMsmZ6Or.d.ts} +4 -4
- package/dist/{index-CHbuPyLL.d.ts.map → index-CMsmZ6Or.d.ts.map} +1 -1
- package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +2 -2
- package/dist/{index-garxxqgV.d.ts.map → index-CYndvTWH.d.ts.map} +1 -1
- package/dist/{index-PjiaGpA5.d.ts → index-CnyoNA6m.d.ts} +4 -4
- package/dist/{index-PjiaGpA5.d.ts.map → index-CnyoNA6m.d.ts.map} +1 -1
- package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
- package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
- package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
- package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
- package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
- package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
- package/dist/index.d.ts +337 -140
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2054 -647
- package/dist/index.js.map +1 -1
- package/dist/item-appearance-BjcMyauw.js +31 -0
- package/dist/item-appearance-BjcMyauw.js.map +1 -0
- package/dist/{labelled-input-base-hoKGwyKT.js → labelled-input-base-BFa4xlYV.js} +11 -8
- package/dist/labelled-input-base-BFa4xlYV.js.map +1 -0
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
- package/dist/labelled-radial-input.d.ts +2 -2
- package/dist/labelled-radial-input.d.ts.map +1 -1
- package/dist/labelled-radial-input.js +1 -1
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-range-input.js +1 -1
- package/dist/{labelled-slider-input-CF2RtzgG.js → labelled-slider-input-FdCRi6st.js} +2 -2
- package/dist/{labelled-slider-input-CF2RtzgG.js.map → labelled-slider-input-FdCRi6st.js.map} +1 -1
- package/dist/labelled-slider-input.d.ts +1 -1
- package/dist/labelled-slider-input.js +1 -1
- package/dist/{list-native-drag-controller-B9reY4uM.d.ts → list-native-drag-controller-CllsHLxw.d.ts} +2 -2
- package/dist/{list-native-drag-controller-B9reY4uM.d.ts.map → list-native-drag-controller-CllsHLxw.d.ts.map} +1 -1
- package/dist/{menu-B2Qv3Oiz.js → menu-BHT6HRND.js} +2 -2
- package/dist/{menu-B2Qv3Oiz.js.map → menu-BHT6HRND.js.map} +1 -1
- package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-DT7bKAIw.d.ts} +4 -2
- package/dist/menu-item-DT7bKAIw.d.ts.map +1 -0
- package/dist/{menu-item-Woc8jJc1.js → menu-item-JcpHCyF0.js} +30 -6
- package/dist/menu-item-JcpHCyF0.js.map +1 -0
- package/dist/menu.d.ts +2 -2
- package/dist/menu.js +2 -2
- package/dist/miller.d.ts +3 -3
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +18 -2
- package/dist/miller.js.map +1 -1
- package/dist/panel.d.ts +1 -1
- package/dist/range-input.js +8 -2
- package/dist/range-input.js.map +1 -1
- package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
- package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
- package/dist/split-layout.d.ts +1 -1
- package/dist/{tab-list-v8mKX7Fu.d.ts → tab-list-pqfKaVAE.d.ts} +2 -2
- package/dist/{tab-list-v8mKX7Fu.d.ts.map → tab-list-pqfKaVAE.d.ts.map} +1 -1
- package/dist/tabs.d.ts +1 -1
- package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +2 -2
- package/dist/{titlebar-B5zSTZnF.d.ts.map → titlebar-Tqu-VvMU.d.ts.map} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/{tree-component-CMGeqyjZ.d.ts → tree-component-DukLqesg.d.ts} +2 -2
- package/dist/{tree-component-CMGeqyjZ.d.ts.map → tree-component-DukLqesg.d.ts.map} +1 -1
- package/dist/tree.d.ts +4 -4
- package/dist/{types-3Jfcaxvo.d.ts → types-D-kYF16b.d.ts} +21 -1
- package/dist/types-D-kYF16b.d.ts.map +1 -0
- package/dist/{vertical-list-R25WqAVO.d.ts → vertical-list-DTp-UIUE.d.ts} +26 -3
- package/dist/vertical-list-DTp-UIUE.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +2 -2
- package/dist/vertical-list.js +53 -4
- package/dist/vertical-list.js.map +1 -1
- package/docs-user/data-grid.md +51 -1
- package/docs-user/detail-list.md +46 -0
- package/docs-user/grid-list.md +49 -2
- package/docs-user/hyperbolic-tree.md +98 -18
- package/docs-user/index.json +2 -2
- package/docs-user/miller.md +52 -7
- package/docs-user/multicol-list.md +1 -0
- package/docs-user/vertical-list.md +63 -0
- package/package.json +1 -1
- package/dist/data-grid-BWrtE8H9.d.ts.map +0 -1
- package/dist/detail-list-DxQZkK-C.d.ts.map +0 -1
- package/dist/grid-list-ChgYpUUq.d.ts.map +0 -1
- package/dist/grid-list-k89JpiNy.js.map +0 -1
- package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
- package/dist/labelled-input-base-hoKGwyKT.js.map +0 -1
- package/dist/menu-item-Woc8jJc1.js.map +0 -1
- package/dist/menu-item-ZmkXNjek.d.ts.map +0 -1
- package/dist/types-3Jfcaxvo.d.ts.map +0 -1
- package/dist/vertical-list-R25WqAVO.d.ts.map +0 -1
package/bundle/index.js
CHANGED
|
@@ -7245,6 +7245,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7245
7245
|
this._isOpen = false;
|
|
7246
7246
|
this._hasMenu = false;
|
|
7247
7247
|
this._hasTriggerSlot = false;
|
|
7248
|
+
this._hasContextTarget = false;
|
|
7248
7249
|
this._anchorName = `--mt-${_MenuTrigger._count++}`;
|
|
7249
7250
|
this._clickListener = this._handleClickOutside.bind(this);
|
|
7250
7251
|
this._handlePopoverToggle = (e) => {
|
|
@@ -7297,11 +7298,14 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7297
7298
|
this._propagateContext();
|
|
7298
7299
|
}
|
|
7299
7300
|
}
|
|
7301
|
+
const contextSlot = this.shadowRoot?.querySelector(`slot[name="context-target"]`);
|
|
7302
|
+
if (contextSlot) this._hasContextTarget = contextSlot.assignedElements({ flatten: true }).length > 0;
|
|
7300
7303
|
this._updatePosition();
|
|
7301
7304
|
});
|
|
7302
7305
|
}
|
|
7303
7306
|
updated(changedProperties) {
|
|
7304
7307
|
if (changedProperties.has(`registry`) || changedProperties.has(`keyboardManager`)) this._propagateContext();
|
|
7308
|
+
if (changedProperties.has(`mode`)) this._updatePosition();
|
|
7305
7309
|
}
|
|
7306
7310
|
_updatePosition() {
|
|
7307
7311
|
const pos = {
|
|
@@ -7348,6 +7352,10 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7348
7352
|
const slot = e.target;
|
|
7349
7353
|
this._hasTriggerSlot = slot.assignedElements({ flatten: true }).length > 0;
|
|
7350
7354
|
}
|
|
7355
|
+
_handleContextTargetSlotChange(e) {
|
|
7356
|
+
const slot = e.target;
|
|
7357
|
+
this._hasContextTarget = slot.assignedElements({ flatten: true }).length > 0;
|
|
7358
|
+
}
|
|
7351
7359
|
get isOpen() {
|
|
7352
7360
|
return this._isOpen;
|
|
7353
7361
|
}
|
|
@@ -7386,6 +7394,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7386
7394
|
document.removeEventListener(`click`, this._clickListener);
|
|
7387
7395
|
}
|
|
7388
7396
|
_handleTriggerClick(e) {
|
|
7397
|
+
if (this.mode === `context`) return;
|
|
7389
7398
|
e.stopPropagation();
|
|
7390
7399
|
if (this._isOpen) {
|
|
7391
7400
|
this._closeMenu();
|
|
@@ -7432,9 +7441,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7432
7441
|
this._popup.style.left = `${e.clientX}px`;
|
|
7433
7442
|
this._popup.style.position = `fixed`;
|
|
7434
7443
|
this._popup.style.positionAnchor = `none`;
|
|
7435
|
-
|
|
7436
|
-
this._popup.showPopover();
|
|
7437
|
-
} catch {}
|
|
7444
|
+
this._openMenu();
|
|
7438
7445
|
}
|
|
7439
7446
|
static {
|
|
7440
7447
|
this.styles = [
|
|
@@ -7456,6 +7463,18 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7456
7463
|
align-items: stretch;
|
|
7457
7464
|
}
|
|
7458
7465
|
|
|
7466
|
+
/* In context mode the target is the consumer's own content surface (a
|
|
7467
|
+
list, canvas, whole panel) rather than a button wrapper. Dissolve the
|
|
7468
|
+
wrapper's box by default so slotted content participates directly in
|
|
7469
|
+
the layout established around the host and can be clipped/scroll within
|
|
7470
|
+
the space actually available. Set --menu-trigger-area-display: flex
|
|
7471
|
+
(plus --menu-trigger-area-flex) to keep a real box instead, e.g. for
|
|
7472
|
+
a mixed visible-trigger + context-target use. */
|
|
7473
|
+
:host([mode="context"]) .trigger-area {
|
|
7474
|
+
display: var(--menu-trigger-area-display, contents);
|
|
7475
|
+
flex: var(--menu-trigger-area-flex, initial);
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7459
7478
|
.trigger-btn {
|
|
7460
7479
|
display: inline-flex;
|
|
7461
7480
|
align-items: center;
|
|
@@ -7518,7 +7537,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7518
7537
|
];
|
|
7519
7538
|
}
|
|
7520
7539
|
render() {
|
|
7521
|
-
const showButton = this.mode !== `dropdown` && this._hasMenu && !this._hasTriggerSlot;
|
|
7540
|
+
const showButton = this.mode !== `dropdown` && this._hasMenu && !this._hasTriggerSlot && !(this.mode === `context` && this._hasContextTarget);
|
|
7522
7541
|
return html`
|
|
7523
7542
|
<div
|
|
7524
7543
|
class="trigger-area"
|
|
@@ -7526,6 +7545,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7526
7545
|
@contextmenu=${this._handleContextMenu}
|
|
7527
7546
|
>
|
|
7528
7547
|
${showButton ? html`<button class="trigger-btn" part="base" tabindex=${this.tabStop ? `0` : `-1`}>${this.iconName ? html`<ixfx-icon name=${this.iconName}></ixfx-icon>` : this.label || html`<ixfx-icon name="ellipsis-vertical"></ixfx-icon>`}</button>` : null}
|
|
7548
|
+
${this.mode === `context` ? html`<slot name="context-target" @slotchange=${this._handleContextTargetSlotChange}></slot>` : null}
|
|
7529
7549
|
<slot name="trigger" @slotchange=${this._handleTriggerSlotChange}></slot>
|
|
7530
7550
|
</div>
|
|
7531
7551
|
<div
|
|
@@ -7538,7 +7558,10 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7538
7558
|
`;
|
|
7539
7559
|
}
|
|
7540
7560
|
};
|
|
7541
|
-
__decorate([property({
|
|
7561
|
+
__decorate([property({
|
|
7562
|
+
type: String,
|
|
7563
|
+
reflect: true
|
|
7564
|
+
})], MenuTrigger.prototype, "mode", void 0);
|
|
7542
7565
|
__decorate([property({ type: String })], MenuTrigger.prototype, "label", void 0);
|
|
7543
7566
|
__decorate([property({
|
|
7544
7567
|
type: String,
|
|
@@ -7554,6 +7577,7 @@ __decorate([property({
|
|
|
7554
7577
|
__decorate([state()], MenuTrigger.prototype, "_isOpen", void 0);
|
|
7555
7578
|
__decorate([state()], MenuTrigger.prototype, "_hasMenu", void 0);
|
|
7556
7579
|
__decorate([state()], MenuTrigger.prototype, "_hasTriggerSlot", void 0);
|
|
7580
|
+
__decorate([state()], MenuTrigger.prototype, "_hasContextTarget", void 0);
|
|
7557
7581
|
__decorate([query(`.trigger-btn`)], MenuTrigger.prototype, "_triggerBtn", void 0);
|
|
7558
7582
|
__decorate([query(`.popup`)], MenuTrigger.prototype, "_popup", void 0);
|
|
7559
7583
|
MenuTrigger = _MenuTrigger = __decorate([safeCustomElement(`ixfx-menu-trigger`)], MenuTrigger);
|
|
@@ -8460,37 +8484,40 @@ var LabelledInputBase = class extends LitElement {
|
|
|
8460
8484
|
this._isDragging = false;
|
|
8461
8485
|
this._debounceTimer = void 0;
|
|
8462
8486
|
this._handleNumberChange = (e) => {
|
|
8463
|
-
const
|
|
8487
|
+
const valueStr = e.detail?.value ?? String(this.value);
|
|
8488
|
+
const newValue = Number.parseFloat(valueStr);
|
|
8464
8489
|
this.value = newValue;
|
|
8465
8490
|
this._syncValue(newValue);
|
|
8466
8491
|
this.dispatchEvent(new CustomEvent(`change`, {
|
|
8467
|
-
detail: { value:
|
|
8492
|
+
detail: { value: valueStr },
|
|
8468
8493
|
bubbles: true,
|
|
8469
8494
|
composed: true
|
|
8470
8495
|
}));
|
|
8471
8496
|
};
|
|
8472
8497
|
this._handleInputChange = (e) => {
|
|
8473
|
-
|
|
8498
|
+
const value = e.detail?.value ?? this.value;
|
|
8499
|
+
this.value = value;
|
|
8474
8500
|
clearTimeout(this._debounceTimer);
|
|
8475
8501
|
this._debounceTimer = window.setTimeout(() => {
|
|
8476
8502
|
this._isDragging = false;
|
|
8477
8503
|
}, 300);
|
|
8478
8504
|
this.dispatchEvent(new CustomEvent(`change`, {
|
|
8479
|
-
detail: { value: String(
|
|
8505
|
+
detail: { value: String(value) },
|
|
8480
8506
|
bubbles: true,
|
|
8481
8507
|
composed: true
|
|
8482
8508
|
}));
|
|
8483
8509
|
};
|
|
8484
8510
|
this._handleSliderInput = (e) => {
|
|
8511
|
+
const value = e.detail?.value ?? this.value;
|
|
8485
8512
|
console.log(`[LABELLED-INPUT] _handleSliderInput called`, {
|
|
8486
|
-
value
|
|
8513
|
+
value,
|
|
8487
8514
|
composedPath: e.composedPath()
|
|
8488
8515
|
});
|
|
8489
|
-
this.value =
|
|
8516
|
+
this.value = value;
|
|
8490
8517
|
this._isDragging = true;
|
|
8491
8518
|
clearTimeout(this._debounceTimer);
|
|
8492
8519
|
this.dispatchEvent(new CustomEvent(`input`, {
|
|
8493
|
-
detail: { value
|
|
8520
|
+
detail: { value },
|
|
8494
8521
|
bubbles: true,
|
|
8495
8522
|
composed: true
|
|
8496
8523
|
}));
|
|
@@ -11986,6 +12013,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
11986
12013
|
this.path = ``;
|
|
11987
12014
|
this.separator = `/`;
|
|
11988
12015
|
this.pathRoot = ``;
|
|
12016
|
+
this.hintPrevious = false;
|
|
11989
12017
|
this._openIdx = null;
|
|
11990
12018
|
this._editing = false;
|
|
11991
12019
|
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
@@ -11997,6 +12025,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
11997
12025
|
this._openCumPath = null;
|
|
11998
12026
|
this._dragCumPath = null;
|
|
11999
12027
|
this._dropCumPath = null;
|
|
12028
|
+
this._hintSegments = null;
|
|
12000
12029
|
this.#uid = Math.random().toString(36).slice(2, 8);
|
|
12001
12030
|
this.#clickSuppressUntil = 0;
|
|
12002
12031
|
this.#onPopoverToggle = (e) => {
|
|
@@ -12132,6 +12161,19 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12132
12161
|
#clicksSuppressed() {
|
|
12133
12162
|
return Date.now() < this.#clickSuppressUntil || this._dragCumPath !== null;
|
|
12134
12163
|
}
|
|
12164
|
+
#isSegmentPrefix(prefix, full) {
|
|
12165
|
+
return prefix.length <= full.length && prefix.every((seg, i) => seg === full[i]);
|
|
12166
|
+
}
|
|
12167
|
+
/**
|
|
12168
|
+
* Called when a real segment click shortens the path. Keeps the deepest
|
|
12169
|
+
* known branch: only overwrites `_hintSegments` when the current hint no
|
|
12170
|
+
* longer covers where we're navigating to.
|
|
12171
|
+
*/
|
|
12172
|
+
#recordHintOnShorten(oldSegments, newSegments) {
|
|
12173
|
+
if (!this.hintPrevious) return;
|
|
12174
|
+
const hint = this._hintSegments;
|
|
12175
|
+
if (!(hint !== null && this.#isSegmentPrefix(newSegments, hint))) this._hintSegments = oldSegments;
|
|
12176
|
+
}
|
|
12135
12177
|
#resolveSegmentIcon(fullPathSegments, index) {
|
|
12136
12178
|
const direct = this.segmentIconLookup?.(fullPathSegments, index);
|
|
12137
12179
|
if (typeof direct === `string` && direct.length > 0) return direct;
|
|
@@ -12277,12 +12319,63 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12277
12319
|
composed: true
|
|
12278
12320
|
}));
|
|
12279
12321
|
}
|
|
12322
|
+
/**
|
|
12323
|
+
* The current leaf segment (deepest visible segment), used as the drop target
|
|
12324
|
+
* when a drag is released over blank space rather than a specific `.seg`.
|
|
12325
|
+
*/
|
|
12326
|
+
#leafSegment() {
|
|
12327
|
+
const segments = this.#getSegments();
|
|
12328
|
+
if (segments.length > 0) {
|
|
12329
|
+
const index = segments.length - 1;
|
|
12330
|
+
return {
|
|
12331
|
+
cumPath: this.#getCumulativePath(index, false),
|
|
12332
|
+
fullPathSegments: this.#resolveFullPathSegments(index, false)
|
|
12333
|
+
};
|
|
12334
|
+
}
|
|
12335
|
+
const rootSegments = this.#getRootSegments();
|
|
12336
|
+
if (rootSegments.length > 0) {
|
|
12337
|
+
const index = rootSegments.length - 1;
|
|
12338
|
+
return {
|
|
12339
|
+
cumPath: this.#getCumulativePath(index, true),
|
|
12340
|
+
fullPathSegments: this.#resolveFullPathSegments(index, true)
|
|
12341
|
+
};
|
|
12342
|
+
}
|
|
12343
|
+
}
|
|
12344
|
+
/**
|
|
12345
|
+
* Is `event` already being handled by a `.seg`'s own drag handlers? Virtual
|
|
12346
|
+
* (hint) segments don't have drag handlers of their own, so a drag released
|
|
12347
|
+
* over one falls through to the host-level fallback (the real leaf).
|
|
12348
|
+
*/
|
|
12349
|
+
#eventTargetsSegment(event) {
|
|
12350
|
+
return Boolean(event.target?.closest?.(`.seg:not(.seg-virtual)`));
|
|
12351
|
+
}
|
|
12352
|
+
#onHostDragOver(event) {
|
|
12353
|
+
if (!this.parseExternalDrop || !event.dataTransfer) return;
|
|
12354
|
+
if (this.#eventTargetsSegment(event)) return;
|
|
12355
|
+
const leaf = this.#leafSegment();
|
|
12356
|
+
if (!leaf) return;
|
|
12357
|
+
event.preventDefault();
|
|
12358
|
+
if (this._dropCumPath !== leaf.cumPath) this._dropCumPath = leaf.cumPath;
|
|
12359
|
+
}
|
|
12360
|
+
#onHostDragLeave(event) {
|
|
12361
|
+
if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
|
|
12362
|
+
const leaf = this.#leafSegment();
|
|
12363
|
+
if (leaf && this._dropCumPath === leaf.cumPath) this._dropCumPath = null;
|
|
12364
|
+
}
|
|
12365
|
+
#onHostDrop(event) {
|
|
12366
|
+
if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
|
|
12367
|
+
const leaf = this.#leafSegment();
|
|
12368
|
+
if (!leaf) return;
|
|
12369
|
+
this.#onSegDrop(leaf.fullPathSegments, event);
|
|
12370
|
+
}
|
|
12280
12371
|
#onLabelClick(index, event) {
|
|
12281
12372
|
if (this.#clicksSuppressed()) return;
|
|
12282
12373
|
event.preventDefault();
|
|
12283
12374
|
event.stopPropagation();
|
|
12284
12375
|
this.#closeDropdown();
|
|
12285
|
-
const
|
|
12376
|
+
const segments = this.#getSegments();
|
|
12377
|
+
const truncatedSegments = segments.slice(0, index + 1);
|
|
12378
|
+
this.#recordHintOnShorten(segments, truncatedSegments);
|
|
12286
12379
|
const hadLeading = this.path.startsWith(this.separator);
|
|
12287
12380
|
const newPath = truncatedSegments.join(this.separator);
|
|
12288
12381
|
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
@@ -12300,6 +12393,31 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12300
12393
|
composed: true
|
|
12301
12394
|
}));
|
|
12302
12395
|
}
|
|
12396
|
+
#onVirtualSegmentClick(hintIndex, event) {
|
|
12397
|
+
if (this.#clicksSuppressed()) return;
|
|
12398
|
+
event.preventDefault();
|
|
12399
|
+
event.stopPropagation();
|
|
12400
|
+
this.#closeDropdown();
|
|
12401
|
+
const hint = this._hintSegments;
|
|
12402
|
+
if (!hint) return;
|
|
12403
|
+
const truncatedSegments = hint.slice(0, hintIndex + 1);
|
|
12404
|
+
const hadLeading = this.path.startsWith(this.separator);
|
|
12405
|
+
const newPath = truncatedSegments.join(this.separator);
|
|
12406
|
+
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
12407
|
+
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
12408
|
+
this._leafSegmentIndex = null;
|
|
12409
|
+
const parentPath = truncatedSegments.slice(0, -1).join(this.separator);
|
|
12410
|
+
this.dispatchEvent(new CustomEvent(`navigate`, {
|
|
12411
|
+
detail: {
|
|
12412
|
+
index: hintIndex,
|
|
12413
|
+
label: truncatedSegments[hintIndex],
|
|
12414
|
+
parentPath,
|
|
12415
|
+
fullPath: newPath
|
|
12416
|
+
},
|
|
12417
|
+
bubbles: true,
|
|
12418
|
+
composed: true
|
|
12419
|
+
}));
|
|
12420
|
+
}
|
|
12303
12421
|
#selectSibling(index, segment) {
|
|
12304
12422
|
const cumPath = this._openCumPath;
|
|
12305
12423
|
this.#closeDropdown();
|
|
@@ -12375,7 +12493,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12375
12493
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
12376
12494
|
<label title=${label}
|
|
12377
12495
|
part="label"
|
|
12378
|
-
|
|
12496
|
+
draggable=${draggable ? `true` : `false`}
|
|
12379
12497
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
12380
12498
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
12381
12499
|
@dragend=${() => this.#onLabelDragEnd()}>
|
|
@@ -12392,6 +12510,26 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12392
12510
|
</div>
|
|
12393
12511
|
` : nothing}
|
|
12394
12512
|
</div>
|
|
12513
|
+
`;
|
|
12514
|
+
}
|
|
12515
|
+
/**
|
|
12516
|
+
* A translucent, click-to-restore segment beyond the current path, shown
|
|
12517
|
+
* while `hintPrevious` is enabled and the current path is a prefix of the
|
|
12518
|
+
* deepest path visited. Doesn't participate in drag'n'drop and isn't
|
|
12519
|
+
* keyboard-focusable.
|
|
12520
|
+
*/
|
|
12521
|
+
#renderVirtualSegment(label, hintIndex) {
|
|
12522
|
+
const fullPathSegments = [...this.#getRootSegments(), ...(this._hintSegments ?? []).slice(0, hintIndex + 1)];
|
|
12523
|
+
const icon = this.#resolveSegmentIcon(fullPathSegments, fullPathSegments.length - 1);
|
|
12524
|
+
return html`
|
|
12525
|
+
<div class="seg seg-virtual" part="segment virtual">
|
|
12526
|
+
<label title=${label}
|
|
12527
|
+
part="label"
|
|
12528
|
+
@click=${(e) => this.#onVirtualSegmentClick(hintIndex, e)}>
|
|
12529
|
+
${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}
|
|
12530
|
+
${label}
|
|
12531
|
+
</label>
|
|
12532
|
+
</div>
|
|
12395
12533
|
`;
|
|
12396
12534
|
}
|
|
12397
12535
|
#renderRootSegment(label, index) {
|
|
@@ -12413,7 +12551,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12413
12551
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
12414
12552
|
<label title=${label}
|
|
12415
12553
|
part="label"
|
|
12416
|
-
|
|
12554
|
+
draggable=${draggable ? `true` : `false`}
|
|
12417
12555
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
12418
12556
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
12419
12557
|
@dragend=${() => this.#onLabelDragEnd()}>${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}${label}</label>
|
|
@@ -12519,6 +12657,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12519
12657
|
#onHostKeyDown;
|
|
12520
12658
|
render() {
|
|
12521
12659
|
const segments = this.#getSegments();
|
|
12660
|
+
const hintSegments = this.hintPrevious ? this._hintSegments : null;
|
|
12661
|
+
const virtualSegments = hintSegments !== null && hintSegments.length > segments.length && this.#isSegmentPrefix(segments, hintSegments) ? hintSegments.slice(segments.length) : [];
|
|
12522
12662
|
if (this._editing) {
|
|
12523
12663
|
const editValue = this.path;
|
|
12524
12664
|
return html`
|
|
@@ -12541,10 +12681,15 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12541
12681
|
${this.separator}
|
|
12542
12682
|
</div>` : nothing;
|
|
12543
12683
|
return html`
|
|
12544
|
-
<div class="crumbs scroll-fade-x" tabindex="0"
|
|
12684
|
+
<div class="crumbs scroll-fade-x" tabindex="0"
|
|
12685
|
+
@keydown=${this.#onHostKeyDown}
|
|
12686
|
+
@dragover=${(e) => this.#onHostDragOver(e)}
|
|
12687
|
+
@dragleave=${(e) => this.#onHostDragLeave(e)}
|
|
12688
|
+
@drop=${(e) => this.#onHostDrop(e)}>
|
|
12545
12689
|
${rootSegments.length > 0 ? rootSegments.map((label, i) => this.#renderRootSegment(label, i)) : nothing}
|
|
12546
12690
|
${leadingCaret}
|
|
12547
12691
|
${segments.map((label, i) => this.#renderSegment(label, i, i === segments.length - 1))}
|
|
12692
|
+
${virtualSegments.map((label, i) => this.#renderVirtualSegment(label, segments.length + i))}
|
|
12548
12693
|
<div class="trailing" @click=${this.#onTrailingClick}></div>
|
|
12549
12694
|
</div>
|
|
12550
12695
|
${this._openIdx !== null ? html`
|
|
@@ -12571,6 +12716,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12571
12716
|
--padding-m: var(--space-s);
|
|
12572
12717
|
--hover-bg: var(--surface-5);
|
|
12573
12718
|
--hover-fg: var(--surface-5-text);
|
|
12719
|
+
--crumb-hint-opacity: 0.35;
|
|
12720
|
+
--crumb-hint-hover-opacity: 0.6;
|
|
12574
12721
|
|
|
12575
12722
|
display: flex;
|
|
12576
12723
|
width: 100%;
|
|
@@ -12682,6 +12829,14 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12682
12829
|
background: var(--crumb-drop-into-bg, color-mix(in srgb, var(--accent) 14%, transparent));
|
|
12683
12830
|
}
|
|
12684
12831
|
|
|
12832
|
+
.seg-virtual {
|
|
12833
|
+
opacity: var(--crumb-hint-opacity);
|
|
12834
|
+
}
|
|
12835
|
+
|
|
12836
|
+
.seg-virtual:hover {
|
|
12837
|
+
opacity: var(--crumb-hint-hover-opacity);
|
|
12838
|
+
}
|
|
12839
|
+
|
|
12685
12840
|
.crumb-icon {
|
|
12686
12841
|
width: 1em;
|
|
12687
12842
|
height: 1em;
|
|
@@ -12766,6 +12921,10 @@ __decorate([property({ type: Object })], CrumbPathElement.prototype, "siblingLoo
|
|
|
12766
12921
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentIconLookup", void 0);
|
|
12767
12922
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentDragLookup", void 0);
|
|
12768
12923
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "parseExternalDrop", void 0);
|
|
12924
|
+
__decorate([property({
|
|
12925
|
+
type: Boolean,
|
|
12926
|
+
attribute: `hint-previous`
|
|
12927
|
+
})], CrumbPathElement.prototype, "hintPrevious", void 0);
|
|
12769
12928
|
__decorate([state()], CrumbPathElement.prototype, "_openIdx", void 0);
|
|
12770
12929
|
__decorate([state()], CrumbPathElement.prototype, "_editing", void 0);
|
|
12771
12930
|
__decorate([state()], CrumbPathElement.prototype, "_cachedSiblings", void 0);
|
|
@@ -12777,6 +12936,7 @@ __decorate([state()], CrumbPathElement.prototype, "_editStartIndex", void 0);
|
|
|
12777
12936
|
__decorate([state()], CrumbPathElement.prototype, "_openCumPath", void 0);
|
|
12778
12937
|
__decorate([state()], CrumbPathElement.prototype, "_dragCumPath", void 0);
|
|
12779
12938
|
__decorate([state()], CrumbPathElement.prototype, "_dropCumPath", void 0);
|
|
12939
|
+
__decorate([state()], CrumbPathElement.prototype, "_hintSegments", void 0);
|
|
12780
12940
|
CrumbPathElement = __decorate([safeCustomElement(`ixfx-crumb-path`)], CrumbPathElement);
|
|
12781
12941
|
//#endregion
|
|
12782
12942
|
//#region src/data-display/data-display.ts
|
|
@@ -13100,6 +13260,34 @@ let DataDisplayComponent = class DataDisplayComponent extends LitElement {
|
|
|
13100
13260
|
__decorate([property({ type: Boolean })], DataDisplayComponent.prototype, "automap", void 0);
|
|
13101
13261
|
DataDisplayComponent = __decorate([safeCustomElement(`ixfx-data-display`)], DataDisplayComponent);
|
|
13102
13262
|
//#endregion
|
|
13263
|
+
//#region src/common/item-appearance.ts
|
|
13264
|
+
/**
|
|
13265
|
+
* Shared helpers for the list components' per-item appearance hooks — the
|
|
13266
|
+
* per-item icon colour / text colour / badge that miller-list, detail-list,
|
|
13267
|
+
* grid-list, data-grid and vertical-list all accept.
|
|
13268
|
+
*
|
|
13269
|
+
* Every component takes these three features in its own idiom (plain fields
|
|
13270
|
+
* on a `TreeItem`, a property-name lookup, an adapter function, a nominated
|
|
13271
|
+
* column) but they all agree on what "not supplied" means, and on never
|
|
13272
|
+
* emitting an empty slot for an absent value.
|
|
13273
|
+
*/
|
|
13274
|
+
/**
|
|
13275
|
+
* Whether a per-item appearance value counts as "not supplied". `false` and
|
|
13276
|
+
* `''` are included so a consumer can clear a feature without rebuilding the
|
|
13277
|
+
* collection — e.g. `badge: flags && tagChip(flags.colour)`.
|
|
13278
|
+
*/
|
|
13279
|
+
function appearanceAbsent(value) {
|
|
13280
|
+
return value === void 0 || value === null || value === false || value === ``;
|
|
13281
|
+
}
|
|
13282
|
+
/**
|
|
13283
|
+
* A usable CSS colour from a per-item value, or `undefined`. Non-strings and
|
|
13284
|
+
* empty strings are ignored rather than stringified, so a stray number or
|
|
13285
|
+
* `0` never lands in a `style` attribute.
|
|
13286
|
+
*/
|
|
13287
|
+
function appearanceColor(value) {
|
|
13288
|
+
return typeof value === `string` && value.length > 0 ? value : void 0;
|
|
13289
|
+
}
|
|
13290
|
+
//#endregion
|
|
13103
13291
|
//#region src/util/drag-ghost.ts
|
|
13104
13292
|
/**
|
|
13105
13293
|
* Theme-aware floating element helper.
|
|
@@ -14224,6 +14412,12 @@ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
|
|
|
14224
14412
|
PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
|
|
14225
14413
|
//#endregion
|
|
14226
14414
|
//#region src/data-grid/data-grid.ts
|
|
14415
|
+
const EMPTY_CELL_LOOK = {
|
|
14416
|
+
textColor: void 0,
|
|
14417
|
+
icon: void 0,
|
|
14418
|
+
iconColor: void 0,
|
|
14419
|
+
badge: void 0
|
|
14420
|
+
};
|
|
14227
14421
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
14228
14422
|
function sameKeys$3(a, b) {
|
|
14229
14423
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -14248,6 +14442,9 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
14248
14442
|
this.allowGrouping = false;
|
|
14249
14443
|
this.crossGroupSelection = false;
|
|
14250
14444
|
this.columnMenu = `auto`;
|
|
14445
|
+
this.textColorColumn = ``;
|
|
14446
|
+
this.iconColumn = ``;
|
|
14447
|
+
this.badgeColumn = ``;
|
|
14251
14448
|
this.itemsDraggable = false;
|
|
14252
14449
|
this._columnFormatters = /* @__PURE__ */ new Map();
|
|
14253
14450
|
this._resizingColKey = void 0;
|
|
@@ -14667,6 +14864,25 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
14667
14864
|
}
|
|
14668
14865
|
.cell.editable-text { cursor: text; }
|
|
14669
14866
|
.cell input, .cell select { width: 100%; font: inherit; }
|
|
14867
|
+
/* Per-row icon / badge, enabled by nominating iconColumn / badgeColumn.
|
|
14868
|
+
The badge self-justifies to the cell's trailing edge; the value text
|
|
14869
|
+
keeps its own flex sizing so the cell still ellipsises. */
|
|
14870
|
+
.cell-icon {
|
|
14871
|
+
flex: none;
|
|
14872
|
+
display: inline-flex;
|
|
14873
|
+
align-items: center;
|
|
14874
|
+
justify-content: center;
|
|
14875
|
+
margin-inline-end: var(--data-grid-cell-icon-gap, 6px);
|
|
14876
|
+
width: var(--data-grid-cell-icon-size, 1em);
|
|
14877
|
+
height: var(--data-grid-cell-icon-size, 1em);
|
|
14878
|
+
}
|
|
14879
|
+
.cell-badge {
|
|
14880
|
+
flex: none;
|
|
14881
|
+
display: inline-flex;
|
|
14882
|
+
align-items: center;
|
|
14883
|
+
margin-inline-start: auto;
|
|
14884
|
+
padding-inline-start: var(--data-grid-badge-gap, 6px);
|
|
14885
|
+
}
|
|
14670
14886
|
.checkbox-col { width: 28px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
14671
14887
|
ixfx-menu-trigger { flex-shrink: 0; border-left: var(--border-subtle); background: var(--data-grid-header-bg); display: flex; align-items: center; --ixfx-button-padding: var(--space-s) var(--space-m); min-width: 44px; min-height: var(--data-grid-header-height); justify-content: center; }
|
|
14672
14888
|
.header-cell[drag-source] { opacity: 0.4; }
|
|
@@ -15438,6 +15654,27 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15438
15654
|
if (this.globalFormatter) return this.globalFormatter(value, col.key, row);
|
|
15439
15655
|
return value;
|
|
15440
15656
|
}
|
|
15657
|
+
/**
|
|
15658
|
+
* Resolve the per-row appearance for the cell at `colKey`, honouring the
|
|
15659
|
+
* nominated columns. Additive — `col.renderer` / `col.formatter` still own
|
|
15660
|
+
* the cell's value; this only decorates it. Returns all-`undefined` for
|
|
15661
|
+
* every cell when nothing is nominated (the common case).
|
|
15662
|
+
*/
|
|
15663
|
+
_cellLook(row, colKey) {
|
|
15664
|
+
if (this.textColorColumn === `` && this.iconColumn === `` && this.badgeColumn === ``) return EMPTY_CELL_LOOK;
|
|
15665
|
+
const rec = typeof row === `object` && row !== null ? row : {};
|
|
15666
|
+
const textColor = colKey === this.textColorColumn ? appearanceColor(rec.textColor) : void 0;
|
|
15667
|
+
const icon = colKey === this.iconColumn && !appearanceAbsent(rec.icon) ? rec.icon : void 0;
|
|
15668
|
+
const iconColor = icon === void 0 ? void 0 : appearanceColor(rec.iconColor);
|
|
15669
|
+
const badge = colKey === this.badgeColumn && !appearanceAbsent(rec.badge) ? rec.badge : void 0;
|
|
15670
|
+
if (textColor === void 0 && icon === void 0 && badge === void 0) return EMPTY_CELL_LOOK;
|
|
15671
|
+
return {
|
|
15672
|
+
textColor,
|
|
15673
|
+
icon,
|
|
15674
|
+
iconColor,
|
|
15675
|
+
badge
|
|
15676
|
+
};
|
|
15677
|
+
}
|
|
15441
15678
|
_handleCellSecondClick(rowKey, col) {
|
|
15442
15679
|
if (this.editability !== `user`) return;
|
|
15443
15680
|
const now = Date.now();
|
|
@@ -15895,7 +16132,9 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15895
16132
|
const textEditable = this._isEditable(row, col) && this._isTextEditor(col);
|
|
15896
16133
|
const indent = col.align !== `right` && col.align !== `center` && col.reorderable !== false ? `padding-left:calc(var(--space-s) + var(--data-grid-reorder-handle-width, 10px) + var(--space-xs));` : ``;
|
|
15897
16134
|
const canPan = this.cellOverflow === `pan-text` && !col.renderer && (selected || tickled);
|
|
15898
|
-
|
|
16135
|
+
const look = this._cellLook(row, col.key);
|
|
16136
|
+
const cellStyle = look.textColor === void 0 ? `` : `color:${look.textColor};`;
|
|
16137
|
+
return html`<div class="cell ${textEditable ? `editable-text` : ``} ${this._animatingColKeys.has(col.key) ? `width-animating` : ``}" data-col=${col.key} style="width:${col.width ?? 150}px; justify-content:${col.align === `right` ? `flex-end` : col.align === `center` ? `center` : `flex-start`}; ${indent} ${cellStyle}"
|
|
15899
16138
|
@click=${(e) => {
|
|
15900
16139
|
this._lastClickedCell = {
|
|
15901
16140
|
rowKey,
|
|
@@ -15917,7 +16156,7 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15917
16156
|
composed: true
|
|
15918
16157
|
}));
|
|
15919
16158
|
}}
|
|
15920
|
-
>${canPan ? html`<ixfx-pan-text mode="loop">${cellContent}</ixfx-pan-text>` : cellContent}</div>`;
|
|
16159
|
+
>${look.icon === void 0 ? nothing : html`<span class="cell-icon" part="icon" style=${look.iconColor === void 0 ? nothing : `color:${look.iconColor}`}>${look.icon}</span>`}${canPan ? html`<ixfx-pan-text mode="loop">${cellContent}</ixfx-pan-text>` : cellContent}${look.badge === void 0 ? nothing : html`<span class="cell-badge" part="badge">${look.badge}</span>`}</div>`;
|
|
15921
16160
|
})}
|
|
15922
16161
|
</div>
|
|
15923
16162
|
`;
|
|
@@ -15977,6 +16216,18 @@ __decorate([property({
|
|
|
15977
16216
|
attribute: `column-menu`
|
|
15978
16217
|
})], DataGridElement.prototype, "columnMenu", void 0);
|
|
15979
16218
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "globalFormatter", void 0);
|
|
16219
|
+
__decorate([property({
|
|
16220
|
+
type: String,
|
|
16221
|
+
attribute: `text-color-column`
|
|
16222
|
+
})], DataGridElement.prototype, "textColorColumn", void 0);
|
|
16223
|
+
__decorate([property({
|
|
16224
|
+
type: String,
|
|
16225
|
+
attribute: `icon-column`
|
|
16226
|
+
})], DataGridElement.prototype, "iconColumn", void 0);
|
|
16227
|
+
__decorate([property({
|
|
16228
|
+
type: String,
|
|
16229
|
+
attribute: `badge-column`
|
|
16230
|
+
})], DataGridElement.prototype, "badgeColumn", void 0);
|
|
15980
16231
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
|
|
15981
16232
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
|
|
15982
16233
|
__decorate([property({
|
|
@@ -16277,6 +16528,14 @@ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableE
|
|
|
16277
16528
|
const ITEM_SELECTOR$3 = `:scope > li`;
|
|
16278
16529
|
const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
|
|
16279
16530
|
const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
|
|
16531
|
+
/**
|
|
16532
|
+
* Layout for the per-item badge wrapper `#ensureBadge` adds. Applied as inline
|
|
16533
|
+
* `cssText` rather than shadow CSS because `::slotted()` only matches the
|
|
16534
|
+
* host's *direct* children — the badge lives inside a slotted `<li>`, so a
|
|
16535
|
+
* `::slotted()` rule could never reach it. The values still read the CSS
|
|
16536
|
+
* custom property, so it stays themeable from the outside.
|
|
16537
|
+
*/
|
|
16538
|
+
const BADGE_EL_CSS = `flex-shrink:0;display:inline-flex;align-items:center;margin-inline-start:auto;padding-inline-start:var(--detail-list-badge-gap,8px)`;
|
|
16280
16539
|
function pointInRect(x, y, rect) {
|
|
16281
16540
|
return rect.width > 0 && rect.height > 0 && x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
|
16282
16541
|
}
|
|
@@ -16310,6 +16569,9 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16310
16569
|
this.items = [];
|
|
16311
16570
|
this.displayProperty = ``;
|
|
16312
16571
|
this.iconProperty = ``;
|
|
16572
|
+
this.iconColorProperty = ``;
|
|
16573
|
+
this.textColorProperty = ``;
|
|
16574
|
+
this.badgeProperty = ``;
|
|
16313
16575
|
this.keyProperty = ``;
|
|
16314
16576
|
this.itemWrap = `stop`;
|
|
16315
16577
|
this.itemsDraggable = false;
|
|
@@ -16511,7 +16773,8 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16511
16773
|
};
|
|
16512
16774
|
this._onWheel = (e) => {
|
|
16513
16775
|
e.preventDefault();
|
|
16514
|
-
|
|
16776
|
+
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
16777
|
+
this._listEl.scrollLeft += delta;
|
|
16515
16778
|
};
|
|
16516
16779
|
}
|
|
16517
16780
|
#itemData;
|
|
@@ -16716,7 +16979,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16716
16979
|
});
|
|
16717
16980
|
return;
|
|
16718
16981
|
}
|
|
16719
|
-
if (changed.has(`items`) && this.provider === void 0 && this.items.length > 0) this.#rebuildFromObjects(this.items);
|
|
16982
|
+
if ((changed.has(`items`) || changed.has(`displayProperty`) || changed.has(`formatter`) || changed.has(`iconProperty`) || changed.has(`iconColorProperty`) || changed.has(`textColorProperty`) || changed.has(`badgeProperty`) || changed.has(`keyProperty`)) && this.provider === void 0 && this.items.length > 0) this.#rebuildFromObjects(this.items);
|
|
16720
16983
|
this.#syncFromProvider();
|
|
16721
16984
|
if (changed.has(`sort`) || changed.has(`sortBy`) || changed.has(`sortComparator`)) this.#applySort();
|
|
16722
16985
|
if (changed.has(`filterPredicate`)) {
|
|
@@ -16921,24 +17184,36 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16921
17184
|
const li = document.createElement(`li`);
|
|
16922
17185
|
this.#itemData.set(li, item);
|
|
16923
17186
|
const content = this.formatter ? this.formatter(item, index) : this.displayProperty ? this.#propertyValue(item, this.displayProperty) : item;
|
|
17187
|
+
const textColor = this.#colorValue(item, this.textColorProperty);
|
|
17188
|
+
const iconColor = this.#colorValue(item, this.iconColorProperty);
|
|
17189
|
+
if (iconColor !== void 0) li.setAttribute(`data-icon-color`, iconColor);
|
|
16924
17190
|
if (typeof content === `string`) {
|
|
16925
|
-
this._setLabel(li, content);
|
|
17191
|
+
const span = this._setLabel(li, content);
|
|
17192
|
+
if (textColor !== void 0) span.style.color = textColor;
|
|
16926
17193
|
if (this.iconProperty) {
|
|
16927
17194
|
const icon = this.#propertyValue(item, this.iconProperty);
|
|
16928
17195
|
if (typeof icon === `string` && icon.length > 0) li.setAttribute(`data-icon`, icon);
|
|
16929
17196
|
}
|
|
16930
|
-
} else if (content instanceof Node)
|
|
17197
|
+
} else if (content instanceof Node) {
|
|
17198
|
+
li.appendChild(content);
|
|
17199
|
+
if (textColor !== void 0) li.style.color = textColor;
|
|
17200
|
+
}
|
|
16931
17201
|
if (this.keyProperty) {
|
|
16932
17202
|
const key = this.#propertyValue(item, this.keyProperty);
|
|
16933
17203
|
if (key !== void 0 && key !== null) li.setAttribute(`data-key`, String(key));
|
|
16934
17204
|
}
|
|
16935
17205
|
this.#ensureIcon(li);
|
|
17206
|
+
this.#ensureBadge(li, this.#propertyValue(item, this.badgeProperty));
|
|
16936
17207
|
return li;
|
|
16937
17208
|
}
|
|
16938
17209
|
#propertyValue(item, prop) {
|
|
16939
17210
|
if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
|
|
16940
17211
|
return item[prop];
|
|
16941
17212
|
}
|
|
17213
|
+
/** A non-empty string from `item[prop]`, else `undefined`. */
|
|
17214
|
+
#colorValue(item, prop) {
|
|
17215
|
+
return prop ? appearanceColor(this.#propertyValue(item, prop)) : void 0;
|
|
17216
|
+
}
|
|
16942
17217
|
#syncFromProvider() {
|
|
16943
17218
|
if (this.provider === void 0) return;
|
|
16944
17219
|
const items = this.#data.items;
|
|
@@ -17196,12 +17471,14 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17196
17471
|
if (this.interactionMode !== `checked`) return true;
|
|
17197
17472
|
return !item.hasAttribute(`not-checked`);
|
|
17198
17473
|
}
|
|
17474
|
+
/** Appends the caption span; returns it so callers can style it per item. */
|
|
17199
17475
|
_setLabel(li, label) {
|
|
17200
17476
|
const span = document.createElement(`span`);
|
|
17201
17477
|
span.style.cssText = LABEL_SPAN_CSS;
|
|
17202
17478
|
span.textContent = label;
|
|
17203
17479
|
li.appendChild(span);
|
|
17204
17480
|
li.setAttribute(`data-search-label`, label);
|
|
17481
|
+
return span;
|
|
17205
17482
|
}
|
|
17206
17483
|
/**
|
|
17207
17484
|
* Inject a leading `<ixfx-icon>` into an item carrying `data-icon`.
|
|
@@ -17215,8 +17492,28 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17215
17492
|
icon.setAttribute(`name`, name);
|
|
17216
17493
|
icon.style.marginInlineEnd = `var(--detail-list-icon-gap, 6px)`;
|
|
17217
17494
|
icon.style.flexShrink = `0`;
|
|
17495
|
+
const color = li.getAttribute(`data-icon-color`);
|
|
17496
|
+
if (color) icon.style.color = color;
|
|
17218
17497
|
li.prepend(icon);
|
|
17219
17498
|
}
|
|
17499
|
+
/**
|
|
17500
|
+
* Append a right-justified Finder-style badge. `badge` may be a DOM `Node`
|
|
17501
|
+
* or anything Lit can render (typically a `TemplateResult`). Idempotent —
|
|
17502
|
+
* a second call replaces the existing badge.
|
|
17503
|
+
*/
|
|
17504
|
+
#ensureBadge(li, badge) {
|
|
17505
|
+
if (appearanceAbsent(badge)) return;
|
|
17506
|
+
let holder = li.querySelector(`:scope > .item-badge`);
|
|
17507
|
+
if (!holder) {
|
|
17508
|
+
holder = document.createElement(`span`);
|
|
17509
|
+
holder.className = `item-badge`;
|
|
17510
|
+
holder.style.cssText = BADGE_EL_CSS;
|
|
17511
|
+
li.appendChild(holder);
|
|
17512
|
+
}
|
|
17513
|
+
holder.replaceChildren();
|
|
17514
|
+
if (badge instanceof Node) holder.appendChild(badge);
|
|
17515
|
+
else render(badge, holder);
|
|
17516
|
+
}
|
|
17220
17517
|
/** Scan light-DOM children for unprocessed `data-icon` items. */
|
|
17221
17518
|
#processChildren() {
|
|
17222
17519
|
for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#ensureIcon(el);
|
|
@@ -17490,6 +17787,10 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17490
17787
|
opacity: 0.6;
|
|
17491
17788
|
}
|
|
17492
17789
|
|
|
17790
|
+
/* Per-item icon / badge (adapter mode) are styled inline by
|
|
17791
|
+
#ensureBadge — ::slotted() only reaches the host's direct children,
|
|
17792
|
+
and these spans live inside a slotted <li>. */
|
|
17793
|
+
|
|
17493
17794
|
/* checked mode: the injected checkbox communicates selection — suppress bg */
|
|
17494
17795
|
:host([interaction-mode="checked"]) ::slotted([data-selected]) {
|
|
17495
17796
|
background: transparent;
|
|
@@ -17564,6 +17865,18 @@ __decorate([property({
|
|
|
17564
17865
|
type: String,
|
|
17565
17866
|
attribute: `icon-property`
|
|
17566
17867
|
})], DetailListElement.prototype, "iconProperty", void 0);
|
|
17868
|
+
__decorate([property({
|
|
17869
|
+
type: String,
|
|
17870
|
+
attribute: `icon-color-property`
|
|
17871
|
+
})], DetailListElement.prototype, "iconColorProperty", void 0);
|
|
17872
|
+
__decorate([property({
|
|
17873
|
+
type: String,
|
|
17874
|
+
attribute: `text-color-property`
|
|
17875
|
+
})], DetailListElement.prototype, "textColorProperty", void 0);
|
|
17876
|
+
__decorate([property({
|
|
17877
|
+
type: String,
|
|
17878
|
+
attribute: `badge-property`
|
|
17879
|
+
})], DetailListElement.prototype, "badgeProperty", void 0);
|
|
17567
17880
|
__decorate([property({
|
|
17568
17881
|
type: String,
|
|
17569
17882
|
attribute: `key-property`
|
|
@@ -23295,6 +23608,10 @@ var ElementGridListSource = class {
|
|
|
23295
23608
|
if (eff === void 0) return void 0;
|
|
23296
23609
|
return html`<slot name=${this.slotNameFor(this.#items[eff])}></slot>`;
|
|
23297
23610
|
}
|
|
23611
|
+
iconAt(_index) {}
|
|
23612
|
+
iconColorAt(_index) {}
|
|
23613
|
+
textColorAt(_index) {}
|
|
23614
|
+
badgeAt(_index) {}
|
|
23298
23615
|
setFilters(opts) {
|
|
23299
23616
|
if (`predicate` in opts) this.#predicate = opts.predicate;
|
|
23300
23617
|
if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
|
|
@@ -23411,6 +23728,22 @@ var DataGridListSource = class {
|
|
|
23411
23728
|
const eff = this.#effectiveIndices[index];
|
|
23412
23729
|
return eff === void 0 ? void 0 : this.#adapter.render(this.#items[eff], eff);
|
|
23413
23730
|
}
|
|
23731
|
+
iconAt(index) {
|
|
23732
|
+
const eff = this.#effectiveIndices[index];
|
|
23733
|
+
return eff === void 0 || this.#adapter.icon === void 0 ? void 0 : this.#adapter.icon(this.#items[eff], eff);
|
|
23734
|
+
}
|
|
23735
|
+
iconColorAt(index) {
|
|
23736
|
+
const eff = this.#effectiveIndices[index];
|
|
23737
|
+
return eff === void 0 ? void 0 : this.#adapter.iconColor?.(this.#items[eff], eff);
|
|
23738
|
+
}
|
|
23739
|
+
textColorAt(index) {
|
|
23740
|
+
const eff = this.#effectiveIndices[index];
|
|
23741
|
+
return eff === void 0 ? void 0 : this.#adapter.textColor?.(this.#items[eff], eff);
|
|
23742
|
+
}
|
|
23743
|
+
badgeAt(index) {
|
|
23744
|
+
const eff = this.#effectiveIndices[index];
|
|
23745
|
+
return eff === void 0 || this.#adapter.badge === void 0 ? void 0 : this.#adapter.badge(this.#items[eff], eff);
|
|
23746
|
+
}
|
|
23414
23747
|
setFilters(opts) {
|
|
23415
23748
|
if (`predicate` in opts) this.#predicate = opts.predicate;
|
|
23416
23749
|
if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
|
|
@@ -23951,6 +24284,50 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
23951
24284
|
color: inherit;
|
|
23952
24285
|
}
|
|
23953
24286
|
|
|
24287
|
+
/* ---- Per-item icon (data mode) ----
|
|
24288
|
+
Only [has-icon] switches to a column, so a cell without an icon keeps
|
|
24289
|
+
exactly the layout it had before. The main content stays stretched;
|
|
24290
|
+
the icon centres itself above it. */
|
|
24291
|
+
.content.has-icon {
|
|
24292
|
+
display: flex;
|
|
24293
|
+
flex-direction: column;
|
|
24294
|
+
min-height: 0;
|
|
24295
|
+
gap: var(--grid-list-icon-gap, 2px);
|
|
24296
|
+
}
|
|
24297
|
+
.cell-icon {
|
|
24298
|
+
flex: none;
|
|
24299
|
+
align-self: center;
|
|
24300
|
+
display: inline-flex;
|
|
24301
|
+
align-items: center;
|
|
24302
|
+
justify-content: center;
|
|
24303
|
+
width: var(--grid-list-icon-size, 1.25em);
|
|
24304
|
+
height: var(--grid-list-icon-size, 1.25em);
|
|
24305
|
+
}
|
|
24306
|
+
|
|
24307
|
+
/* ---- Per-item badge (data mode) ----
|
|
24308
|
+
Finder thumbnails put the tag inline, immediately before the name, so
|
|
24309
|
+
the caption becomes a flex row. The text is wrapped in its own
|
|
24310
|
+
shrinkable box so the ellipsis survives. */
|
|
24311
|
+
.caption.has-badge {
|
|
24312
|
+
display: flex;
|
|
24313
|
+
align-items: center;
|
|
24314
|
+
gap: var(--grid-list-badge-gap, 3px);
|
|
24315
|
+
text-align: initial;
|
|
24316
|
+
}
|
|
24317
|
+
:host([caption-align="left"]) .caption.has-badge { justify-content: flex-start; }
|
|
24318
|
+
:host([caption-align="right"]) .caption.has-badge { justify-content: flex-end; }
|
|
24319
|
+
.caption.has-badge .caption-text {
|
|
24320
|
+
flex: 0 1 auto;
|
|
24321
|
+
min-width: 0;
|
|
24322
|
+
overflow: hidden;
|
|
24323
|
+
text-overflow: ellipsis;
|
|
24324
|
+
}
|
|
24325
|
+
.caption .badge {
|
|
24326
|
+
flex: none;
|
|
24327
|
+
display: inline-flex;
|
|
24328
|
+
align-items: center;
|
|
24329
|
+
}
|
|
24330
|
+
|
|
23954
24331
|
/* ---- Box modes (hidden / under / overlaid): the cell is one box with a
|
|
23955
24332
|
single selection outline ---- */
|
|
23956
24333
|
:host(:not([caption-style="separate"])) .cell {
|
|
@@ -24959,6 +25336,8 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
24959
25336
|
const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
|
|
24960
25337
|
const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
|
|
24961
25338
|
const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
|
|
25339
|
+
const rawIcon = this.#source.iconAt(index);
|
|
25340
|
+
const icon = appearanceAbsent(rawIcon) ? void 0 : rawIcon;
|
|
24962
25341
|
return html`
|
|
24963
25342
|
<div
|
|
24964
25343
|
class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
|
|
@@ -24976,8 +25355,11 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
24976
25355
|
@dblclick=${() => this.#onCellActivate(key)}
|
|
24977
25356
|
@pointerenter=${() => this.#handleCellPointerEnter(key)}
|
|
24978
25357
|
>
|
|
24979
|
-
<div class="content" part="content"
|
|
24980
|
-
|
|
25358
|
+
<div class="content${icon === void 0 ? `` : ` has-icon`}" part="content">
|
|
25359
|
+
${icon === void 0 ? nothing : html`<span class="cell-icon" part="icon" style=${this.#colorStyle(this.#source.iconColorAt(index))}>${icon}</span>`}
|
|
25360
|
+
${this.#source.renderCellContent(index)}
|
|
25361
|
+
</div>
|
|
25362
|
+
${showCaption ? this.#renderCaption(caption, captionCss, tickled, this.#source.badgeAt(index), this.#source.textColorAt(index)) : ``}
|
|
24981
25363
|
</div>
|
|
24982
25364
|
`;
|
|
24983
25365
|
}
|
|
@@ -24985,10 +25367,25 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
24985
25367
|
* A tickled cell swaps its plain label for an `ixfx-pan-text` in loop mode
|
|
24986
25368
|
* so a truncated label scrolls instead of staying an ellipsis. Unticked
|
|
24987
25369
|
* cells keep the cheap plain-text caption.
|
|
25370
|
+
*
|
|
25371
|
+
* With no `badge` and no `textColor` the DOM is exactly as it was — the
|
|
25372
|
+
* flex row (and its extra `<span>` around the text) only appears when a
|
|
25373
|
+
* badge needs to sit inline ahead of the name.
|
|
24988
25374
|
*/
|
|
24989
|
-
#renderCaption(caption, captionCss, tickled) {
|
|
24990
|
-
|
|
24991
|
-
|
|
25375
|
+
#renderCaption(caption, captionCss, tickled, badge, textColor) {
|
|
25376
|
+
const color = appearanceColor(textColor);
|
|
25377
|
+
const style = color === void 0 ? captionCss : `${captionCss}${captionCss.length > 0 ? `;` : ``}color:${color};`;
|
|
25378
|
+
if (appearanceAbsent(badge)) {
|
|
25379
|
+
if (tickled) return html`<div class="caption" part="caption" style=${style} title=${caption}><ixfx-pan-text mode="loop">${caption}</ixfx-pan-text></div>`;
|
|
25380
|
+
return html`<div class="caption" part="caption" style=${style} title=${caption}>${caption}</div>`;
|
|
25381
|
+
}
|
|
25382
|
+
const label = tickled ? html`<ixfx-pan-text mode="loop">${caption}</ixfx-pan-text>` : html`<span class="caption-text">${caption}</span>`;
|
|
25383
|
+
return html`<div class="caption has-badge" part="caption" style=${style} title=${caption}><span class="badge" part="badge">${badge}</span>${label}</div>`;
|
|
25384
|
+
}
|
|
25385
|
+
/** `color:<c>;` for a non-empty colour, else `nothing` (attribute removed). */
|
|
25386
|
+
#colorStyle(color) {
|
|
25387
|
+
const c = appearanceColor(color);
|
|
25388
|
+
return c === void 0 ? nothing : `color:${c};`;
|
|
24992
25389
|
}
|
|
24993
25390
|
/** Stop the active caption pan, if any, and forget it. */
|
|
24994
25391
|
#stopActiveCaptionPan() {
|
|
@@ -28191,6 +28588,16 @@ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data
|
|
|
28191
28588
|
* vertical rows are shorter relative to the viewport, so 3.
|
|
28192
28589
|
*/
|
|
28193
28590
|
const VIRTUAL_OVERSCAN = 3;
|
|
28591
|
+
/**
|
|
28592
|
+
* Layout for the per-item icon / badge wrappers `#decorateAdapterItem` adds.
|
|
28593
|
+
*
|
|
28594
|
+
* Applied as inline `cssText` rather than shadow CSS because `::slotted()` only
|
|
28595
|
+
* matches the host's *direct* children — these spans live inside a slotted
|
|
28596
|
+
* `<li>`, so a `::slotted()` rule could never reach them. The values still read
|
|
28597
|
+
* the CSS custom properties, so they stay themeable from the outside.
|
|
28598
|
+
*/
|
|
28599
|
+
const ITEM_ICON_EL_CSS = `flex:none;display:inline-flex;align-items:center;justify-content:center;overflow:hidden;margin-inline-end:var(--list-item-icon-gap,6px);width:var(--list-item-icon-size,1em);height:var(--list-item-icon-size,1em)`;
|
|
28600
|
+
const ITEM_BADGE_EL_CSS = `flex:none;display:inline-flex;align-items:center;margin-inline-start:auto;padding-inline-start:var(--list-item-badge-gap,8px)`;
|
|
28194
28601
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
28195
28602
|
function sameKeys(a, b) {
|
|
28196
28603
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -28600,7 +29007,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28600
29007
|
}
|
|
28601
29008
|
this.#syncSearchCommands();
|
|
28602
29009
|
}
|
|
28603
|
-
if ((changed.has(`items`) || changed.has(`formatter`) || changed.has(`keyProperty`)) && this.items !== void 0) {
|
|
29010
|
+
if ((changed.has(`items`) || changed.has(`formatter`) || changed.has(`keyProperty`) || changed.has(`icon`) || changed.has(`iconColor`) || changed.has(`textColor`) || changed.has(`badge`)) && this.items !== void 0) {
|
|
28604
29011
|
if (this.#suspendAdapterRebuild) this.#suspendAdapterRebuild = false;
|
|
28605
29012
|
else if (this.#virtualActive()) this.#rebuildAdapterVirtual();
|
|
28606
29013
|
else this.#rebuildFromAdapter();
|
|
@@ -28870,7 +29277,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28870
29277
|
const fresh = this.formatter ? this.formatter(data, index) : data;
|
|
28871
29278
|
if (el instanceof HTMLLIElement) {
|
|
28872
29279
|
for (const child of [...el.childNodes]) child.remove();
|
|
28873
|
-
this.#fillAdapterItem(el, fresh, data);
|
|
29280
|
+
this.#fillAdapterItem(el, fresh, data, index);
|
|
28874
29281
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
28875
29282
|
this._syncItemStates();
|
|
28876
29283
|
} else if (fresh instanceof Element && this.#isRowElement(fresh)) {
|
|
@@ -28925,17 +29332,47 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28925
29332
|
return content;
|
|
28926
29333
|
}
|
|
28927
29334
|
const li = document.createElement(`li`);
|
|
28928
|
-
this.#fillAdapterItem(li, content, item);
|
|
29335
|
+
this.#fillAdapterItem(li, content, item, index);
|
|
28929
29336
|
return li;
|
|
28930
29337
|
}
|
|
28931
|
-
#fillAdapterItem(li, content, item) {
|
|
29338
|
+
#fillAdapterItem(li, content, item, index) {
|
|
28932
29339
|
if (typeof content === `string`) li.textContent = content;
|
|
28933
29340
|
else if (content instanceof Node) li.appendChild(content);
|
|
28934
29341
|
else if (this.formatter && content !== void 0 && content !== null) render(content, li);
|
|
29342
|
+
this.#decorateAdapterItem(li, item, index);
|
|
28935
29343
|
this.#stampAdapterKey(li, item);
|
|
28936
29344
|
this.#itemData.set(li, item);
|
|
28937
29345
|
}
|
|
28938
29346
|
/**
|
|
29347
|
+
* Apply the per-item icon / text colour / badge to a built row. Split out
|
|
29348
|
+
* from {@link #fillAdapterItem} so the parts stay readable.
|
|
29349
|
+
*/
|
|
29350
|
+
#decorateAdapterItem(li, item, index) {
|
|
29351
|
+
const textColor = appearanceColor(this.textColor?.(item, index));
|
|
29352
|
+
if (textColor !== void 0) li.style.color = textColor;
|
|
29353
|
+
else li.style.removeProperty(`color`);
|
|
29354
|
+
const icon = this.icon?.(item, index);
|
|
29355
|
+
if (!appearanceAbsent(icon)) {
|
|
29356
|
+
const holder = document.createElement(`span`);
|
|
29357
|
+
holder.className = `item-icon`;
|
|
29358
|
+
holder.style.cssText = ITEM_ICON_EL_CSS;
|
|
29359
|
+
const iconColor = appearanceColor(this.iconColor?.(item, index));
|
|
29360
|
+
if (iconColor !== void 0) holder.style.color = iconColor;
|
|
29361
|
+
if (icon instanceof Node) holder.appendChild(icon);
|
|
29362
|
+
else render(icon, holder);
|
|
29363
|
+
li.prepend(holder);
|
|
29364
|
+
}
|
|
29365
|
+
const badge = this.badge?.(item, index);
|
|
29366
|
+
if (!appearanceAbsent(badge)) {
|
|
29367
|
+
const holder = document.createElement(`span`);
|
|
29368
|
+
holder.className = `item-badge`;
|
|
29369
|
+
holder.style.cssText = ITEM_BADGE_EL_CSS;
|
|
29370
|
+
if (badge instanceof Node) holder.appendChild(badge);
|
|
29371
|
+
else render(badge, holder);
|
|
29372
|
+
li.appendChild(holder);
|
|
29373
|
+
}
|
|
29374
|
+
}
|
|
29375
|
+
/**
|
|
28939
29376
|
* Whether a formatter-produced element is a whole row (used directly)
|
|
28940
29377
|
* rather than row content (rendered into a `<li>`): an `<li>`, already
|
|
28941
29378
|
* marked `[data-list-item]`, or a custom element.
|
|
@@ -29457,6 +29894,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
29457
29894
|
outline: 1px solid transparent;
|
|
29458
29895
|
}
|
|
29459
29896
|
|
|
29897
|
+
/* Per-item icon / badge (adapter mode) are styled inline by
|
|
29898
|
+
#decorateAdapterItem — ::slotted() only reaches the host's direct
|
|
29899
|
+
children, and these spans live inside a slotted <li>. */
|
|
29900
|
+
|
|
29460
29901
|
/* Custom shadow-DOM components handle their own internal layout/padding.
|
|
29461
29902
|
Wrapped in :where() so the selector stays at zero specificity and a
|
|
29462
29903
|
consumer's own :host rule (e.g. display: flex) wins without
|
|
@@ -29603,6 +30044,10 @@ __decorate([property({
|
|
|
29603
30044
|
attribute: `key-property`
|
|
29604
30045
|
})], VerticalListElement.prototype, "keyProperty", void 0);
|
|
29605
30046
|
__decorate([property({ attribute: false })], VerticalListElement.prototype, "formatter", void 0);
|
|
30047
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "icon", void 0);
|
|
30048
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "iconColor", void 0);
|
|
30049
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "textColor", void 0);
|
|
30050
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "badge", void 0);
|
|
29606
30051
|
__decorate([property({
|
|
29607
30052
|
type: Boolean,
|
|
29608
30053
|
attribute: `virtualize`,
|
|
@@ -30878,6 +31323,90 @@ __decorate([state()], IxfxHexEditorElement.prototype, "_containerH", void 0);
|
|
|
30878
31323
|
__decorate([state()], IxfxHexEditorElement.prototype, "_shadeMap", void 0);
|
|
30879
31324
|
__decorate([state()], IxfxHexEditorElement.prototype, "_focusSide", void 0);
|
|
30880
31325
|
IxfxHexEditorElement = __decorate([safeCustomElement(`ixfx-hex-editor`)], IxfxHexEditorElement);
|
|
31326
|
+
/**
|
|
31327
|
+
* Whether `n` deserves a preview: it has children and at least one of them
|
|
31328
|
+
* isn't legible right now — laid out without a label, or not laid out at
|
|
31329
|
+
* all (outside the `maxRadius` window).
|
|
31330
|
+
*/
|
|
31331
|
+
function needsChildPreview(n, visible, labelIds) {
|
|
31332
|
+
const children = n.node.children ?? [];
|
|
31333
|
+
if (children.length === 0) return false;
|
|
31334
|
+
const shown = visible.filter((v) => v.parentId === n.id);
|
|
31335
|
+
return shown.length < children.length || shown.some((v) => !labelIds.has(v.id));
|
|
31336
|
+
}
|
|
31337
|
+
/** Where node position `p` (disk units) lands on screen, in CSS px from the host's top-left. */
|
|
31338
|
+
function diskToHostPx(p, zoom, width, height) {
|
|
31339
|
+
const pxPerUnit = Math.min(width, height) / 2.1;
|
|
31340
|
+
return {
|
|
31341
|
+
x: width / 2 + (zoom.offset.x + zoom.zoom * p.x) * pxPerUnit,
|
|
31342
|
+
y: height / 2 + (zoom.offset.y + zoom.zoom * p.y) * pxPerUnit
|
|
31343
|
+
};
|
|
31344
|
+
}
|
|
31345
|
+
function renderChildPreview(preview, handlers) {
|
|
31346
|
+
if (!preview) return nothing;
|
|
31347
|
+
const children = preview.node.children ?? [];
|
|
31348
|
+
const listed = children.slice(0, 12);
|
|
31349
|
+
const more = children.length - listed.length;
|
|
31350
|
+
return html`
|
|
31351
|
+
<div
|
|
31352
|
+
class="child-preview ${preview.above ? `above` : `below`}"
|
|
31353
|
+
part="child-preview"
|
|
31354
|
+
role="dialog"
|
|
31355
|
+
aria-label=${`Children of ${preview.node.item.label}`}
|
|
31356
|
+
style="left: ${preview.anchor.x}px; top: ${preview.anchor.y}px"
|
|
31357
|
+
@pointerenter=${handlers.onPointerEnter}
|
|
31358
|
+
@pointerleave=${handlers.onPointerLeave}
|
|
31359
|
+
@pointerdown=${(e) => e.stopPropagation()}
|
|
31360
|
+
>
|
|
31361
|
+
<div class="child-preview-title">${preview.node.item.label}</div>
|
|
31362
|
+
<ul>
|
|
31363
|
+
${listed.map((child) => html`<li>
|
|
31364
|
+
<button type="button" tabindex="-1" @click=${() => handlers.onPick(child)}>${child.item.label}</button>
|
|
31365
|
+
</li>`)}
|
|
31366
|
+
</ul>
|
|
31367
|
+
${more > 0 ? html`<div class="child-preview-more">+${more} more</div>` : nothing}
|
|
31368
|
+
</div>
|
|
31369
|
+
`;
|
|
31370
|
+
}
|
|
31371
|
+
/**
|
|
31372
|
+
* Show/hide timing for the preview: shows at once for a qualifying node;
|
|
31373
|
+
* otherwise hides after a short grace period, unless the pointer is over the
|
|
31374
|
+
* popover itself — so it can travel from the node onto the list.
|
|
31375
|
+
*/
|
|
31376
|
+
function createChildPreviewTiming(host) {
|
|
31377
|
+
let hovered = false;
|
|
31378
|
+
let timer;
|
|
31379
|
+
const update = (id) => {
|
|
31380
|
+
clearTimeout(timer);
|
|
31381
|
+
const candidate = host.candidate(id);
|
|
31382
|
+
if (candidate !== void 0) {
|
|
31383
|
+
host.setPreviewId(candidate);
|
|
31384
|
+
return;
|
|
31385
|
+
}
|
|
31386
|
+
timer = setTimeout(() => {
|
|
31387
|
+
if (!hovered) host.setPreviewId(void 0);
|
|
31388
|
+
}, 250);
|
|
31389
|
+
};
|
|
31390
|
+
return {
|
|
31391
|
+
update,
|
|
31392
|
+
pointerEnter() {
|
|
31393
|
+
hovered = true;
|
|
31394
|
+
clearTimeout(timer);
|
|
31395
|
+
},
|
|
31396
|
+
pointerLeave(tickledId) {
|
|
31397
|
+
hovered = false;
|
|
31398
|
+
update(tickledId);
|
|
31399
|
+
},
|
|
31400
|
+
hide() {
|
|
31401
|
+
hovered = false;
|
|
31402
|
+
clearTimeout(timer);
|
|
31403
|
+
host.setPreviewId(void 0);
|
|
31404
|
+
},
|
|
31405
|
+
dispose() {
|
|
31406
|
+
clearTimeout(timer);
|
|
31407
|
+
}
|
|
31408
|
+
};
|
|
31409
|
+
}
|
|
30881
31410
|
//#endregion
|
|
30882
31411
|
//#region src/hyperbolic-tree/poincare-math.ts
|
|
30883
31412
|
const EPSILON = 1e-9;
|
|
@@ -30982,8 +31511,625 @@ function geodesicInterpolate(a, b, t) {
|
|
|
30982
31511
|
if (localNorm < EPSILON) return a;
|
|
30983
31512
|
return mobiusFromOrigin(complexScale(complexScale(bLocal, 1 / localNorm), euclideanRadiusForHyperbolicDistance(hyperbolicDistanceForEuclideanRadius(localNorm) * t)), a);
|
|
30984
31513
|
}
|
|
31514
|
+
const identityIsometry = {
|
|
31515
|
+
a: {
|
|
31516
|
+
x: 1,
|
|
31517
|
+
y: 0
|
|
31518
|
+
},
|
|
31519
|
+
b: {
|
|
31520
|
+
x: 0,
|
|
31521
|
+
y: 0
|
|
31522
|
+
}
|
|
31523
|
+
};
|
|
31524
|
+
/** Apply `m` to `z`. */
|
|
31525
|
+
function applyIsometry(m, z) {
|
|
31526
|
+
return clampToDisk(complexDiv(complexAdd(complexMul(m.a, z), m.b), complexAdd(complexMul(complexConjugate(m.b), z), complexConjugate(m.a))));
|
|
31527
|
+
}
|
|
31528
|
+
/** `outer ∘ inner` — apply `inner` first, then `outer`. */
|
|
31529
|
+
function composeIsometry(outer, inner) {
|
|
31530
|
+
const a = complexAdd(complexMul(outer.a, inner.a), complexMul(outer.b, complexConjugate(inner.b)));
|
|
31531
|
+
const b = complexAdd(complexMul(outer.a, inner.b), complexMul(outer.b, complexConjugate(inner.a)));
|
|
31532
|
+
const det = Math.sqrt(Math.max(EPSILON, a.x * a.x + a.y * a.y - b.x * b.x - b.y * b.y));
|
|
31533
|
+
return {
|
|
31534
|
+
a: complexScale(a, 1 / det),
|
|
31535
|
+
b: complexScale(b, 1 / det)
|
|
31536
|
+
};
|
|
31537
|
+
}
|
|
31538
|
+
function invertIsometry(m) {
|
|
31539
|
+
return {
|
|
31540
|
+
a: complexConjugate(m.a),
|
|
31541
|
+
b: complexScale(m.b, -1)
|
|
31542
|
+
};
|
|
31543
|
+
}
|
|
31544
|
+
/** Pure translation carrying the origin to `p` (same map as {@link mobiusFromOrigin}). */
|
|
31545
|
+
function translationFromOrigin(p) {
|
|
31546
|
+
const scale = 1 / Math.sqrt(Math.max(EPSILON, 1 - p.x * p.x - p.y * p.y));
|
|
31547
|
+
return {
|
|
31548
|
+
a: {
|
|
31549
|
+
x: scale,
|
|
31550
|
+
y: 0
|
|
31551
|
+
},
|
|
31552
|
+
b: complexScale(p, scale)
|
|
31553
|
+
};
|
|
31554
|
+
}
|
|
31555
|
+
/** `z ↦ e^{iθ}·z`. */
|
|
31556
|
+
function rotationIsometry(theta) {
|
|
31557
|
+
return {
|
|
31558
|
+
a: {
|
|
31559
|
+
x: Math.cos(theta / 2),
|
|
31560
|
+
y: Math.sin(theta / 2)
|
|
31561
|
+
},
|
|
31562
|
+
b: {
|
|
31563
|
+
x: 0,
|
|
31564
|
+
y: 0
|
|
31565
|
+
}
|
|
31566
|
+
};
|
|
31567
|
+
}
|
|
31568
|
+
/**
|
|
31569
|
+
* The view map that sends canonical point `center` to the origin, then
|
|
31570
|
+
* rotates by `rotation` radians: `z ↦ e^{i·rotation}·mobiusToOrigin(z, center)`.
|
|
31571
|
+
*/
|
|
31572
|
+
function viewIsometry(center, rotation = 0) {
|
|
31573
|
+
return composeIsometry(rotationIsometry(rotation), invertIsometry(translationFromOrigin(center)));
|
|
31574
|
+
}
|
|
31575
|
+
/** Decompose `m` into the `{ center, rotation }` accepted by {@link viewIsometry}. */
|
|
31576
|
+
function decomposeViewIsometry(m) {
|
|
31577
|
+
return {
|
|
31578
|
+
center: applyIsometry(invertIsometry(m), {
|
|
31579
|
+
x: 0,
|
|
31580
|
+
y: 0
|
|
31581
|
+
}),
|
|
31582
|
+
rotation: 2 * Math.atan2(m.a.y, m.a.x)
|
|
31583
|
+
};
|
|
31584
|
+
}
|
|
31585
|
+
function geodesicSegment(p, q) {
|
|
31586
|
+
const det = p.x * q.y - p.y * q.x;
|
|
31587
|
+
const chord = Math.hypot(q.x - p.x, q.y - p.y);
|
|
31588
|
+
if (Math.abs(det) < 1e-6 * Math.max(chord, EPSILON)) return { kind: `line` };
|
|
31589
|
+
const rp = (p.x * p.x + p.y * p.y + 1) / 2;
|
|
31590
|
+
const rq = (q.x * q.x + q.y * q.y + 1) / 2;
|
|
31591
|
+
const center = {
|
|
31592
|
+
x: (rp * q.y - rq * p.y) / det,
|
|
31593
|
+
y: (p.x * rq - q.x * rp) / det
|
|
31594
|
+
};
|
|
31595
|
+
return {
|
|
31596
|
+
kind: `arc`,
|
|
31597
|
+
center,
|
|
31598
|
+
radius: Math.sqrt(Math.max(0, center.x * center.x + center.y * center.y - 1)),
|
|
31599
|
+
sweep: (p.x - center.x) * (q.y - center.y) - (p.y - center.y) * (q.x - center.x) > 0 ? 1 : 0
|
|
31600
|
+
};
|
|
31601
|
+
}
|
|
31602
|
+
/** SVG path `d` for the geodesic from `p` to `q`, with coordinates multiplied by `scale`. */
|
|
31603
|
+
function geodesicPath(p, q, scale = 1) {
|
|
31604
|
+
const fmt = (n) => (n * scale).toFixed(2);
|
|
31605
|
+
const segment = geodesicSegment(p, q);
|
|
31606
|
+
const start = `M${fmt(p.x)},${fmt(p.y)}`;
|
|
31607
|
+
if (segment.kind === `line` || segment.radius * scale > 1e6) return `${start} L${fmt(q.x)},${fmt(q.y)}`;
|
|
31608
|
+
return `${start} A${fmt(segment.radius)},${fmt(segment.radius)} 0 0 ${segment.sweep} ${fmt(q.x)},${fmt(q.y)}`;
|
|
31609
|
+
}
|
|
31610
|
+
//#endregion
|
|
31611
|
+
//#region src/hyperbolic-tree/view-motion.ts
|
|
31612
|
+
/** Wrap an angle into `(-π, π]`. */
|
|
31613
|
+
function wrapAngle(theta) {
|
|
31614
|
+
return Math.atan2(Math.sin(theta), Math.cos(theta));
|
|
31615
|
+
}
|
|
31616
|
+
function easeInOutCubic(t) {
|
|
31617
|
+
return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
31618
|
+
}
|
|
31619
|
+
function prefersReducedMotion$1() {
|
|
31620
|
+
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
31621
|
+
}
|
|
31622
|
+
/**
|
|
31623
|
+
* The view after dragging from `start` to `current` (both disk
|
|
31624
|
+
* coordinates), starting from `baseView`. Applies the hyperbolic translation
|
|
31625
|
+
* along the geodesic carrying the grabbed point to the pointer — a true
|
|
31626
|
+
* isometry, so the grabbed point tracks the pointer exactly and the tree
|
|
31627
|
+
* never distorts. (Adding a Euclidean offset to the canonical center
|
|
31628
|
+
* instead is not an isometry: it lurches badly near the disk boundary.)
|
|
31629
|
+
*/
|
|
31630
|
+
function panView(baseView, start, current) {
|
|
31631
|
+
const toStart = translationFromOrigin(start);
|
|
31632
|
+
return composeIsometry(composeIsometry(toStart, composeIsometry(translationFromOrigin(mobiusToOrigin(current, start)), invertIsometry(toStart))), baseView);
|
|
31633
|
+
}
|
|
31634
|
+
/**
|
|
31635
|
+
* A focus-change animation from view `from` — the old view, re-expressed in
|
|
31636
|
+
* the *new* focus's local frame, so the new focus currently sits somewhere
|
|
31637
|
+
* off-center — to a settled view with the new focus at the center. The
|
|
31638
|
+
* center travels along the hyperbolic geodesic; the rotation is held, so the
|
|
31639
|
+
* tree glides into place without spinning, and the settled view keeps
|
|
31640
|
+
* whatever orientation it arrived with (`settledRotation`).
|
|
31641
|
+
*/
|
|
31642
|
+
function focusTransition(from) {
|
|
31643
|
+
const { center, rotation } = decomposeViewIsometry(from);
|
|
31644
|
+
const settledRotation = wrapAngle(rotation);
|
|
31645
|
+
const origin = {
|
|
31646
|
+
x: 0,
|
|
31647
|
+
y: 0
|
|
31648
|
+
};
|
|
31649
|
+
return {
|
|
31650
|
+
settledRotation,
|
|
31651
|
+
viewAt: (t) => {
|
|
31652
|
+
const eased = easeInOutCubic(Math.min(1, Math.max(0, t)));
|
|
31653
|
+
return viewIsometry(geodesicInterpolate(center, origin, eased), settledRotation);
|
|
31654
|
+
}
|
|
31655
|
+
};
|
|
31656
|
+
}
|
|
31657
|
+
//#endregion
|
|
31658
|
+
//#region src/hyperbolic-tree/gestures.ts
|
|
31659
|
+
const MIN_ZOOM = .5;
|
|
31660
|
+
/**
|
|
31661
|
+
* Keep the view center over the disk (so you can't zoom off into empty
|
|
31662
|
+
* space), and re-center entirely once the whole disk fits again.
|
|
31663
|
+
*/
|
|
31664
|
+
function clampZoomState(state) {
|
|
31665
|
+
const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom));
|
|
31666
|
+
if (zoom <= 1) return {
|
|
31667
|
+
zoom,
|
|
31668
|
+
offset: {
|
|
31669
|
+
x: 0,
|
|
31670
|
+
y: 0
|
|
31671
|
+
}
|
|
31672
|
+
};
|
|
31673
|
+
const limit = zoom - 1;
|
|
31674
|
+
const length = Math.hypot(state.offset.x, state.offset.y);
|
|
31675
|
+
return {
|
|
31676
|
+
zoom,
|
|
31677
|
+
offset: length > limit ? {
|
|
31678
|
+
x: state.offset.x * limit / length,
|
|
31679
|
+
y: state.offset.y * limit / length
|
|
31680
|
+
} : state.offset
|
|
31681
|
+
};
|
|
31682
|
+
}
|
|
31683
|
+
/**
|
|
31684
|
+
* Zoom by `factor` keeping the disk point under `screenPoint` fixed on
|
|
31685
|
+
* screen — the "zoom where I'm pointing" behaviour of maps and image viewers.
|
|
31686
|
+
*/
|
|
31687
|
+
function zoomAt(state, screenPoint, factor) {
|
|
31688
|
+
const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom * factor));
|
|
31689
|
+
const disk = screenToDisk(screenPoint, state);
|
|
31690
|
+
return clampZoomState({
|
|
31691
|
+
zoom,
|
|
31692
|
+
offset: {
|
|
31693
|
+
x: screenPoint.x - zoom * disk.x,
|
|
31694
|
+
y: screenPoint.y - zoom * disk.y
|
|
31695
|
+
}
|
|
31696
|
+
});
|
|
31697
|
+
}
|
|
31698
|
+
/**
|
|
31699
|
+
* A client-space position in screen units: disk radii from the SVG's center,
|
|
31700
|
+
* before zoom. The disk fills the SVG's smaller dimension, with the
|
|
31701
|
+
* viewBox's 5% margin.
|
|
31702
|
+
*/
|
|
31703
|
+
function clientToScreen(rect, clientX, clientY) {
|
|
31704
|
+
const unitsPerPixel = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
31705
|
+
return {
|
|
31706
|
+
x: (clientX - (rect.left + rect.width / 2)) * unitsPerPixel,
|
|
31707
|
+
y: (clientY - (rect.top + rect.height / 2)) * unitsPerPixel
|
|
31708
|
+
};
|
|
31709
|
+
}
|
|
31710
|
+
function screenToDisk(p, state) {
|
|
31711
|
+
const zoom = state.zoom || 1;
|
|
31712
|
+
return {
|
|
31713
|
+
x: (p.x - state.offset.x) / zoom,
|
|
31714
|
+
y: (p.y - state.offset.y) / zoom
|
|
31715
|
+
};
|
|
31716
|
+
}
|
|
31717
|
+
/** Is `event` on empty disk space (the SVG itself or the boundary circle), rather than a node? */
|
|
31718
|
+
function isBackground(event) {
|
|
31719
|
+
const target = event.target;
|
|
31720
|
+
return target === event.currentTarget || target?.classList.contains(`disk-boundary`) === true;
|
|
31721
|
+
}
|
|
31722
|
+
function createGestureHandlers(host) {
|
|
31723
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
31724
|
+
let pan;
|
|
31725
|
+
let pinch;
|
|
31726
|
+
const screenOf = (event) => {
|
|
31727
|
+
const rect = host.svgRect();
|
|
31728
|
+
return rect ? clientToScreen(rect, event.clientX, event.clientY) : void 0;
|
|
31729
|
+
};
|
|
31730
|
+
const diskOf = (screen) => clampToDisk(screenToDisk(screen, host.zoomState()), .98);
|
|
31731
|
+
const midpoint = (a, b) => ({
|
|
31732
|
+
x: (a.x + b.x) / 2,
|
|
31733
|
+
y: (a.y + b.y) / 2
|
|
31734
|
+
});
|
|
31735
|
+
const startPan = (pointerId, screen) => {
|
|
31736
|
+
pan = {
|
|
31737
|
+
pointerId,
|
|
31738
|
+
start: diskOf(screen),
|
|
31739
|
+
baseView: host.takeView()
|
|
31740
|
+
};
|
|
31741
|
+
};
|
|
31742
|
+
const startPinch = () => {
|
|
31743
|
+
const [[idA, a], [idB, b]] = [...pointers.entries()];
|
|
31744
|
+
pan = void 0;
|
|
31745
|
+
const { zoom } = host.zoomState();
|
|
31746
|
+
pinch = {
|
|
31747
|
+
ids: [idA, idB],
|
|
31748
|
+
startDistance: Math.max(1e-6, Math.hypot(a.x - b.x, a.y - b.y)),
|
|
31749
|
+
startZoom: zoom,
|
|
31750
|
+
anchor: screenToDisk(midpoint(a, b), host.zoomState())
|
|
31751
|
+
};
|
|
31752
|
+
};
|
|
31753
|
+
return {
|
|
31754
|
+
onPointerDown(event) {
|
|
31755
|
+
if (!isBackground(event)) return;
|
|
31756
|
+
host.focusTree();
|
|
31757
|
+
const screen = screenOf(event);
|
|
31758
|
+
if (!screen) return;
|
|
31759
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
31760
|
+
pointers.set(event.pointerId, screen);
|
|
31761
|
+
if (pointers.size === 2 && host.zoomGesturesEnabled()) startPinch();
|
|
31762
|
+
else if (pointers.size === 1) startPan(event.pointerId, screen);
|
|
31763
|
+
},
|
|
31764
|
+
onPointerMove(event) {
|
|
31765
|
+
if (!pointers.has(event.pointerId)) return;
|
|
31766
|
+
const screen = screenOf(event);
|
|
31767
|
+
if (!screen) return;
|
|
31768
|
+
pointers.set(event.pointerId, screen);
|
|
31769
|
+
if (pinch) {
|
|
31770
|
+
const a = pointers.get(pinch.ids[0]);
|
|
31771
|
+
const b = pointers.get(pinch.ids[1]);
|
|
31772
|
+
if (!a || !b) return;
|
|
31773
|
+
const zoom = pinch.startZoom * Math.hypot(a.x - b.x, a.y - b.y) / pinch.startDistance;
|
|
31774
|
+
const mid = midpoint(a, b);
|
|
31775
|
+
const clampedZoom = Math.min(8, Math.max(MIN_ZOOM, zoom));
|
|
31776
|
+
host.setZoomState(clampZoomState({
|
|
31777
|
+
zoom: clampedZoom,
|
|
31778
|
+
offset: {
|
|
31779
|
+
x: mid.x - clampedZoom * pinch.anchor.x,
|
|
31780
|
+
y: mid.y - clampedZoom * pinch.anchor.y
|
|
31781
|
+
}
|
|
31782
|
+
}));
|
|
31783
|
+
} else if (pan && pan.pointerId === event.pointerId) host.setView(panView(pan.baseView, pan.start, diskOf(screen)));
|
|
31784
|
+
},
|
|
31785
|
+
onPointerUp(event) {
|
|
31786
|
+
if (!pointers.delete(event.pointerId)) return;
|
|
31787
|
+
if (pinch) {
|
|
31788
|
+
pinch = void 0;
|
|
31789
|
+
const [remaining] = [...pointers.entries()];
|
|
31790
|
+
if (remaining) startPan(remaining[0], remaining[1]);
|
|
31791
|
+
} else if (pan?.pointerId === event.pointerId) pan = void 0;
|
|
31792
|
+
},
|
|
31793
|
+
/**
|
|
31794
|
+
* Ctrl/⌘-wheel — which is also what a trackpad pinch produces — always
|
|
31795
|
+
* zooms while the pointer is over the tree. A plain wheel only zooms
|
|
31796
|
+
* once the tree has keyboard focus (the library's focus-gated wheel
|
|
31797
|
+
* convention), so scrolling past it doesn't get hijacked.
|
|
31798
|
+
*/
|
|
31799
|
+
onWheel(event) {
|
|
31800
|
+
const modified = event.ctrlKey || event.metaKey;
|
|
31801
|
+
if (!host.zoomGesturesEnabled() || !modified && !host.hasFocus()) return;
|
|
31802
|
+
const screen = screenOf(event);
|
|
31803
|
+
if (!screen) return;
|
|
31804
|
+
event.preventDefault();
|
|
31805
|
+
const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
|
|
31806
|
+
const factor = Math.exp(-delta * (modified ? .01 : .002));
|
|
31807
|
+
host.setZoomState(zoomAt(host.zoomState(), screen, factor));
|
|
31808
|
+
},
|
|
31809
|
+
/** Forget any in-progress pan (e.g. when a focus change takes over the view). */
|
|
31810
|
+
cancel() {
|
|
31811
|
+
pan = void 0;
|
|
31812
|
+
pinch = void 0;
|
|
31813
|
+
pointers.clear();
|
|
31814
|
+
}
|
|
31815
|
+
};
|
|
31816
|
+
}
|
|
31817
|
+
//#endregion
|
|
31818
|
+
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
31819
|
+
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
31820
|
+
const hyperbolicTreeStyles = css`
|
|
31821
|
+
:host {
|
|
31822
|
+
--hyperbolictree-node-color: var(--surface-4);
|
|
31823
|
+
--hyperbolictree-node-text: var(--surface-3-text);
|
|
31824
|
+
--hyperbolictree-edge-color: var(--surface-6);
|
|
31825
|
+
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
31826
|
+
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
31827
|
+
--hyperbolictree-font-size: 13px;
|
|
31828
|
+
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
31829
|
+
--hyperbolictree-box-text: var(--surface-4-text);
|
|
31830
|
+
--hyperbolictree-box-border: var(--surface-2);
|
|
31831
|
+
--hyperbolictree-branch-hue-start: 250;
|
|
31832
|
+
--hyperbolictree-branch-chroma: 0.13;
|
|
31833
|
+
--hyperbolictree-branch-lightness: 62%;
|
|
31834
|
+
--hyperbolictree-branch-mix: 55%;
|
|
31835
|
+
|
|
31836
|
+
display: block;
|
|
31837
|
+
position: relative;
|
|
31838
|
+
overflow: hidden;
|
|
31839
|
+
outline: none;
|
|
31840
|
+
overscroll-behavior: contain;
|
|
31841
|
+
}
|
|
31842
|
+
|
|
31843
|
+
/*
|
|
31844
|
+
* delegatesFocus means the host itself reports as focused whenever
|
|
31845
|
+
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
31846
|
+
* the host also picks up the browser's own default focus ring/fill
|
|
31847
|
+
* sized to the whole box.
|
|
31848
|
+
*/
|
|
31849
|
+
:host(:focus),
|
|
31850
|
+
:host(:focus-visible) {
|
|
31851
|
+
outline: none;
|
|
31852
|
+
}
|
|
31853
|
+
|
|
31854
|
+
.hyperbolictree {
|
|
31855
|
+
display: block;
|
|
31856
|
+
width: 100%;
|
|
31857
|
+
height: 100%;
|
|
31858
|
+
outline: none;
|
|
31859
|
+
touch-action: none;
|
|
31860
|
+
overscroll-behavior: contain;
|
|
31861
|
+
}
|
|
31862
|
+
|
|
31863
|
+
.disk-boundary {
|
|
31864
|
+
fill: none;
|
|
31865
|
+
stroke: var(--hyperbolictree-boundary-color);
|
|
31866
|
+
stroke-width: 1;
|
|
31867
|
+
}
|
|
31868
|
+
|
|
31869
|
+
.edge {
|
|
31870
|
+
fill: none;
|
|
31871
|
+
stroke: var(--hyperbolictree-edge-color);
|
|
31872
|
+
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
31873
|
+
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
31874
|
+
/* Constant on-screen width, however far the disk is zoomed. */
|
|
31875
|
+
vector-effect: non-scaling-stroke;
|
|
31876
|
+
}
|
|
31877
|
+
|
|
31878
|
+
.node {
|
|
31879
|
+
cursor: pointer;
|
|
31880
|
+
}
|
|
31881
|
+
|
|
31882
|
+
/*
|
|
31883
|
+
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
31884
|
+
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
31885
|
+
* so it stays in keeping with — and legible in — the current theme.
|
|
31886
|
+
* Only sets the base colour vars, so selected/tickled states still win.
|
|
31887
|
+
*/
|
|
31888
|
+
:host([color-by-branch]) .node,
|
|
31889
|
+
:host([color-by-branch]) .edge {
|
|
31890
|
+
--_branch-color: oklch(
|
|
31891
|
+
var(--hyperbolictree-branch-lightness)
|
|
31892
|
+
var(--hyperbolictree-branch-chroma)
|
|
31893
|
+
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
31894
|
+
);
|
|
31895
|
+
}
|
|
31896
|
+
|
|
31897
|
+
:host([color-by-branch]) .node {
|
|
31898
|
+
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
31899
|
+
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
31900
|
+
}
|
|
31901
|
+
|
|
31902
|
+
:host([color-by-branch]) .edge {
|
|
31903
|
+
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
31904
|
+
}
|
|
31905
|
+
|
|
31906
|
+
.node-hit {
|
|
31907
|
+
fill: transparent;
|
|
31908
|
+
pointer-events: all;
|
|
31909
|
+
}
|
|
31910
|
+
|
|
31911
|
+
.node-dot {
|
|
31912
|
+
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
31913
|
+
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
31914
|
+
stroke: var(--surface-2);
|
|
31915
|
+
stroke-width: 1;
|
|
31916
|
+
pointer-events: none;
|
|
31917
|
+
}
|
|
31918
|
+
|
|
31919
|
+
.node-box {
|
|
31920
|
+
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
31921
|
+
stroke: var(--hyperbolictree-box-border);
|
|
31922
|
+
stroke-width: 1.5;
|
|
31923
|
+
/* Keep the border crisp however far the box has been scaled down. */
|
|
31924
|
+
vector-effect: non-scaling-stroke;
|
|
31925
|
+
}
|
|
31926
|
+
|
|
31927
|
+
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
31928
|
+
.node-box.marker {
|
|
31929
|
+
stroke-width: 1;
|
|
31930
|
+
}
|
|
31931
|
+
|
|
31932
|
+
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
31933
|
+
.hyperbolictree-label {
|
|
31934
|
+
fill: var(--hyperbolictree-node-text);
|
|
31935
|
+
cursor: pointer;
|
|
31936
|
+
user-select: none;
|
|
31937
|
+
}
|
|
31938
|
+
|
|
31939
|
+
.box-label {
|
|
31940
|
+
fill: var(--hyperbolictree-box-text);
|
|
31941
|
+
pointer-events: none;
|
|
31942
|
+
}
|
|
31943
|
+
|
|
31944
|
+
/*
|
|
31945
|
+
* State fills come paired with their theme text tokens: a box label sits
|
|
31946
|
+
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
31947
|
+
* --item-text-selected on top, not the resting box text colour.
|
|
31948
|
+
*/
|
|
31949
|
+
.node.tickled .node-dot,
|
|
31950
|
+
.node.tickled .node-box {
|
|
31951
|
+
fill: var(--item-bg-tickled);
|
|
31952
|
+
}
|
|
31953
|
+
|
|
31954
|
+
.node.tickled .box-label {
|
|
31955
|
+
fill: var(--item-text-tickled);
|
|
31956
|
+
}
|
|
31957
|
+
|
|
31958
|
+
.node.selected .node-dot,
|
|
31959
|
+
.node.selected .node-box {
|
|
31960
|
+
fill: var(--item-bg-selected);
|
|
31961
|
+
}
|
|
31962
|
+
|
|
31963
|
+
.node.selected .box-label {
|
|
31964
|
+
fill: var(--item-text-selected);
|
|
31965
|
+
}
|
|
31966
|
+
|
|
31967
|
+
.node.selected.tickled .node-dot,
|
|
31968
|
+
.node.selected.tickled .node-box {
|
|
31969
|
+
fill: var(--item-bg-selected-tickled);
|
|
31970
|
+
}
|
|
31971
|
+
|
|
31972
|
+
.node.focused .node-dot,
|
|
31973
|
+
.node.focused .node-box {
|
|
31974
|
+
stroke: var(--accent);
|
|
31975
|
+
stroke-width: 3;
|
|
31976
|
+
}
|
|
31977
|
+
|
|
31978
|
+
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
31979
|
+
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
31980
|
+
paint-order: stroke;
|
|
31981
|
+
stroke: var(--surface-2);
|
|
31982
|
+
stroke-width: 4px;
|
|
31983
|
+
stroke-linejoin: round;
|
|
31984
|
+
}
|
|
31985
|
+
|
|
31986
|
+
.node.filtered {
|
|
31987
|
+
opacity: 0.3;
|
|
31988
|
+
}
|
|
31989
|
+
|
|
31990
|
+
.node.loading .node-dot,
|
|
31991
|
+
.node.loading .node-box {
|
|
31992
|
+
opacity: 0.6;
|
|
31993
|
+
}
|
|
31994
|
+
|
|
31995
|
+
|
|
31996
|
+
/* Child preview: a clickable list of a crowded node's children. */
|
|
31997
|
+
.child-preview {
|
|
31998
|
+
position: absolute;
|
|
31999
|
+
z-index: 1;
|
|
32000
|
+
min-width: 8rem;
|
|
32001
|
+
max-width: min(18rem, 60%);
|
|
32002
|
+
max-height: 45%;
|
|
32003
|
+
overflow: auto;
|
|
32004
|
+
padding: var(--space-xs, 4px);
|
|
32005
|
+
background: var(--surface-3);
|
|
32006
|
+
color: var(--surface-3-text);
|
|
32007
|
+
border: var(--border-subtle);
|
|
32008
|
+
border-radius: var(--radius-m, 6px);
|
|
32009
|
+
box-shadow: var(--shadow-m);
|
|
32010
|
+
font-size: var(--hyperbolictree-font-size);
|
|
32011
|
+
transform: translate(-50%, 14px);
|
|
32012
|
+
}
|
|
32013
|
+
|
|
32014
|
+
.child-preview.above {
|
|
32015
|
+
transform: translate(-50%, calc(-100% - 14px));
|
|
32016
|
+
}
|
|
32017
|
+
|
|
32018
|
+
.child-preview-title {
|
|
32019
|
+
padding: 2px 6px 4px;
|
|
32020
|
+
font-weight: 600;
|
|
32021
|
+
overflow: hidden;
|
|
32022
|
+
text-overflow: ellipsis;
|
|
32023
|
+
white-space: nowrap;
|
|
32024
|
+
}
|
|
32025
|
+
|
|
32026
|
+
.child-preview ul {
|
|
32027
|
+
margin: 0;
|
|
32028
|
+
padding: 0;
|
|
32029
|
+
list-style: none;
|
|
32030
|
+
}
|
|
32031
|
+
|
|
32032
|
+
.child-preview button {
|
|
32033
|
+
display: block;
|
|
32034
|
+
width: 100%;
|
|
32035
|
+
padding: 2px 6px;
|
|
32036
|
+
border: none;
|
|
32037
|
+
border-radius: var(--radius-s, 4px);
|
|
32038
|
+
background: none;
|
|
32039
|
+
color: inherit;
|
|
32040
|
+
font: inherit;
|
|
32041
|
+
text-align: start;
|
|
32042
|
+
overflow: hidden;
|
|
32043
|
+
text-overflow: ellipsis;
|
|
32044
|
+
white-space: nowrap;
|
|
32045
|
+
cursor: pointer;
|
|
32046
|
+
}
|
|
32047
|
+
|
|
32048
|
+
.child-preview button:hover {
|
|
32049
|
+
background: var(--item-bg-tickled);
|
|
32050
|
+
color: var(--item-text-tickled);
|
|
32051
|
+
}
|
|
32052
|
+
|
|
32053
|
+
.child-preview-more {
|
|
32054
|
+
padding: 2px 6px;
|
|
32055
|
+
color: var(--surface-muted-text);
|
|
32056
|
+
}
|
|
32057
|
+
`;
|
|
30985
32058
|
//#endregion
|
|
30986
32059
|
//#region src/hyperbolic-tree/layout.ts
|
|
32060
|
+
const ID_SEPARATOR = `\u001F`;
|
|
32061
|
+
/**
|
|
32062
|
+
* Path-derived ids for `children` of the node with id `parentId`. `TreeItem.key`
|
|
32063
|
+
* is meant to be unique, but real data (e.g. a file system, where `readme.md`
|
|
32064
|
+
* recurs in many folders) often only has keys unique among siblings. Keying
|
|
32065
|
+
* the layout on `item.key` alone would silently merge those nodes — edges get
|
|
32066
|
+
* attached to the wrong parent and clicks focus the wrong node — so the
|
|
32067
|
+
* layout identifies nodes by their key path instead. Repeated keys among
|
|
32068
|
+
* siblings are disambiguated by occurrence count. The root's id is its key.
|
|
32069
|
+
*/
|
|
32070
|
+
function hyperbolicChildIds(parentId, children) {
|
|
32071
|
+
const seen = /* @__PURE__ */ new Map();
|
|
32072
|
+
return children.map((child) => {
|
|
32073
|
+
const key = child.item.key;
|
|
32074
|
+
const count = seen.get(key) ?? 0;
|
|
32075
|
+
seen.set(key, count + 1);
|
|
32076
|
+
return `${parentId}${ID_SEPARATOR}${count === 0 ? key : `${key}#${count}`}`;
|
|
32077
|
+
});
|
|
32078
|
+
}
|
|
32079
|
+
/** Depth (root is 0) of the node with layout id `id`. */
|
|
32080
|
+
function hyperbolicIdDepth(id) {
|
|
32081
|
+
return id.split(ID_SEPARATOR).length - 1;
|
|
32082
|
+
}
|
|
32083
|
+
/**
|
|
32084
|
+
* Layout id of the root's child that `id` descends from (or is) — i.e. which
|
|
32085
|
+
* top-level branch it belongs to. `undefined` for the root itself.
|
|
32086
|
+
*/
|
|
32087
|
+
function hyperbolicBranchId(id) {
|
|
32088
|
+
const parts = id.split(ID_SEPARATOR);
|
|
32089
|
+
return parts.length < 2 ? void 0 : `${parts[0]}${ID_SEPARATOR}${parts[1]}`;
|
|
32090
|
+
}
|
|
32091
|
+
/** Whether layout id `id` is a strict descendant of `ancestorId`. */
|
|
32092
|
+
function isHyperbolicDescendantId(id, ancestorId) {
|
|
32093
|
+
return id.startsWith(`${ancestorId}${ID_SEPARATOR}`);
|
|
32094
|
+
}
|
|
32095
|
+
/** Chain of nodes from `root` down to the node with layout id `id`, or `undefined` if absent. */
|
|
32096
|
+
function findHyperbolicPathById(root, id) {
|
|
32097
|
+
if (!root) return void 0;
|
|
32098
|
+
const walk = (node, nodeId) => {
|
|
32099
|
+
if (nodeId === id) return [node];
|
|
32100
|
+
if (!isHyperbolicDescendantId(id, nodeId)) return void 0;
|
|
32101
|
+
const children = node.children ?? [];
|
|
32102
|
+
const ids = hyperbolicChildIds(nodeId, children);
|
|
32103
|
+
for (let i = 0; i < children.length; i++) {
|
|
32104
|
+
const hit = walk(children[i], ids[i]);
|
|
32105
|
+
if (hit) return [node, ...hit];
|
|
32106
|
+
}
|
|
32107
|
+
};
|
|
32108
|
+
return walk(root, root.item.key);
|
|
32109
|
+
}
|
|
32110
|
+
/** Layout id of `target` (matched by object identity) within `root`, or `undefined` if absent. */
|
|
32111
|
+
function findHyperbolicIdOf(root, target) {
|
|
32112
|
+
if (!root) return void 0;
|
|
32113
|
+
const walk = (node, nodeId) => {
|
|
32114
|
+
if (node === target) return nodeId;
|
|
32115
|
+
const children = node.children ?? [];
|
|
32116
|
+
const ids = hyperbolicChildIds(nodeId, children);
|
|
32117
|
+
for (let i = 0; i < children.length; i++) {
|
|
32118
|
+
const hit = walk(children[i], ids[i]);
|
|
32119
|
+
if (hit !== void 0) return hit;
|
|
32120
|
+
}
|
|
32121
|
+
};
|
|
32122
|
+
return walk(root, root.item.key);
|
|
32123
|
+
}
|
|
32124
|
+
/** Key path (from the root's children) to the first node, depth-first, satisfying `predicate`. */
|
|
32125
|
+
function findHyperbolicKeyPath(nodes, predicate, prefix = []) {
|
|
32126
|
+
for (const node of nodes) {
|
|
32127
|
+
const path = [...prefix, node.item.key];
|
|
32128
|
+
if (predicate(node)) return path;
|
|
32129
|
+
const hit = node.children?.length ? findHyperbolicKeyPath(node.children, predicate, path) : void 0;
|
|
32130
|
+
if (hit) return hit;
|
|
32131
|
+
}
|
|
32132
|
+
}
|
|
30987
32133
|
/** Depth-first search for the node with `key`. Returns `undefined` when absent. */
|
|
30988
32134
|
function findHyperbolicNode(root, key) {
|
|
30989
32135
|
if (!root) return void 0;
|
|
@@ -31058,21 +32204,58 @@ function stableUnitJitter(key) {
|
|
|
31058
32204
|
* radially).
|
|
31059
32205
|
*/
|
|
31060
32206
|
const CHAIN_JITTER_RADIANS = .22;
|
|
32207
|
+
/** Default minimum hyperbolic distance between adjacent siblings. See {@link childDistance}. */
|
|
32208
|
+
const DEFAULT_SIBLING_SPACING = .5;
|
|
32209
|
+
/**
|
|
32210
|
+
* Hyperbolic distance from parent to a child whose angular slice (in the
|
|
32211
|
+
* parent's frame) is `share`, such that the child sits at least `spacing`
|
|
32212
|
+
* from a neighbour one slice-width away: from the hyperbolic law of cosines
|
|
32213
|
+
* for an isosceles triangle, `sinh(d/2) = sinh(L)·sin(Δθ/2)`, solved for L.
|
|
32214
|
+
* Never less than `minimum` (the `curvature`), and capped so a sliver-thin
|
|
32215
|
+
* slice can't fling a child off to infinity.
|
|
32216
|
+
*
|
|
32217
|
+
* This is what makes panning *useful* in a crowded fan: panning is an
|
|
32218
|
+
* isometry — it can't change the hyperbolic distance between siblings, only
|
|
32219
|
+
* move them to where the disk magnifies more. If siblings are laid out
|
|
32220
|
+
* nearly on top of each other in hyperbolic terms, no amount of dragging
|
|
32221
|
+
* separates them; guaranteeing a minimum spacing means dragging a crowd
|
|
32222
|
+
* towards the center always fans it out.
|
|
32223
|
+
*/
|
|
32224
|
+
function childDistance(share, spacing, minimum) {
|
|
32225
|
+
if (spacing <= 0) return minimum;
|
|
32226
|
+
const halfAngle = Math.min(share, Math.PI) / 2;
|
|
32227
|
+
const needed = Math.asinh(Math.sinh(spacing / 2) / Math.max(Math.sin(halfAngle), 1e-6));
|
|
32228
|
+
return Math.min(Math.max(minimum, needed), minimum + 6);
|
|
32229
|
+
}
|
|
31061
32230
|
/**
|
|
31062
32231
|
* Canonical (true-root-anchored) hyperbolic position of every currently-loaded
|
|
31063
|
-
* node, keyed by
|
|
32232
|
+
* node, keyed by layout id (see {@link hyperbolicChildIds}). Positions never depend on the current focus —
|
|
31064
32233
|
* refocusing is purely a Möbius re-projection of these fixed coordinates, so
|
|
31065
32234
|
* the tree never needs to be re-laid-out when the focus changes.
|
|
31066
32235
|
*
|
|
31067
32236
|
* Each node holds an angular wedge — inherited from its parent, expressed in
|
|
31068
32237
|
* its *own* local frame (root starts with the full `[-π, π]`) — across which
|
|
31069
|
-
* its children are placed at hyperbolic distance
|
|
31070
|
-
*
|
|
32238
|
+
* its children are placed at hyperbolic distance of at least `curvature`
|
|
32239
|
+
* (default `1`) — further for a thin slice, per `siblingSpacing` (see
|
|
32240
|
+
* {@link childDistance}).
|
|
32241
|
+
* That wedge is partitioned among the children, weighted by √ of each child's own
|
|
31071
32242
|
* subtree size rather than split evenly, and each child's own share is
|
|
31072
32243
|
* carried into its local frame via {@link boundaryAngleInLocalFrame} before
|
|
31073
|
-
* recursing. This is a strict partition
|
|
31074
|
-
*
|
|
31075
|
-
*
|
|
32244
|
+
* recursing. This is a strict partition, and each child's wedge region
|
|
32245
|
+
* (bounded by geodesic rays to the same two ideal boundary points as its
|
|
32246
|
+
* slice) nests inside its parent's, which is what guarantees sibling
|
|
32247
|
+
* subtrees never spatially cross regardless of how deep or bushy they are.
|
|
32248
|
+
*
|
|
32249
|
+
* Every node's local frame is tracked as a full {@link DiskIsometry}
|
|
32250
|
+
* (local → canonical), built by composing its parent's frame with the
|
|
32251
|
+
* translation to the child. Composing Möbius translations introduces a
|
|
32252
|
+
* rotation (gyration); dropping it — i.e. assuming a node's frame is simply
|
|
32253
|
+
* `mobiusToOrigin(·, position)` — makes every wedge from depth 3 onward
|
|
32254
|
+
* point in a subtly wrong direction, so subtrees cross each other.
|
|
32255
|
+
*
|
|
32256
|
+
* A wedge wider than π is narrowed to π (around the same bisector) before
|
|
32257
|
+
* the child recurses into it: a near-full-circle wedge would otherwise fan
|
|
32258
|
+
* that child's own children back around towards — and across — its parent.
|
|
31076
32259
|
*
|
|
31077
32260
|
* `curvature` scales how far apart (in hyperbolic distance) each generation
|
|
31078
32261
|
* sits. Raising it pushes each generation further out, so the Poincaré
|
|
@@ -31081,7 +32264,7 @@ const CHAIN_JITTER_RADIANS = .22;
|
|
|
31081
32264
|
* in" on the focus. Lowering it spreads generations out more evenly,
|
|
31082
32265
|
* flattening the view toward a Euclidean-feeling layout.
|
|
31083
32266
|
*/
|
|
31084
|
-
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
32267
|
+
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING) {
|
|
31085
32268
|
const map = /* @__PURE__ */ new Map();
|
|
31086
32269
|
if (!root) return map;
|
|
31087
32270
|
const origin = {
|
|
@@ -31090,70 +32273,122 @@ function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
|
31090
32273
|
};
|
|
31091
32274
|
map.set(root.item.key, {
|
|
31092
32275
|
node: root,
|
|
32276
|
+
id: root.item.key,
|
|
31093
32277
|
depth: 0,
|
|
31094
32278
|
parentKey: void 0,
|
|
32279
|
+
parentId: void 0,
|
|
31095
32280
|
position: origin,
|
|
32281
|
+
toParent: identityIsometry,
|
|
31096
32282
|
angularShare: 2 * Math.PI
|
|
31097
32283
|
});
|
|
31098
|
-
const place = (node,
|
|
32284
|
+
const place = (node, id, frame, wedgeStart, wedgeEnd, depth) => {
|
|
31099
32285
|
if (depth >= maxDepth) return;
|
|
31100
32286
|
const children = node.children ?? [];
|
|
31101
32287
|
const n = children.length;
|
|
31102
32288
|
if (n === 0) return;
|
|
31103
|
-
const r0 = euclideanRadiusForHyperbolicDistance(curvature);
|
|
31104
32289
|
const angularWidth = wedgeEnd - wedgeStart;
|
|
31105
|
-
const weights = children.map((child) => subtreeSize(child, maxDepth - depth - 1));
|
|
32290
|
+
const weights = children.map((child) => Math.sqrt(subtreeSize(child, maxDepth - depth - 1)));
|
|
31106
32291
|
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
32292
|
+
const ids = hyperbolicChildIds(id, children);
|
|
31107
32293
|
let cursor = wedgeStart;
|
|
31108
32294
|
children.forEach((child, index) => {
|
|
31109
32295
|
const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
|
|
31110
|
-
const
|
|
31111
|
-
|
|
31112
|
-
|
|
31113
|
-
const
|
|
32296
|
+
const bisector = cursor + share / 2;
|
|
32297
|
+
cursor += share;
|
|
32298
|
+
const theta = bisector + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
|
|
32299
|
+
const r = euclideanRadiusForHyperbolicDistance(n === 1 ? curvature : childDistance(share, siblingSpacing, curvature));
|
|
31114
32300
|
const local = {
|
|
31115
|
-
x:
|
|
31116
|
-
y:
|
|
32301
|
+
x: r * Math.cos(theta),
|
|
32302
|
+
y: r * Math.sin(theta)
|
|
31117
32303
|
};
|
|
31118
|
-
const
|
|
31119
|
-
|
|
32304
|
+
const toParent = translationFromOrigin(local);
|
|
32305
|
+
const childFrame = composeIsometry(frame, toParent);
|
|
32306
|
+
map.set(ids[index], {
|
|
31120
32307
|
node: child,
|
|
32308
|
+
id: ids[index],
|
|
31121
32309
|
depth: depth + 1,
|
|
31122
32310
|
parentKey: node.item.key,
|
|
31123
|
-
|
|
32311
|
+
parentId: id,
|
|
32312
|
+
position: applyIsometry(childFrame, origin),
|
|
32313
|
+
toParent,
|
|
31124
32314
|
angularShare: share
|
|
31125
32315
|
});
|
|
31126
|
-
const
|
|
31127
|
-
|
|
32316
|
+
const halfWedge = Math.min(share, Math.PI) / 2;
|
|
32317
|
+
const localStart = boundaryAngleInLocalFrame(bisector - halfWedge, local);
|
|
32318
|
+
let localWidth = boundaryAngleInLocalFrame(bisector + halfWedge, local) - localStart;
|
|
31128
32319
|
if (localWidth <= 0) localWidth += 2 * Math.PI;
|
|
31129
|
-
place(child,
|
|
32320
|
+
place(child, ids[index], childFrame, localStart, localStart + localWidth, depth + 1);
|
|
31130
32321
|
});
|
|
31131
32322
|
};
|
|
31132
|
-
place(root,
|
|
32323
|
+
place(root, root.item.key, identityIsometry, -Math.PI, Math.PI, 0);
|
|
31133
32324
|
return map;
|
|
31134
32325
|
}
|
|
31135
|
-
/**
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
|
|
31140
|
-
|
|
31141
|
-
|
|
32326
|
+
/**
|
|
32327
|
+
* Every node within `maxRadius` tree-graph steps of `focusedId`, with its
|
|
32328
|
+
* frame relative to the focus. Built by walking outward from the focus and
|
|
32329
|
+
* composing one short {@link CanonicalEntry.toParent} hop per edge — so
|
|
32330
|
+
* precision depends only on distance *from the focus*, never on how far the
|
|
32331
|
+
* focus is from the root.
|
|
32332
|
+
*/
|
|
32333
|
+
function focusFrames(entries, focusedId, maxRadius) {
|
|
32334
|
+
const frames = /* @__PURE__ */ new Map();
|
|
32335
|
+
if (!entries.has(focusedId)) return frames;
|
|
32336
|
+
frames.set(focusedId, {
|
|
32337
|
+
ring: 0,
|
|
32338
|
+
frame: identityIsometry
|
|
32339
|
+
});
|
|
32340
|
+
let frontier = [focusedId];
|
|
32341
|
+
for (let ring = 1; ring <= maxRadius && frontier.length > 0; ring++) {
|
|
31142
32342
|
const next = [];
|
|
31143
|
-
for (const
|
|
31144
|
-
const entry =
|
|
31145
|
-
|
|
31146
|
-
const
|
|
31147
|
-
|
|
31148
|
-
|
|
31149
|
-
|
|
31150
|
-
|
|
31151
|
-
|
|
31152
|
-
|
|
32343
|
+
for (const id of frontier) {
|
|
32344
|
+
const entry = entries.get(id);
|
|
32345
|
+
const { frame } = frames.get(id);
|
|
32346
|
+
const visit = (neighbourId, neighbourFrame) => {
|
|
32347
|
+
if (frames.has(neighbourId) || !entries.has(neighbourId)) return;
|
|
32348
|
+
frames.set(neighbourId, {
|
|
32349
|
+
ring,
|
|
32350
|
+
frame: neighbourFrame()
|
|
32351
|
+
});
|
|
32352
|
+
next.push(neighbourId);
|
|
32353
|
+
};
|
|
32354
|
+
for (const childId of hyperbolicChildIds(id, entry.node.children ?? [])) visit(childId, () => composeIsometry(frame, entries.get(childId).toParent));
|
|
32355
|
+
if (entry.parentId !== void 0) visit(entry.parentId, () => composeIsometry(frame, invertIsometry(entry.toParent)));
|
|
31153
32356
|
}
|
|
31154
32357
|
frontier = next;
|
|
31155
32358
|
}
|
|
31156
|
-
return
|
|
32359
|
+
return frames;
|
|
32360
|
+
}
|
|
32361
|
+
/** `toParent` hops composed from `id` up to (not including) ancestor `ancestorId`: `id`-local → `ancestorId`-local. */
|
|
32362
|
+
function frameUpTo(entries, id, ancestorId) {
|
|
32363
|
+
let frame = identityIsometry;
|
|
32364
|
+
for (let cursor = id; cursor !== void 0 && cursor !== ancestorId; cursor = entries.get(cursor)?.parentId) frame = composeIsometry(entries.get(cursor).toParent, frame);
|
|
32365
|
+
return frame;
|
|
32366
|
+
}
|
|
32367
|
+
/**
|
|
32368
|
+
* The frame of node `toId` expressed in node `fromId`'s local frame (maps
|
|
32369
|
+
* `toId`-local coordinates to `fromId`-local ones), composed along the tree
|
|
32370
|
+
* path between them via their lowest common ancestor. Used to hand a view
|
|
32371
|
+
* over from one focus to another without passing through root-anchored
|
|
32372
|
+
* coordinates. `undefined` if either id is absent.
|
|
32373
|
+
*/
|
|
32374
|
+
function hyperbolicRelativeFrame(entries, fromId, toId) {
|
|
32375
|
+
if (!entries.has(fromId) || !entries.has(toId)) return void 0;
|
|
32376
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
32377
|
+
for (let cursor = fromId; cursor !== void 0; cursor = entries.get(cursor)?.parentId) ancestors.add(cursor);
|
|
32378
|
+
let lca = toId;
|
|
32379
|
+
while (lca !== void 0 && !ancestors.has(lca)) lca = entries.get(lca)?.parentId;
|
|
32380
|
+
if (lca === void 0) return void 0;
|
|
32381
|
+
return composeIsometry(invertIsometry(frameUpTo(entries, fromId, lca)), frameUpTo(entries, toId, lca));
|
|
32382
|
+
}
|
|
32383
|
+
/**
|
|
32384
|
+
* Resolve `focusedKey` — either a layout id, or (as a convenience, and for
|
|
32385
|
+
* data with globally-unique keys) a plain `item.key`, matching the first
|
|
32386
|
+
* node in depth-first order carrying it.
|
|
32387
|
+
*/
|
|
32388
|
+
function resolveFocusId(canonical, focusedKey) {
|
|
32389
|
+
if (focusedKey === void 0) return void 0;
|
|
32390
|
+
if (canonical.has(focusedKey)) return focusedKey;
|
|
32391
|
+
for (const entry of canonical.values()) if (entry.node.item.key === focusedKey) return entry.id;
|
|
31157
32392
|
}
|
|
31158
32393
|
/**
|
|
31159
32394
|
* Compute the visible nodes/edges of a hyperbolic tree view: every
|
|
@@ -31161,58 +32396,69 @@ function ringDistances(canonical, focusedKey, maxRadius) {
|
|
|
31161
32396
|
* node, projected onto the Poincaré disk with the focus at its center.
|
|
31162
32397
|
*
|
|
31163
32398
|
* @param root True tree root. `undefined` yields an empty layout.
|
|
31164
|
-
* @param options `focusedKey` (
|
|
31165
|
-
* to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
31166
|
-
* cap on
|
|
31167
|
-
* (
|
|
31168
|
-
*
|
|
32399
|
+
* @param options `focusedKey` (a layout id or plain `item.key`; default the
|
|
32400
|
+
* true root; unknown keys fall back to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
32401
|
+
* cap on layout recursion depth, default 24), `curvature`,
|
|
32402
|
+
* `siblingSpacing`, `centerOverride` (put this point of the focus's local
|
|
32403
|
+
* frame at the center instead of the focus itself), `view` (a full
|
|
32404
|
+
* focus-local → screen isometry, taking precedence over `centerOverride` —
|
|
32405
|
+
* used for panning and animation, which also rotate).
|
|
31169
32406
|
*/
|
|
31170
32407
|
function computeHyperbolicLayout(root, options = {}) {
|
|
31171
32408
|
if (!root) return {
|
|
31172
32409
|
nodes: [],
|
|
31173
32410
|
edges: []
|
|
31174
32411
|
};
|
|
31175
|
-
const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1 } = options;
|
|
31176
|
-
const canonical = computeCanonicalPositions(root, maxDepth, curvature);
|
|
31177
|
-
const
|
|
31178
|
-
|
|
31179
|
-
if (!focusEntry) return {
|
|
32412
|
+
const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING } = options;
|
|
32413
|
+
const canonical = computeCanonicalPositions(root, maxDepth, curvature, siblingSpacing);
|
|
32414
|
+
const focusedId = resolveFocusId(canonical, options.focusedKey) ?? root.item.key;
|
|
32415
|
+
if (!canonical.has(focusedId)) return {
|
|
31180
32416
|
nodes: [],
|
|
31181
32417
|
edges: []
|
|
31182
32418
|
};
|
|
31183
|
-
const
|
|
31184
|
-
|
|
31185
|
-
|
|
32419
|
+
const view = options.view ?? viewIsometry(options.centerOverride ?? {
|
|
32420
|
+
x: 0,
|
|
32421
|
+
y: 0
|
|
32422
|
+
});
|
|
32423
|
+
const frames = focusFrames(canonical, focusedId, maxRadius);
|
|
32424
|
+
const rawWeights = [...frames.keys()].map((key) => canonical.get(key).node.item.weight).filter((w) => typeof w === `number`);
|
|
31186
32425
|
const minWeight = rawWeights.length > 0 ? Math.min(...rawWeights) : void 0;
|
|
31187
32426
|
const maxWeight = rawWeights.length > 0 ? Math.max(...rawWeights) : void 0;
|
|
31188
32427
|
const normalizeWeight = (weight) => {
|
|
31189
32428
|
if (weight === void 0 || minWeight === void 0 || maxWeight === void 0 || maxWeight === minWeight) return .5;
|
|
31190
32429
|
return (weight - minWeight) / (maxWeight - minWeight);
|
|
31191
32430
|
};
|
|
31192
|
-
const nodes = [...
|
|
31193
|
-
const entry = canonical.get(
|
|
32431
|
+
const nodes = [...frames.entries()].map(([id, { ring, frame }]) => {
|
|
32432
|
+
const entry = canonical.get(id);
|
|
31194
32433
|
const weight = entry.node.item.weight;
|
|
31195
32434
|
return {
|
|
31196
32435
|
node: entry.node,
|
|
31197
|
-
key,
|
|
32436
|
+
key: entry.node.item.key,
|
|
32437
|
+
id,
|
|
31198
32438
|
depth: entry.depth,
|
|
31199
|
-
ring
|
|
32439
|
+
ring,
|
|
31200
32440
|
parentKey: entry.parentKey,
|
|
31201
|
-
|
|
32441
|
+
parentId: entry.parentId,
|
|
32442
|
+
position: applyIsometry(composeIsometry(view, frame), {
|
|
32443
|
+
x: 0,
|
|
32444
|
+
y: 0
|
|
32445
|
+
}),
|
|
31202
32446
|
angularShare: entry.angularShare,
|
|
31203
32447
|
weight,
|
|
31204
32448
|
weightNormalized: normalizeWeight(weight)
|
|
31205
32449
|
};
|
|
31206
32450
|
}).sort((a, b) => a.ring - b.ring);
|
|
31207
|
-
const
|
|
32451
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
31208
32452
|
const edges = [];
|
|
31209
32453
|
for (const n of nodes) {
|
|
31210
|
-
if (n.
|
|
31211
|
-
const parent =
|
|
32454
|
+
if (n.parentId === void 0) continue;
|
|
32455
|
+
const parent = byId.get(n.parentId);
|
|
31212
32456
|
if (!parent) continue;
|
|
31213
32457
|
edges.push({
|
|
31214
32458
|
fromKey: parent.key,
|
|
31215
32459
|
toKey: n.key,
|
|
32460
|
+
fromId: parent.id,
|
|
32461
|
+
toId: n.id,
|
|
31216
32462
|
from: parent.position,
|
|
31217
32463
|
to: n.position,
|
|
31218
32464
|
weightNormalized: (parent.weightNormalized + n.weightNormalized) / 2
|
|
@@ -31224,17 +32470,476 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
31224
32470
|
};
|
|
31225
32471
|
}
|
|
31226
32472
|
//#endregion
|
|
31227
|
-
//#region src/hyperbolic-tree/
|
|
32473
|
+
//#region src/hyperbolic-tree/keyboard.ts
|
|
32474
|
+
const unhandled = {
|
|
32475
|
+
action: { kind: `none` },
|
|
32476
|
+
handled: false
|
|
32477
|
+
};
|
|
32478
|
+
/** Angular distance (radians, 0–π) of a layout node from screen-"up" (−π/2). */
|
|
32479
|
+
function angleDistanceFromUp(n) {
|
|
32480
|
+
const angle = Math.atan2(n.position.y, n.position.x);
|
|
32481
|
+
let diff = Math.abs(angle - -Math.PI / 2);
|
|
32482
|
+
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
|
32483
|
+
return diff;
|
|
32484
|
+
}
|
|
32485
|
+
/**
|
|
32486
|
+
* Resolve `key` against the focused node (layout id `focusId`):
|
|
32487
|
+
* - `ArrowUp` — the visible child closest to screen-"up"
|
|
32488
|
+
* - `ArrowDown` — the parent
|
|
32489
|
+
* - `ArrowLeft` / `ArrowRight` — previous / next sibling, wrapping
|
|
32490
|
+
* - `Enter` — activate the focus
|
|
32491
|
+
* - `Escape` — back to the root (unhandled when already there)
|
|
32492
|
+
*/
|
|
32493
|
+
function resolveKeyboardAction(key, root, focusId, visibleNodes) {
|
|
32494
|
+
const path = findHyperbolicPathById(root, focusId);
|
|
32495
|
+
const focusNode = path?.at(-1);
|
|
32496
|
+
if (!path || !focusNode) return unhandled;
|
|
32497
|
+
const parent = path.length >= 2 ? path[path.length - 2] : void 0;
|
|
32498
|
+
const none = {
|
|
32499
|
+
action: { kind: `none` },
|
|
32500
|
+
handled: true
|
|
32501
|
+
};
|
|
32502
|
+
const focus = (node) => node ? {
|
|
32503
|
+
action: {
|
|
32504
|
+
kind: `focus`,
|
|
32505
|
+
node
|
|
32506
|
+
},
|
|
32507
|
+
handled: true
|
|
32508
|
+
} : none;
|
|
32509
|
+
switch (key) {
|
|
32510
|
+
case `ArrowUp`: {
|
|
32511
|
+
const children = visibleNodes.filter((n) => n.parentId === focusId);
|
|
32512
|
+
if (children.length === 0) return none;
|
|
32513
|
+
return focus(children.reduce((a, b) => angleDistanceFromUp(a) < angleDistanceFromUp(b) ? a : b).node);
|
|
32514
|
+
}
|
|
32515
|
+
case `ArrowDown`: return focus(parent);
|
|
32516
|
+
case `ArrowLeft`:
|
|
32517
|
+
case `ArrowRight`: {
|
|
32518
|
+
const siblings = parent?.children ?? [];
|
|
32519
|
+
const index = siblings.indexOf(focusNode);
|
|
32520
|
+
if (index === -1 || siblings.length <= 1) return none;
|
|
32521
|
+
return focus(siblings[(index + (key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length]);
|
|
32522
|
+
}
|
|
32523
|
+
case `Enter`: return {
|
|
32524
|
+
action: {
|
|
32525
|
+
kind: `activate`,
|
|
32526
|
+
node: focusNode,
|
|
32527
|
+
depth: path.length - 1
|
|
32528
|
+
},
|
|
32529
|
+
handled: true
|
|
32530
|
+
};
|
|
32531
|
+
case `Escape`: return focusNode === root ? unhandled : focus(root);
|
|
32532
|
+
default: return unhandled;
|
|
32533
|
+
}
|
|
32534
|
+
}
|
|
32535
|
+
//#endregion
|
|
32536
|
+
//#region src/hyperbolic-tree/lazy-children.ts
|
|
32537
|
+
function createLazyChildLoader(source, onChange) {
|
|
32538
|
+
const pending = /* @__PURE__ */ new Set();
|
|
32539
|
+
const aborters = /* @__PURE__ */ new Map();
|
|
32540
|
+
const fetchChildren = async (node, depth, signal) => {
|
|
32541
|
+
const { model, loadChildren } = source();
|
|
32542
|
+
if (model) return model.getChildren(node, depth, signal);
|
|
32543
|
+
return [...await collectDataResult(loadChildren({
|
|
32544
|
+
node,
|
|
32545
|
+
depth
|
|
32546
|
+
}, signal), signal)];
|
|
32547
|
+
};
|
|
32548
|
+
const load = async (node, depth) => {
|
|
32549
|
+
pending.add(node);
|
|
32550
|
+
const controller = new AbortController();
|
|
32551
|
+
aborters.set(node, controller);
|
|
32552
|
+
onChange();
|
|
32553
|
+
try {
|
|
32554
|
+
const children = await fetchChildren(node, depth, controller.signal);
|
|
32555
|
+
if (!controller.signal.aborted) node.children = children;
|
|
32556
|
+
} catch {
|
|
32557
|
+
if (!controller.signal.aborted) node.children = [];
|
|
32558
|
+
} finally {
|
|
32559
|
+
pending.delete(node);
|
|
32560
|
+
aborters.delete(node);
|
|
32561
|
+
if (!controller.signal.aborted) onChange();
|
|
32562
|
+
}
|
|
32563
|
+
};
|
|
32564
|
+
return {
|
|
32565
|
+
/** Start loading children for every node in `visible` that has none yet. */
|
|
32566
|
+
ensure(visible) {
|
|
32567
|
+
const { model, loadChildren } = source();
|
|
32568
|
+
if (!model && !loadChildren) return;
|
|
32569
|
+
for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
32570
|
+
},
|
|
32571
|
+
isPending: (node) => pending.has(node),
|
|
32572
|
+
abortAll() {
|
|
32573
|
+
for (const aborter of aborters.values()) aborter.abort();
|
|
32574
|
+
aborters.clear();
|
|
32575
|
+
pending.clear();
|
|
32576
|
+
}
|
|
32577
|
+
};
|
|
32578
|
+
}
|
|
32579
|
+
//#endregion
|
|
32580
|
+
//#region src/hyperbolic-tree/render-utils.ts
|
|
32581
|
+
/**
|
|
32582
|
+
* The Poincaré disk's conformal factor relative to the origin, `1 − |p|²`:
|
|
32583
|
+
* how much a shape of fixed hyperbolic size shrinks (in Euclidean terms) when
|
|
32584
|
+
* moved from the center to `p`. Sizing nodes by this makes them shrink
|
|
32585
|
+
* exactly as the space does, rather than by an ad-hoc falloff.
|
|
32586
|
+
*/
|
|
32587
|
+
function conformalScale(p) {
|
|
32588
|
+
return Math.max(0, 1 - (p.x * p.x + p.y * p.y));
|
|
32589
|
+
}
|
|
32590
|
+
/**
|
|
32591
|
+
* Euclidean distance from each node to its nearest visible neighbour, keyed
|
|
32592
|
+
* by layout id (`Infinity` for a lone node). Used to cap on-screen node size
|
|
32593
|
+
* so crowded nodes never overlap — measured in the *projected* view, so it
|
|
32594
|
+
* stays right whichever node is focused. O(n²), fine for the few hundred
|
|
32595
|
+
* nodes a `maxRadius` window typically holds.
|
|
32596
|
+
*/
|
|
32597
|
+
function nearestNeighbourDistances(nodes) {
|
|
32598
|
+
const out = /* @__PURE__ */ new Map();
|
|
32599
|
+
for (const n of nodes) out.set(n.id, Number.POSITIVE_INFINITY);
|
|
32600
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
32601
|
+
const a = nodes[i].position;
|
|
32602
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
32603
|
+
const b = nodes[j].position;
|
|
32604
|
+
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
32605
|
+
if (d < out.get(nodes[i].id)) out.set(nodes[i].id, d);
|
|
32606
|
+
if (d < out.get(nodes[j].id)) out.set(nodes[j].id, d);
|
|
32607
|
+
}
|
|
32608
|
+
}
|
|
32609
|
+
return out;
|
|
32610
|
+
}
|
|
32611
|
+
/** Weight multiplier for `n`, interpolating between the min/max weight scales. */
|
|
32612
|
+
function weightScale(n, options) {
|
|
32613
|
+
return options.minWeightScale + (options.maxWeightScale - options.minWeightScale) * n.weightNormalized;
|
|
32614
|
+
}
|
|
32615
|
+
/**
|
|
32616
|
+
* A dot's rendered radius: base size × weight × conformal shrink, capped to
|
|
32617
|
+
* under half the distance to its nearest neighbour so crowded siblings keep
|
|
32618
|
+
* a visible gap instead of merging into a blob.
|
|
32619
|
+
*/
|
|
32620
|
+
function dotRadius(n, nearest, options) {
|
|
32621
|
+
const radius = options.nodeRadius * conformalScale(n.position) * weightScale(n, options);
|
|
32622
|
+
return Math.max(Math.min(radius, nearest * .45), .003);
|
|
32623
|
+
}
|
|
32624
|
+
/**
|
|
32625
|
+
* Footprint of a `2·halfWidth × 2·halfHeight` rectangle centered on
|
|
32626
|
+
* `center` and rotated by `angle` radians.
|
|
32627
|
+
*/
|
|
32628
|
+
function orientedRect(id, center, halfWidth, halfHeight, angle = 0) {
|
|
32629
|
+
const cos = Math.cos(angle);
|
|
32630
|
+
const sin = Math.sin(angle);
|
|
32631
|
+
const corner = (u, v) => ({
|
|
32632
|
+
x: center.x + u * cos - v * sin,
|
|
32633
|
+
y: center.y + u * sin + v * cos
|
|
32634
|
+
});
|
|
32635
|
+
return {
|
|
32636
|
+
id,
|
|
32637
|
+
corners: [
|
|
32638
|
+
corner(-halfWidth, -halfHeight),
|
|
32639
|
+
corner(halfWidth, -halfHeight),
|
|
32640
|
+
corner(halfWidth, halfHeight),
|
|
32641
|
+
corner(-halfWidth, halfHeight)
|
|
32642
|
+
]
|
|
32643
|
+
};
|
|
32644
|
+
}
|
|
32645
|
+
function polygonArea(points) {
|
|
32646
|
+
let area = 0;
|
|
32647
|
+
for (let i = 0; i < points.length; i++) {
|
|
32648
|
+
const a = points[i];
|
|
32649
|
+
const b = points[(i + 1) % points.length];
|
|
32650
|
+
area += a.x * b.y - b.x * a.y;
|
|
32651
|
+
}
|
|
32652
|
+
return Math.abs(area) / 2;
|
|
32653
|
+
}
|
|
32654
|
+
/** Sutherland–Hodgman: `subject` clipped to convex `clip` (either winding). */
|
|
32655
|
+
function clipPolygon(subject, clip) {
|
|
32656
|
+
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;
|
|
32657
|
+
let output = [...subject];
|
|
32658
|
+
for (let i = 0; i < clip.length && output.length > 0; i++) {
|
|
32659
|
+
const a = clip[i];
|
|
32660
|
+
const b = clip[(i + 1) % clip.length];
|
|
32661
|
+
const side = (p) => orientation * ((b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x));
|
|
32662
|
+
const input = output;
|
|
32663
|
+
output = [];
|
|
32664
|
+
for (let j = 0; j < input.length; j++) {
|
|
32665
|
+
const current = input[j];
|
|
32666
|
+
const previous = input[(j + input.length - 1) % input.length];
|
|
32667
|
+
const sc = side(current);
|
|
32668
|
+
const sp = side(previous);
|
|
32669
|
+
if (sc >= 0 !== sp >= 0) {
|
|
32670
|
+
const t = sp / (sp - sc);
|
|
32671
|
+
output.push({
|
|
32672
|
+
x: previous.x + (current.x - previous.x) * t,
|
|
32673
|
+
y: previous.y + (current.y - previous.y) * t
|
|
32674
|
+
});
|
|
32675
|
+
}
|
|
32676
|
+
if (sc >= 0) output.push(current);
|
|
32677
|
+
}
|
|
32678
|
+
}
|
|
32679
|
+
return output;
|
|
32680
|
+
}
|
|
32681
|
+
function bounds(points) {
|
|
32682
|
+
const xs = points.map((p) => p.x);
|
|
32683
|
+
const ys = points.map((p) => p.y);
|
|
32684
|
+
return {
|
|
32685
|
+
x0: Math.min(...xs),
|
|
32686
|
+
y0: Math.min(...ys),
|
|
32687
|
+
x1: Math.max(...xs),
|
|
32688
|
+
y1: Math.max(...ys)
|
|
32689
|
+
};
|
|
32690
|
+
}
|
|
32691
|
+
/** Fraction (0–1) of `candidate`'s area covered by `other`. */
|
|
32692
|
+
function overlapFraction(candidate, other) {
|
|
32693
|
+
const a = bounds(candidate.corners);
|
|
32694
|
+
const b = bounds(other.corners);
|
|
32695
|
+
if (a.x1 <= b.x0 || b.x1 <= a.x0 || a.y1 <= b.y0 || b.y1 <= a.y0) return 0;
|
|
32696
|
+
const area = polygonArea(candidate.corners);
|
|
32697
|
+
if (area <= 0) return 1;
|
|
32698
|
+
return polygonArea(clipPolygon(candidate.corners, other.corners)) / area;
|
|
32699
|
+
}
|
|
32700
|
+
/**
|
|
32701
|
+
* Greedy label placement: walk `shapes` in priority order and keep each one
|
|
32702
|
+
* whose overlap with every already-kept shape is at most `maxOverlap` of its
|
|
32703
|
+
* own area. Returns the ids of the kept labels. Checking actual (possibly
|
|
32704
|
+
* rotated) label footprints, not just node positions, is what stops a fan of
|
|
32705
|
+
* same-radius siblings from stacking their text into an unreadable pile.
|
|
32706
|
+
*/
|
|
32707
|
+
function cullOverlappingLabels(shapes, maxOverlap = .03) {
|
|
32708
|
+
const kept = [];
|
|
32709
|
+
for (const shape of shapes) if (kept.every((k) => overlapFraction(shape, k) <= maxOverlap)) kept.push(shape);
|
|
32710
|
+
return new Set(kept.map((r) => r.id));
|
|
32711
|
+
}
|
|
32712
|
+
/** `label` cut to at most `maxChars` characters, ending in `…` when cut. `0` or less disables truncation. */
|
|
32713
|
+
function truncateLabel(label, maxChars) {
|
|
32714
|
+
if (maxChars <= 0 || label.length <= maxChars) return label;
|
|
32715
|
+
return `${label.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
|
|
32716
|
+
}
|
|
32717
|
+
let measureContext;
|
|
32718
|
+
const widthCache = /* @__PURE__ */ new Map();
|
|
32719
|
+
/**
|
|
32720
|
+
* Rendered width of `text` at `fontSize` px in `fontFamily`, via an offscreen
|
|
32721
|
+
* canvas (cached). Falls back to a per-character estimate where canvas text
|
|
32722
|
+
* measurement is unavailable (e.g. non-browser test environments).
|
|
32723
|
+
*/
|
|
32724
|
+
function measureTextWidth(text, fontSize, fontFamily) {
|
|
32725
|
+
const cacheKey = `${fontFamily}\u0000${text}`;
|
|
32726
|
+
let atUnitSize = widthCache.get(cacheKey);
|
|
32727
|
+
if (atUnitSize === void 0) {
|
|
32728
|
+
if (measureContext === void 0) try {
|
|
32729
|
+
measureContext = typeof document === `undefined` ? null : document.createElement(`canvas`).getContext(`2d`);
|
|
32730
|
+
} catch {
|
|
32731
|
+
measureContext = null;
|
|
32732
|
+
}
|
|
32733
|
+
const reference = 100;
|
|
32734
|
+
let measured;
|
|
32735
|
+
if (measureContext) {
|
|
32736
|
+
measureContext.font = `${reference}px ${fontFamily || `sans-serif`}`;
|
|
32737
|
+
const width = measureContext.measureText(text).width;
|
|
32738
|
+
if (Number.isFinite(width) && width > 0) measured = width / reference;
|
|
32739
|
+
}
|
|
32740
|
+
atUnitSize = measured ?? text.length * .6;
|
|
32741
|
+
widthCache.set(cacheKey, atUnitSize);
|
|
32742
|
+
}
|
|
32743
|
+
return atUnitSize * fontSize;
|
|
32744
|
+
}
|
|
32745
|
+
/**
|
|
32746
|
+
* Smallest scale a box shrinks to, however far from the focus. Pure conformal
|
|
32747
|
+
* scaling (`1 − |z|²`) is geometrically exact but shrinks the second ring to
|
|
32748
|
+
* well under half size, taking labels below legibility long before the
|
|
32749
|
+
* boundary — flooring it keeps nearby rings readable while still reading as
|
|
32750
|
+
* "further away".
|
|
32751
|
+
*/
|
|
32752
|
+
const BOX_SCALE_FLOOR = .45;
|
|
32753
|
+
/**
|
|
32754
|
+
* Geometry for a `box`-shaped node. The box is laid out at its focus size and
|
|
32755
|
+
* then uniformly scaled — by the conformal factor, floored at
|
|
32756
|
+
* {@link BOX_SCALE_FLOOR} — so the label is measured just once. Weight
|
|
32757
|
+
* nudges the scale gently (square-rooted), so heavy nodes read as larger
|
|
32758
|
+
* without dwarfing their neighbours' labels.
|
|
32759
|
+
*/
|
|
32760
|
+
function boxGeometry(n, options) {
|
|
32761
|
+
const conformal = BOX_SCALE_FLOOR + .55 * conformalScale(n.position);
|
|
32762
|
+
const scale = Math.min(1, conformal * (options.zoom ?? 1)) * Math.sqrt(weightScale(n, options));
|
|
32763
|
+
const text = truncateLabel(n.node.item.label, options.maxChars);
|
|
32764
|
+
const padX = options.fontSize * .5;
|
|
32765
|
+
const height = options.fontSize * 1.55;
|
|
32766
|
+
return {
|
|
32767
|
+
scale,
|
|
32768
|
+
width: measureTextWidth(text, options.fontSize, options.fontFamily) + padX * 2,
|
|
32769
|
+
height,
|
|
32770
|
+
markerSize: options.fontSize * .55,
|
|
32771
|
+
text
|
|
32772
|
+
};
|
|
32773
|
+
}
|
|
32774
|
+
/** Label footprint of a box of `geometry` centered at `center` and rotated by `angle` (render units). */
|
|
32775
|
+
function boxLabelShape(id, center, geometry, angle = 0) {
|
|
32776
|
+
return orientedRect(id, center, geometry.width * geometry.scale / 2, geometry.height * geometry.scale / 2, angle);
|
|
32777
|
+
}
|
|
32778
|
+
//#endregion
|
|
32779
|
+
//#region src/hyperbolic-tree/render-context.ts
|
|
32780
|
+
const HORIZONTAL = {
|
|
32781
|
+
direction: 0,
|
|
32782
|
+
textAngle: 0,
|
|
32783
|
+
flipped: false
|
|
32784
|
+
};
|
|
32785
|
+
/**
|
|
32786
|
+
* Label font metrics. `--hyperbolictree-font-size` is in real screen px;
|
|
32787
|
+
* the SVG's viewBox is fixed, so it's converted to render units using the
|
|
32788
|
+
* on-screen disk size (`extentPx`) and zoom — keeping text legible whatever
|
|
32789
|
+
* size the element is laid out at, and constant-size while zooming
|
|
32790
|
+
* (zooming in spreads crowded labels apart rather than just magnifying the
|
|
32791
|
+
* pile).
|
|
32792
|
+
*/
|
|
32793
|
+
function fontMetrics(host, extentPx, zoom) {
|
|
32794
|
+
const style = typeof getComputedStyle === `function` ? getComputedStyle(host) : void 0;
|
|
32795
|
+
const parsed = Number.parseFloat(style?.getPropertyValue(`--hyperbolictree-font-size`) ?? ``);
|
|
32796
|
+
const px = Number.isFinite(parsed) && parsed > 0 ? parsed : 13;
|
|
32797
|
+
return {
|
|
32798
|
+
px,
|
|
32799
|
+
units: px * (1050 / (extentPx > 0 ? extentPx : 420) / (zoom || 1)),
|
|
32800
|
+
family: style?.fontFamily ?? ``
|
|
32801
|
+
};
|
|
32802
|
+
}
|
|
32803
|
+
/** Map of top-level-branch layout id → index among the root's children. */
|
|
32804
|
+
function branchIndices(root) {
|
|
32805
|
+
if (!root) return /* @__PURE__ */ new Map();
|
|
32806
|
+
return new Map(hyperbolicChildIds(root.item.key, root.children ?? []).map((id, i) => [id, i]));
|
|
32807
|
+
}
|
|
32808
|
+
/** Inline `--branch-index`/`--branch-count` declarations for the node/edge with layout id `id` (empty for the root). */
|
|
32809
|
+
function branchStyle(id, branches) {
|
|
32810
|
+
const branch = hyperbolicBranchId(id);
|
|
32811
|
+
const index = branch === void 0 ? void 0 : branches.get(branch);
|
|
32812
|
+
return index === void 0 ? `` : ` --branch-index: ${index}; --branch-count: ${branches.size};`;
|
|
32813
|
+
}
|
|
32814
|
+
function groupSiblings(nodes) {
|
|
32815
|
+
const groups = /* @__PURE__ */ new Map();
|
|
32816
|
+
for (const n of nodes) {
|
|
32817
|
+
const group = groups.get(n.parentId) ?? [];
|
|
32818
|
+
group.push(n);
|
|
32819
|
+
groups.set(n.parentId, group);
|
|
32820
|
+
}
|
|
32821
|
+
return groups;
|
|
32822
|
+
}
|
|
32823
|
+
/**
|
|
32824
|
+
* Label priority: the focus, then selected nodes, then larger (nearer the
|
|
32825
|
+
* center) first.
|
|
32826
|
+
*/
|
|
32827
|
+
function byLabelPriority(nodes, sizeOf, isSelected) {
|
|
32828
|
+
const rank = (n) => n.ring === 0 ? 0 : isSelected(n.node) ? 1 : 2;
|
|
32829
|
+
return [...nodes].sort((a, b) => rank(a) - rank(b) || sizeOf(b) - sizeOf(a) || a.ring - b.ring);
|
|
32830
|
+
}
|
|
32831
|
+
/**
|
|
32832
|
+
* Each node's label orientation. The reference neighbour is the one a step
|
|
32833
|
+
* nearer the focus: usually the parent, but for the focus's own ancestors
|
|
32834
|
+
* (which sit *behind* it) the child on the path back to the focus. The focus
|
|
32835
|
+
* itself stays horizontal.
|
|
32836
|
+
*/
|
|
32837
|
+
function labelOrientations(nodes, radial) {
|
|
32838
|
+
const out = /* @__PURE__ */ new Map();
|
|
32839
|
+
if (!radial) {
|
|
32840
|
+
for (const n of nodes) out.set(n.id, HORIZONTAL);
|
|
32841
|
+
return out;
|
|
32842
|
+
}
|
|
32843
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
32844
|
+
const nearer = /* @__PURE__ */ new Map();
|
|
32845
|
+
for (const n of nodes) {
|
|
32846
|
+
const parent = n.parentId === void 0 ? void 0 : byId.get(n.parentId);
|
|
32847
|
+
if (!parent) continue;
|
|
32848
|
+
if (parent.ring < n.ring) nearer.set(n.id, parent);
|
|
32849
|
+
else if (n.ring < parent.ring) nearer.set(parent.id, n);
|
|
32850
|
+
}
|
|
32851
|
+
for (const n of nodes) {
|
|
32852
|
+
const from = nearer.get(n.id);
|
|
32853
|
+
if (!from || n.ring === 0) {
|
|
32854
|
+
out.set(n.id, HORIZONTAL);
|
|
32855
|
+
continue;
|
|
32856
|
+
}
|
|
32857
|
+
const direction = Math.atan2(n.position.y - from.position.y, n.position.x - from.position.x);
|
|
32858
|
+
const flipped = Math.cos(direction) < 0;
|
|
32859
|
+
out.set(n.id, {
|
|
32860
|
+
direction,
|
|
32861
|
+
textAngle: flipped ? direction + Math.PI : direction,
|
|
32862
|
+
flipped
|
|
32863
|
+
});
|
|
32864
|
+
}
|
|
32865
|
+
return out;
|
|
32866
|
+
}
|
|
32867
|
+
const renderCenter = (n) => ({
|
|
32868
|
+
x: n.position.x * 500,
|
|
32869
|
+
y: n.position.y * 500
|
|
32870
|
+
});
|
|
32871
|
+
function boxLabels(nodes, options, orientations) {
|
|
32872
|
+
const { font } = options;
|
|
32873
|
+
const boxes = new Map(nodes.map((n) => [n.id, boxGeometry(n, {
|
|
32874
|
+
fontSize: font.units,
|
|
32875
|
+
fontFamily: font.family,
|
|
32876
|
+
maxChars: options.boxMaxChars,
|
|
32877
|
+
minWeightScale: options.minWeightScale,
|
|
32878
|
+
maxWeightScale: options.maxWeightScale,
|
|
32879
|
+
zoom: options.zoom
|
|
32880
|
+
})]));
|
|
32881
|
+
return {
|
|
32882
|
+
boxes,
|
|
32883
|
+
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)))
|
|
32884
|
+
};
|
|
32885
|
+
}
|
|
32886
|
+
function dotLabels(nodes, options, orientations) {
|
|
32887
|
+
const { font } = options;
|
|
32888
|
+
const nearest = nearestNeighbourDistances(nodes);
|
|
32889
|
+
const radii = new Map(nodes.map((n) => [n.id, dotRadius(n, nearest.get(n.id), options)]));
|
|
32890
|
+
return {
|
|
32891
|
+
radii,
|
|
32892
|
+
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) => {
|
|
32893
|
+
const { direction } = orientations.get(n.id);
|
|
32894
|
+
const halfWidth = measureTextWidth(n.node.item.label, font.units, font.family) / 2;
|
|
32895
|
+
const along = (radii.get(n.id) + .01) * 500 + halfWidth;
|
|
32896
|
+
const c = renderCenter(n);
|
|
32897
|
+
const center = {
|
|
32898
|
+
x: c.x + Math.cos(direction) * along,
|
|
32899
|
+
y: c.y + Math.sin(direction) * along
|
|
32900
|
+
};
|
|
32901
|
+
return orientedRect(n.id, center, halfWidth + font.units * .6, font.units * .6, direction);
|
|
32902
|
+
}))
|
|
32903
|
+
};
|
|
32904
|
+
}
|
|
32905
|
+
/**
|
|
32906
|
+
* Everything the node templates need for one render. Labels are placed
|
|
32907
|
+
* greedily by priority and dropped where they'd overlap; hovered/cursor
|
|
32908
|
+
* nodes are exempt from culling entirely — always labelled — so hovering a
|
|
32909
|
+
* marker reveals it without reshuffling everyone else's labels.
|
|
32910
|
+
*/
|
|
32911
|
+
function buildNodeRenderContext(nodes, options) {
|
|
32912
|
+
const orientations = labelOrientations(nodes, options.radialLabels);
|
|
32913
|
+
const shapeData = options.nodeShape === `box` ? {
|
|
32914
|
+
...boxLabels(nodes, options, orientations),
|
|
32915
|
+
radii: /* @__PURE__ */ new Map()
|
|
32916
|
+
} : {
|
|
32917
|
+
...dotLabels(nodes, options, orientations),
|
|
32918
|
+
boxes: /* @__PURE__ */ new Map()
|
|
32919
|
+
};
|
|
32920
|
+
const labelIds = new Set(shapeData.labelIds);
|
|
32921
|
+
for (const n of nodes) if (options.isTickled(n.id)) labelIds.add(n.id);
|
|
32922
|
+
return {
|
|
32923
|
+
siblingGroups: groupSiblings(nodes),
|
|
32924
|
+
font: options.font,
|
|
32925
|
+
branches: options.branches,
|
|
32926
|
+
boxes: shapeData.boxes,
|
|
32927
|
+
radii: shapeData.radii,
|
|
32928
|
+
labelIds,
|
|
32929
|
+
orientations
|
|
32930
|
+
};
|
|
32931
|
+
}
|
|
31228
32932
|
/**
|
|
31229
|
-
*
|
|
31230
|
-
*
|
|
31231
|
-
* few hundred device pixels) is an extreme, unusual upscale factor that has
|
|
31232
|
-
* been observed to trigger a Chromium GPU-rasterization glitch — a `fill:
|
|
31233
|
-
* none` shape painting as solid-filled, matching its computed style exactly
|
|
31234
|
-
* but not what's on screen. Rendering in a "normal"-magnitude SVG coordinate
|
|
31235
|
-
* system (viewBox radius {@link RENDER_SCALE} instead of `1`) avoids it.
|
|
32933
|
+
* Paint order: far-from-focus first so nearer (larger) nodes sit on top;
|
|
32934
|
+
* then labelled over unlabelled; the hovered node above everything.
|
|
31236
32935
|
*/
|
|
31237
|
-
|
|
32936
|
+
function paintOrder(nodes, ctx, isTickled) {
|
|
32937
|
+
const layer = (n) => isTickled(n.id) ? 2 : ctx.labelIds.has(n.id) ? 1 : 0;
|
|
32938
|
+
const distance = (n) => n.position.x ** 2 + n.position.y ** 2;
|
|
32939
|
+
return [...nodes].sort((a, b) => layer(a) - layer(b) || distance(b) - distance(a));
|
|
32940
|
+
}
|
|
32941
|
+
//#endregion
|
|
32942
|
+
//#region src/hyperbolic-tree/render-templates.ts
|
|
31238
32943
|
/**
|
|
31239
32944
|
* Sanitizes a node key into a token safe to use inside a `part` attribute /
|
|
31240
32945
|
* `::part()` selector — arbitrary key characters (spaces, quotes, etc.)
|
|
@@ -31243,13 +32948,121 @@ const RENDER_SCALE = 500;
|
|
|
31243
32948
|
function partToken(key) {
|
|
31244
32949
|
return key.replace(/[^\w-]/g, `-`);
|
|
31245
32950
|
}
|
|
31246
|
-
|
|
31247
|
-
|
|
31248
|
-
|
|
31249
|
-
|
|
31250
|
-
|
|
31251
|
-
|
|
32951
|
+
const preventMouseDownFocus = (e) => e.preventDefault();
|
|
32952
|
+
/** The shared `<g part="node">` wrapper — ARIA, parts, weight/branch vars, tooltip, and pointer handling. */
|
|
32953
|
+
function renderNodeGroup(n, args, transform, content) {
|
|
32954
|
+
const { node, key, id } = n;
|
|
32955
|
+
const { ctx, state, handlers } = args;
|
|
32956
|
+
const selected = state.isSelected(node);
|
|
32957
|
+
const classes = [
|
|
32958
|
+
`node`,
|
|
32959
|
+
selected ? `selected` : ``,
|
|
32960
|
+
state.isTickled(id) ? `tickled` : ``,
|
|
32961
|
+
state.isFiltered(n) ? `filtered` : ``,
|
|
32962
|
+
state.isPending(node) ? `loading` : ``,
|
|
32963
|
+
n.ring === 0 ? `focused` : ``
|
|
32964
|
+
].filter(Boolean).join(` `);
|
|
32965
|
+
const siblings = ctx.siblingGroups.get(n.parentId) ?? [n];
|
|
32966
|
+
const weightStyle = `--node-weight: ${n.weightNormalized};${n.weight === void 0 ? `` : ` --node-weight-raw: ${n.weight};`}`;
|
|
32967
|
+
return svg`
|
|
32968
|
+
<g
|
|
32969
|
+
class=${classes}
|
|
32970
|
+
data-key=${key}
|
|
32971
|
+
role="treeitem"
|
|
32972
|
+
tabindex="-1"
|
|
32973
|
+
aria-level=${n.depth + 1}
|
|
32974
|
+
aria-selected=${selected}
|
|
32975
|
+
aria-setsize=${siblings.length}
|
|
32976
|
+
aria-posinset=${siblings.findIndex((sib) => sib.id === id) + 1}
|
|
32977
|
+
aria-label=${node.item.label}
|
|
32978
|
+
part="node node-${partToken(key)}"
|
|
32979
|
+
style=${weightStyle + branchStyle(id, ctx.branches)}
|
|
32980
|
+
transform=${transform}
|
|
32981
|
+
@pointerdown=${handlers.onPointerDown}
|
|
32982
|
+
@mousedown=${preventMouseDownFocus}
|
|
32983
|
+
@click=${(e) => handlers.onClick(n, e)}
|
|
32984
|
+
@dblclick=${(e) => handlers.onDblClick(n, e)}
|
|
32985
|
+
@pointerenter=${() => handlers.onPointerEnter(id)}
|
|
32986
|
+
>${state.showTitle(n) ? svg`<title>${node.item.label}</title>` : nothing}${content}</g>
|
|
32987
|
+
`;
|
|
31252
32988
|
}
|
|
32989
|
+
const degrees = (radians) => radians * 180 / Math.PI;
|
|
32990
|
+
/** A dot's label: just past the dot, running along its orientation — flipped labels anchor at their end so they still read outward. */
|
|
32991
|
+
function renderDotLabel(n, ctx, scaledRadius) {
|
|
32992
|
+
const { textAngle, flipped } = ctx.orientations.get(n.id);
|
|
32993
|
+
const gap = scaledRadius + 5;
|
|
32994
|
+
return svg`<text
|
|
32995
|
+
class="hyperbolictree-label"
|
|
32996
|
+
part="label"
|
|
32997
|
+
x=${flipped ? -gap : gap}
|
|
32998
|
+
y="0"
|
|
32999
|
+
transform=${textAngle === 0 ? nothing : `rotate(${degrees(textAngle)})`}
|
|
33000
|
+
text-anchor=${flipped ? `end` : `start`}
|
|
33001
|
+
font-size=${ctx.font.units}
|
|
33002
|
+
dominant-baseline="middle"
|
|
33003
|
+
>${n.node.item.label}</text>`;
|
|
33004
|
+
}
|
|
33005
|
+
function renderDotNode(n, args) {
|
|
33006
|
+
const { ctx } = args;
|
|
33007
|
+
const scaledRadius = ctx.radii.get(n.id) * 500;
|
|
33008
|
+
return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})`, svg`
|
|
33009
|
+
<circle class="node-hit" r=${Math.max(scaledRadius, 17.5)} part="hit" />
|
|
33010
|
+
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
33011
|
+
${ctx.labelIds.has(n.id) ? renderDotLabel(n, ctx, scaledRadius) : nothing}
|
|
33012
|
+
`);
|
|
33013
|
+
}
|
|
33014
|
+
function renderBoxNode(n, args) {
|
|
33015
|
+
const { ctx } = args;
|
|
33016
|
+
const box = ctx.boxes.get(n.id);
|
|
33017
|
+
const showText = ctx.labelIds.has(n.id);
|
|
33018
|
+
const width = showText ? box.width : box.markerSize;
|
|
33019
|
+
const height = showText ? box.height : box.markerSize;
|
|
33020
|
+
const { textAngle } = ctx.orientations.get(n.id);
|
|
33021
|
+
const rotate = showText && textAngle !== 0 ? ` rotate(${degrees(textAngle)})` : ``;
|
|
33022
|
+
return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})${rotate} scale(${box.scale})`, svg`
|
|
33023
|
+
<rect
|
|
33024
|
+
class="node-box ${showText ? `` : `marker`}"
|
|
33025
|
+
part="box"
|
|
33026
|
+
x=${-width / 2}
|
|
33027
|
+
y=${-height / 2}
|
|
33028
|
+
width=${width}
|
|
33029
|
+
height=${height}
|
|
33030
|
+
rx=${ctx.font.units * .2}
|
|
33031
|
+
/>
|
|
33032
|
+
${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}
|
|
33033
|
+
`);
|
|
33034
|
+
}
|
|
33035
|
+
/** Every visible node, in paint order, in the shape `ctx` was built for. */
|
|
33036
|
+
function renderNodes(nodes, shape, args) {
|
|
33037
|
+
const ordered = paintOrder(nodes, args.ctx, args.state.isTickled);
|
|
33038
|
+
const renderOne = shape === `box` ? renderBoxNode : renderDotNode;
|
|
33039
|
+
return repeat(ordered, (n) => n.id, (n) => renderOne(n, args));
|
|
33040
|
+
}
|
|
33041
|
+
function renderDefaultEdge(edge, branches) {
|
|
33042
|
+
return svg`<path
|
|
33043
|
+
class="edge"
|
|
33044
|
+
d=${geodesicPath(edge.from, edge.to, 500)}
|
|
33045
|
+
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
33046
|
+
data-from=${edge.fromKey}
|
|
33047
|
+
data-to=${edge.toKey}
|
|
33048
|
+
style="--edge-weight: ${edge.weightNormalized};${branchStyle(edge.toId, branches)}"
|
|
33049
|
+
/>`;
|
|
33050
|
+
}
|
|
33051
|
+
/** Every visible edge — via `edgeRenderer` when given, else the default geodesic arc. */
|
|
33052
|
+
function renderEdges(edges, byId, branches, edgeRenderer) {
|
|
33053
|
+
return repeat(edges, (e) => `${e.fromId}->${e.toId}`, (edge) => {
|
|
33054
|
+
const fromNode = byId.get(edge.fromId)?.node;
|
|
33055
|
+
const toNode = byId.get(edge.toId)?.node;
|
|
33056
|
+
if (edgeRenderer && fromNode && toNode) return svg`<g transform="scale(${500})">${edgeRenderer({
|
|
33057
|
+
edge,
|
|
33058
|
+
fromNode,
|
|
33059
|
+
toNode
|
|
33060
|
+
})}</g>`;
|
|
33061
|
+
return renderDefaultEdge(edge, branches);
|
|
33062
|
+
});
|
|
33063
|
+
}
|
|
33064
|
+
//#endregion
|
|
33065
|
+
//#region src/hyperbolic-tree/hyperbolictree-list.ts
|
|
31253
33066
|
let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitElement {
|
|
31254
33067
|
constructor(..._args) {
|
|
31255
33068
|
super(..._args);
|
|
@@ -31257,37 +33070,63 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31257
33070
|
this.selectionFilter = `all`;
|
|
31258
33071
|
this.interactionMode = `standard`;
|
|
31259
33072
|
this.maxRadius = 4;
|
|
33073
|
+
this.nodeShape = `dot`;
|
|
33074
|
+
this.boxMaxChars = 24;
|
|
33075
|
+
this.minLabelSize = 9;
|
|
33076
|
+
this.colorByBranch = false;
|
|
31260
33077
|
this.nodeRadius = .05;
|
|
31261
33078
|
this.minNodeRadius = .015;
|
|
31262
33079
|
this.minWeightScale = .6;
|
|
31263
33080
|
this.maxWeightScale = 1.6;
|
|
31264
33081
|
this.zoom = 1;
|
|
33082
|
+
this.disableZoomGestures = false;
|
|
33083
|
+
this.disableRadialLabels = false;
|
|
33084
|
+
this.disableChildPreview = false;
|
|
31265
33085
|
this.curvature = 1;
|
|
33086
|
+
this.siblingSpacing = DEFAULT_SIBLING_SPACING;
|
|
31266
33087
|
this.focusAnimationDuration = 400;
|
|
31267
33088
|
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
31268
33089
|
this.disableKeyboardNavigation = false;
|
|
31269
33090
|
this.registry = void 0;
|
|
31270
|
-
this.
|
|
31271
|
-
this.
|
|
31272
|
-
this.
|
|
33091
|
+
this._focusId = void 0;
|
|
33092
|
+
this._viewOverride = void 0;
|
|
33093
|
+
this._focusRotation = 0;
|
|
33094
|
+
this._zoomOffset = {
|
|
31273
33095
|
x: 0,
|
|
31274
33096
|
y: 0
|
|
31275
33097
|
};
|
|
33098
|
+
this._previewId = void 0;
|
|
33099
|
+
this.#preview = createChildPreviewTiming({
|
|
33100
|
+
candidate: (id) => {
|
|
33101
|
+
const nodes = this.#layout().nodes;
|
|
33102
|
+
const n = id === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === id);
|
|
33103
|
+
return n && needsChildPreview(n, nodes, this.#lastLabelIds) ? n.id : void 0;
|
|
33104
|
+
},
|
|
33105
|
+
setPreviewId: (id) => {
|
|
33106
|
+
this._previewId = id;
|
|
33107
|
+
}
|
|
33108
|
+
});
|
|
33109
|
+
this.#lazy = createLazyChildLoader(() => ({
|
|
33110
|
+
model: this.model,
|
|
33111
|
+
loadChildren: this.loadChildren
|
|
33112
|
+
}), () => this.requestUpdate());
|
|
33113
|
+
this.#lastLabelIds = /* @__PURE__ */ new Set();
|
|
31276
33114
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
31277
33115
|
labelSelector: `.hyperbolictree-label`,
|
|
31278
33116
|
highlightKey: `incr-search-highlight`
|
|
31279
33117
|
}) });
|
|
31280
33118
|
this.tickled = new TickledController(this, {
|
|
31281
33119
|
getItems: () => this.#layout().nodes.map((n) => ({
|
|
31282
|
-
id: n.
|
|
33120
|
+
id: n.id,
|
|
31283
33121
|
enabled: true
|
|
31284
33122
|
})),
|
|
31285
33123
|
onStateChange: () => {
|
|
31286
33124
|
this.setAttribute(`data-input-mode`, this.tickled.inputMode);
|
|
31287
33125
|
},
|
|
31288
33126
|
onTickle: (id) => {
|
|
33127
|
+
this.#preview.update(id);
|
|
31289
33128
|
if (id === void 0) return;
|
|
31290
|
-
const n = this.#layout().nodes.find((n) => n.
|
|
33129
|
+
const n = this.#layout().nodes.find((n) => n.id === id);
|
|
31291
33130
|
if (!n) return;
|
|
31292
33131
|
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
31293
33132
|
detail: {
|
|
@@ -31299,8 +33138,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31299
33138
|
}));
|
|
31300
33139
|
}
|
|
31301
33140
|
});
|
|
31302
|
-
this
|
|
31303
|
-
|
|
33141
|
+
this._hostSize = {
|
|
33142
|
+
width: 0,
|
|
33143
|
+
height: 0
|
|
33144
|
+
};
|
|
33145
|
+
this.#gestures = createGestureHandlers({
|
|
33146
|
+
svgRect: () => this.renderRoot.querySelector(`svg.hyperbolictree`)?.getBoundingClientRect(),
|
|
33147
|
+
takeView: () => {
|
|
33148
|
+
const view = this.#currentView();
|
|
33149
|
+
this.#cancelAnimation();
|
|
33150
|
+
this._viewOverride = view;
|
|
33151
|
+
return view;
|
|
33152
|
+
},
|
|
33153
|
+
setView: (view) => {
|
|
33154
|
+
this._viewOverride = view;
|
|
33155
|
+
},
|
|
33156
|
+
zoomState: () => this.#zoomState(),
|
|
33157
|
+
setZoomState: (state) => {
|
|
33158
|
+
this.zoom = state.zoom;
|
|
33159
|
+
this._zoomOffset = state.offset;
|
|
33160
|
+
},
|
|
33161
|
+
zoomGesturesEnabled: () => !this.disableZoomGestures,
|
|
33162
|
+
hasFocus: () => this.renderRoot.activeElement !== null,
|
|
33163
|
+
focusTree: () => this.#focusTree()
|
|
33164
|
+
});
|
|
31304
33165
|
}
|
|
31305
33166
|
static {
|
|
31306
33167
|
this.shadowRootOptions = {
|
|
@@ -31313,37 +33174,55 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31313
33174
|
}
|
|
31314
33175
|
/** The node currently centered on the disk. Pure view-state — `root` is never mutated by focus changes. */
|
|
31315
33176
|
get focusedNode() {
|
|
31316
|
-
if (this.
|
|
31317
|
-
return
|
|
33177
|
+
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
33178
|
+
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
31318
33179
|
}
|
|
33180
|
+
#preview;
|
|
33181
|
+
#lazy;
|
|
33182
|
+
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
33183
|
+
#lastLabelIds;
|
|
31319
33184
|
#modelUnsubscribe;
|
|
31320
|
-
#pendingKeys;
|
|
31321
|
-
#aborters;
|
|
31322
33185
|
#ownedRegistry;
|
|
31323
33186
|
#unregisterSearchCommands;
|
|
31324
33187
|
#animFrame;
|
|
31325
|
-
#
|
|
33188
|
+
#resizeObserver;
|
|
33189
|
+
#gestures;
|
|
31326
33190
|
get #effectiveRoot() {
|
|
31327
33191
|
return this.model?.root ?? this.root;
|
|
31328
33192
|
}
|
|
31329
33193
|
#layout() {
|
|
31330
33194
|
return computeHyperbolicLayout(this.#effectiveRoot, {
|
|
31331
|
-
focusedKey: this.
|
|
33195
|
+
focusedKey: this._focusId,
|
|
31332
33196
|
maxRadius: this.maxRadius,
|
|
31333
33197
|
curvature: this.curvature,
|
|
31334
|
-
|
|
33198
|
+
siblingSpacing: this.siblingSpacing,
|
|
33199
|
+
view: this.#currentView()
|
|
31335
33200
|
});
|
|
31336
33201
|
}
|
|
31337
|
-
#
|
|
31338
|
-
|
|
31339
|
-
|
|
31340
|
-
|
|
31341
|
-
|
|
31342
|
-
|
|
31343
|
-
|
|
31344
|
-
|
|
31345
|
-
|
|
31346
|
-
|
|
33202
|
+
get #rootId() {
|
|
33203
|
+
return this.#effectiveRoot?.item.key;
|
|
33204
|
+
}
|
|
33205
|
+
/** The projection currently on screen, from the focus's local frame. */
|
|
33206
|
+
#currentView() {
|
|
33207
|
+
return this._viewOverride ?? rotationIsometry(this._focusRotation);
|
|
33208
|
+
}
|
|
33209
|
+
/**
|
|
33210
|
+
* The current view re-expressed in `targetId`'s local frame — i.e. the same
|
|
33211
|
+
* picture, but with `targetId` as the reference node. Where the view hands
|
|
33212
|
+
* over on a focus change.
|
|
33213
|
+
*/
|
|
33214
|
+
#viewFromFrameOf(targetId, currentId) {
|
|
33215
|
+
const entries = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature, this.siblingSpacing);
|
|
33216
|
+
const relative = currentId === void 0 ? void 0 : hyperbolicRelativeFrame(entries, currentId, targetId);
|
|
33217
|
+
return relative ? composeIsometry(this.#currentView(), relative) : rotationIsometry(this._focusRotation);
|
|
33218
|
+
}
|
|
33219
|
+
/** Layout id of `node` — by identity, falling back to the first node sharing its key. */
|
|
33220
|
+
#idOf(node) {
|
|
33221
|
+
const root = this.#effectiveRoot;
|
|
33222
|
+
const direct = findHyperbolicIdOf(root, node);
|
|
33223
|
+
if (direct !== void 0) return direct;
|
|
33224
|
+
const byKey = findHyperbolicNode(root, node.item.key);
|
|
33225
|
+
return byKey ? findHyperbolicIdOf(root, byKey) : void 0;
|
|
31347
33226
|
}
|
|
31348
33227
|
connectedCallback() {
|
|
31349
33228
|
super.connectedCallback();
|
|
@@ -31363,14 +33242,25 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31363
33242
|
});
|
|
31364
33243
|
this.registry = registry;
|
|
31365
33244
|
this.#syncSearchCommands();
|
|
33245
|
+
if (typeof ResizeObserver === `function`) {
|
|
33246
|
+
this.#resizeObserver = new ResizeObserver(([entry]) => {
|
|
33247
|
+
const { width, height } = entry.contentRect;
|
|
33248
|
+
this._hostSize = {
|
|
33249
|
+
width,
|
|
33250
|
+
height
|
|
33251
|
+
};
|
|
33252
|
+
});
|
|
33253
|
+
this.#resizeObserver.observe(this);
|
|
33254
|
+
}
|
|
31366
33255
|
}
|
|
31367
33256
|
disconnectedCallback() {
|
|
31368
33257
|
super.disconnectedCallback();
|
|
33258
|
+
this.#resizeObserver?.disconnect();
|
|
33259
|
+
this.#resizeObserver = void 0;
|
|
31369
33260
|
this.#modelUnsubscribe?.();
|
|
31370
33261
|
this.#modelUnsubscribe = void 0;
|
|
31371
|
-
|
|
31372
|
-
this.#
|
|
31373
|
-
this.#pendingKeys.clear();
|
|
33262
|
+
this.#preview.dispose();
|
|
33263
|
+
this.#lazy.abortAll();
|
|
31374
33264
|
if (this.#animFrame !== void 0) {
|
|
31375
33265
|
cancelAnimationFrame(this.#animFrame);
|
|
31376
33266
|
this.#animFrame = void 0;
|
|
@@ -31411,58 +33301,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31411
33301
|
}
|
|
31412
33302
|
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
31413
33303
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
31414
|
-
if (this.
|
|
31415
|
-
this.
|
|
31416
|
-
this.
|
|
33304
|
+
if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
|
|
33305
|
+
this._focusId = void 0;
|
|
33306
|
+
this._viewOverride = void 0;
|
|
33307
|
+
this._focusRotation = 0;
|
|
31417
33308
|
}
|
|
31418
|
-
this.#
|
|
33309
|
+
this.#lazy.ensure(this.#layout().nodes);
|
|
31419
33310
|
}
|
|
31420
33311
|
#resetViewState() {
|
|
31421
33312
|
this.#clearSelectionInternal();
|
|
31422
33313
|
this.tickled.clearTickled();
|
|
31423
|
-
this.
|
|
31424
|
-
this.
|
|
31425
|
-
this.
|
|
33314
|
+
this._focusId = void 0;
|
|
33315
|
+
this._viewOverride = void 0;
|
|
33316
|
+
this._focusRotation = 0;
|
|
33317
|
+
this._zoomOffset = {
|
|
31426
33318
|
x: 0,
|
|
31427
33319
|
y: 0
|
|
31428
33320
|
};
|
|
33321
|
+
this._previewId = void 0;
|
|
33322
|
+
this.#gestures.cancel();
|
|
31429
33323
|
if (this.#animFrame !== void 0) {
|
|
31430
33324
|
cancelAnimationFrame(this.#animFrame);
|
|
31431
33325
|
this.#animFrame = void 0;
|
|
31432
33326
|
}
|
|
31433
33327
|
}
|
|
31434
|
-
#ensureVisibleChildren() {
|
|
31435
|
-
if (!this.model && !this.loadChildren) return;
|
|
31436
|
-
for (const n of this.#layout().nodes) {
|
|
31437
|
-
const { node } = n;
|
|
31438
|
-
if (node.children !== void 0 || this.#pendingKeys.has(node.item.key)) continue;
|
|
31439
|
-
this.#loadChildren(node, n.depth);
|
|
31440
|
-
}
|
|
31441
|
-
}
|
|
31442
|
-
async #loadChildren(node, depth) {
|
|
31443
|
-
const key = node.item.key;
|
|
31444
|
-
this.#pendingKeys.add(key);
|
|
31445
|
-
const controller = new AbortController();
|
|
31446
|
-
this.#aborters.set(key, controller);
|
|
31447
|
-
this.requestUpdate();
|
|
31448
|
-
try {
|
|
31449
|
-
let children;
|
|
31450
|
-
if (this.model) children = await this.model.getChildren(node, depth, controller.signal);
|
|
31451
|
-
else children = [...await collectDataResult(this.loadChildren({
|
|
31452
|
-
node,
|
|
31453
|
-
depth
|
|
31454
|
-
}, controller.signal), controller.signal)];
|
|
31455
|
-
if (controller.signal.aborted) return;
|
|
31456
|
-
node.children = children;
|
|
31457
|
-
} catch {
|
|
31458
|
-
if (controller.signal.aborted) return;
|
|
31459
|
-
node.children = [];
|
|
31460
|
-
} finally {
|
|
31461
|
-
this.#pendingKeys.delete(key);
|
|
31462
|
-
this.#aborters.delete(key);
|
|
31463
|
-
if (!controller.signal.aborted) this.requestUpdate();
|
|
31464
|
-
}
|
|
31465
|
-
}
|
|
31466
33328
|
#canSelect(node) {
|
|
31467
33329
|
if (this.selectionFilter === `none`) return false;
|
|
31468
33330
|
if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
|
|
@@ -31538,83 +33400,83 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31538
33400
|
* the previous one, for interop with generic `TreeComponent` listeners.
|
|
31539
33401
|
*/
|
|
31540
33402
|
focusOn(node) {
|
|
31541
|
-
const
|
|
31542
|
-
const
|
|
31543
|
-
|
|
31544
|
-
|
|
31545
|
-
const
|
|
31546
|
-
const
|
|
31547
|
-
|
|
31548
|
-
|
|
31549
|
-
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
y: 0
|
|
31553
|
-
};
|
|
31554
|
-
this._focusKey = targetKey;
|
|
31555
|
-
this._panOffset = {
|
|
31556
|
-
x: 0,
|
|
31557
|
-
y: 0
|
|
31558
|
-
};
|
|
31559
|
-
this.#animateTo(fromPosition, toPosition);
|
|
33403
|
+
const root = this.#effectiveRoot;
|
|
33404
|
+
const targetId = this.#idOf(node);
|
|
33405
|
+
const currentId = this._focusId ?? this.#rootId;
|
|
33406
|
+
if (targetId === void 0 || currentId === targetId) return;
|
|
33407
|
+
const targetNode = findHyperbolicPathById(root, targetId)?.at(-1) ?? node;
|
|
33408
|
+
const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
|
|
33409
|
+
const fromView = this.#viewFromFrameOf(targetId, currentId);
|
|
33410
|
+
this._focusId = targetId;
|
|
33411
|
+
this.#gestures.cancel();
|
|
33412
|
+
this._previewId = void 0;
|
|
33413
|
+
this.#animateTo(fromView);
|
|
31560
33414
|
this.dispatchEvent(new CustomEvent(`focus`, {
|
|
31561
33415
|
detail: {
|
|
31562
|
-
node,
|
|
33416
|
+
node: targetNode,
|
|
31563
33417
|
previous: previousNode
|
|
31564
33418
|
},
|
|
31565
33419
|
bubbles: true,
|
|
31566
33420
|
composed: true
|
|
31567
33421
|
}));
|
|
31568
|
-
if (previousNode) {
|
|
31569
|
-
if (
|
|
31570
|
-
|
|
31571
|
-
|
|
31572
|
-
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
|
|
31580
|
-
|
|
31581
|
-
|
|
31582
|
-
|
|
31583
|
-
|
|
31584
|
-
|
|
31585
|
-
},
|
|
31586
|
-
bubbles: true,
|
|
31587
|
-
composed: true
|
|
31588
|
-
}));
|
|
31589
|
-
}
|
|
33422
|
+
if (previousNode && currentId !== void 0) {
|
|
33423
|
+
if (isHyperbolicDescendantId(targetId, currentId)) this.dispatchEvent(new CustomEvent(`expand`, {
|
|
33424
|
+
detail: {
|
|
33425
|
+
node: targetNode,
|
|
33426
|
+
depth: hyperbolicIdDepth(targetId)
|
|
33427
|
+
},
|
|
33428
|
+
bubbles: true,
|
|
33429
|
+
composed: true
|
|
33430
|
+
}));
|
|
33431
|
+
else if (isHyperbolicDescendantId(currentId, targetId)) this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
33432
|
+
detail: {
|
|
33433
|
+
node: previousNode,
|
|
33434
|
+
depth: hyperbolicIdDepth(currentId)
|
|
33435
|
+
},
|
|
33436
|
+
bubbles: true,
|
|
33437
|
+
composed: true
|
|
33438
|
+
}));
|
|
31590
33439
|
}
|
|
31591
33440
|
this.requestUpdate();
|
|
31592
33441
|
}
|
|
31593
|
-
#
|
|
31594
|
-
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
31595
|
-
}
|
|
31596
|
-
#animateTo(from, to) {
|
|
33442
|
+
#cancelAnimation() {
|
|
31597
33443
|
if (this.#animFrame !== void 0) {
|
|
31598
33444
|
cancelAnimationFrame(this.#animFrame);
|
|
31599
33445
|
this.#animFrame = void 0;
|
|
31600
33446
|
}
|
|
31601
|
-
|
|
31602
|
-
|
|
33447
|
+
}
|
|
33448
|
+
/** Animate from view `from` (in the new focus's frame) to the settled, centered view — see {@link focusTransition}. */
|
|
33449
|
+
#animateTo(from) {
|
|
33450
|
+
this.#cancelAnimation();
|
|
33451
|
+
const { viewAt, settledRotation } = focusTransition(from);
|
|
33452
|
+
this._focusRotation = settledRotation;
|
|
33453
|
+
const fromOffset = this._zoomOffset;
|
|
33454
|
+
if (this.focusAnimationDuration <= 0 || prefersReducedMotion$1() || typeof requestAnimationFrame !== `function`) {
|
|
33455
|
+
this._viewOverride = void 0;
|
|
33456
|
+
this._zoomOffset = {
|
|
33457
|
+
x: 0,
|
|
33458
|
+
y: 0
|
|
33459
|
+
};
|
|
31603
33460
|
return;
|
|
31604
33461
|
}
|
|
31605
33462
|
const start = performance.now();
|
|
31606
33463
|
const duration = this.focusAnimationDuration;
|
|
31607
33464
|
const step = (now) => {
|
|
31608
33465
|
const t = Math.min(1, (now - start) / duration);
|
|
31609
|
-
|
|
31610
|
-
this.
|
|
33466
|
+
const remaining = 1 - easeInOutCubic(t);
|
|
33467
|
+
this._zoomOffset = {
|
|
33468
|
+
x: fromOffset.x * remaining,
|
|
33469
|
+
y: fromOffset.y * remaining
|
|
33470
|
+
};
|
|
33471
|
+
this._viewOverride = t >= 1 ? void 0 : viewAt(t);
|
|
31611
33472
|
this.#animFrame = t < 1 ? requestAnimationFrame(step) : void 0;
|
|
31612
33473
|
};
|
|
31613
33474
|
this.#animFrame = requestAnimationFrame(step);
|
|
31614
33475
|
}
|
|
31615
33476
|
#focusAndMaybeSelect(node) {
|
|
31616
33477
|
this.focusOn(node);
|
|
31617
|
-
this
|
|
33478
|
+
const id = this.#idOf(node);
|
|
33479
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
31618
33480
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
31619
33481
|
}
|
|
31620
33482
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -31629,7 +33491,8 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31629
33491
|
}
|
|
31630
33492
|
this.focusOn(current);
|
|
31631
33493
|
this.select(current);
|
|
31632
|
-
this
|
|
33494
|
+
const id = this.#idOf(current);
|
|
33495
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
31633
33496
|
this.requestUpdate();
|
|
31634
33497
|
return true;
|
|
31635
33498
|
}
|
|
@@ -31637,7 +33500,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31637
33500
|
navigateToNode(predicate) {
|
|
31638
33501
|
const root = this.#effectiveRoot;
|
|
31639
33502
|
if (!root?.children) return false;
|
|
31640
|
-
const keyPath =
|
|
33503
|
+
const keyPath = findHyperbolicKeyPath(root.children, predicate);
|
|
31641
33504
|
if (!keyPath) return false;
|
|
31642
33505
|
return this.navigateTo(keyPath);
|
|
31643
33506
|
}
|
|
@@ -31648,22 +33511,13 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31648
33511
|
*/
|
|
31649
33512
|
revealKey(key) {
|
|
31650
33513
|
const node = findHyperbolicNode(this.#effectiveRoot, key);
|
|
31651
|
-
|
|
31652
|
-
if (!
|
|
31653
|
-
this.
|
|
33514
|
+
const id = node ? this.#idOf(node) : void 0;
|
|
33515
|
+
if (!node || id === void 0) return false;
|
|
33516
|
+
if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
|
|
33517
|
+
this.tickled.setTickled(id);
|
|
31654
33518
|
this.requestUpdate();
|
|
31655
33519
|
return true;
|
|
31656
33520
|
}
|
|
31657
|
-
#findKeyPath(nodes, predicate, prefix = []) {
|
|
31658
|
-
for (const node of nodes) {
|
|
31659
|
-
const path = [...prefix, node.item.key];
|
|
31660
|
-
if (predicate(node)) return path;
|
|
31661
|
-
if (node.children?.length) {
|
|
31662
|
-
const hit = this.#findKeyPath(node.children, predicate, path);
|
|
31663
|
-
if (hit) return hit;
|
|
31664
|
-
}
|
|
31665
|
-
}
|
|
31666
|
-
}
|
|
31667
33521
|
/**
|
|
31668
33522
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
31669
33523
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -31700,7 +33554,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31700
33554
|
this.requestUpdate();
|
|
31701
33555
|
}
|
|
31702
33556
|
} else if (this.interactionMode !== `manual`) this.select(n.node);
|
|
31703
|
-
this.tickled.setTickled(n.
|
|
33557
|
+
this.tickled.setTickled(n.id);
|
|
31704
33558
|
this.focusOn(n.node);
|
|
31705
33559
|
}
|
|
31706
33560
|
#handleItemDblClick(n) {
|
|
@@ -31722,239 +33576,147 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31722
33576
|
}));
|
|
31723
33577
|
}
|
|
31724
33578
|
#handleKeyDown(event) {
|
|
31725
|
-
if (this.disableKeyboardNavigation) return;
|
|
31726
33579
|
const root = this.#effectiveRoot;
|
|
31727
|
-
|
|
31728
|
-
if (
|
|
31729
|
-
|
|
31730
|
-
|
|
31731
|
-
|
|
31732
|
-
|
|
31733
|
-
|
|
31734
|
-
|
|
31735
|
-
|
|
31736
|
-
|
|
31737
|
-
|
|
31738
|
-
|
|
31739
|
-
|
|
31740
|
-
|
|
31741
|
-
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
|
|
31746
|
-
}
|
|
31747
|
-
case `ArrowLeft`:
|
|
31748
|
-
case `ArrowRight`: {
|
|
31749
|
-
event.preventDefault();
|
|
31750
|
-
const path = findHyperbolicPath(root, focusNode.item.key);
|
|
31751
|
-
const siblings = (path && path.length >= 2 ? path[path.length - 2] : void 0)?.children ?? (focusNode === root ? [focusNode] : []);
|
|
31752
|
-
const index = siblings.findIndex((n) => n.item.key === focusNode.item.key);
|
|
31753
|
-
if (index === -1 || siblings.length <= 1) return;
|
|
31754
|
-
const next = siblings[(index + (event.key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length];
|
|
31755
|
-
this.#focusAndMaybeSelect(next);
|
|
31756
|
-
return;
|
|
31757
|
-
}
|
|
31758
|
-
case `Enter`: {
|
|
31759
|
-
event.preventDefault();
|
|
31760
|
-
if (this.interactionMode !== `manual`) this.select(focusNode);
|
|
31761
|
-
const depth = (findHyperbolicPath(root, focusNode.item.key)?.length ?? 1) - 1;
|
|
31762
|
-
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
31763
|
-
detail: {
|
|
31764
|
-
node: focusNode,
|
|
31765
|
-
depth
|
|
31766
|
-
},
|
|
31767
|
-
bubbles: true,
|
|
31768
|
-
composed: true
|
|
31769
|
-
}));
|
|
31770
|
-
return;
|
|
31771
|
-
}
|
|
31772
|
-
case `Escape`: if (this._focusKey !== void 0 && this._focusKey !== root.item.key) {
|
|
31773
|
-
event.preventDefault();
|
|
31774
|
-
this.focusOn(root);
|
|
31775
|
-
}
|
|
33580
|
+
if (this.disableKeyboardNavigation || !root) return;
|
|
33581
|
+
if (event.key === `Escape` && this._previewId !== void 0) {
|
|
33582
|
+
event.preventDefault();
|
|
33583
|
+
this._previewId = void 0;
|
|
33584
|
+
return;
|
|
33585
|
+
}
|
|
33586
|
+
const { action, handled } = resolveKeyboardAction(event.key, root, this._focusId ?? root.item.key, this.#layout().nodes);
|
|
33587
|
+
if (handled) event.preventDefault();
|
|
33588
|
+
if (action.kind === `focus`) this.#focusAndMaybeSelect(action.node);
|
|
33589
|
+
else if (action.kind === `activate`) {
|
|
33590
|
+
if (this.interactionMode !== `manual`) this.select(action.node);
|
|
33591
|
+
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
33592
|
+
detail: {
|
|
33593
|
+
node: action.node,
|
|
33594
|
+
depth: action.depth
|
|
33595
|
+
},
|
|
33596
|
+
bubbles: true,
|
|
33597
|
+
composed: true
|
|
33598
|
+
}));
|
|
31776
33599
|
}
|
|
31777
33600
|
}
|
|
31778
|
-
#
|
|
31779
|
-
|
|
31780
|
-
|
|
31781
|
-
|
|
31782
|
-
|
|
31783
|
-
if (!this.#isBackgroundTarget(event)) return;
|
|
31784
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
31785
|
-
this.#panStart = {
|
|
31786
|
-
pointerId: event.pointerId,
|
|
31787
|
-
startClient: {
|
|
31788
|
-
x: event.clientX,
|
|
31789
|
-
y: event.clientY
|
|
31790
|
-
},
|
|
31791
|
-
baseOffset: this._panOffset
|
|
31792
|
-
};
|
|
31793
|
-
}
|
|
31794
|
-
#handleBackgroundPointerMove(event) {
|
|
31795
|
-
if (!this.#panStart || event.pointerId !== this.#panStart.pointerId) return;
|
|
31796
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
31797
|
-
const scale = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
31798
|
-
const dx = (event.clientX - this.#panStart.startClient.x) * scale;
|
|
31799
|
-
const dy = (event.clientY - this.#panStart.startClient.y) * scale;
|
|
31800
|
-
this._panOffset = clampToDisk({
|
|
31801
|
-
x: this.#panStart.baseOffset.x - dx,
|
|
31802
|
-
y: this.#panStart.baseOffset.y - dy
|
|
31803
|
-
}, .9);
|
|
31804
|
-
this.requestUpdate();
|
|
33601
|
+
#zoomState() {
|
|
33602
|
+
return clampZoomState({
|
|
33603
|
+
zoom: this.zoom,
|
|
33604
|
+
offset: this._zoomOffset
|
|
33605
|
+
});
|
|
31805
33606
|
}
|
|
31806
|
-
|
|
31807
|
-
|
|
31808
|
-
|
|
31809
|
-
|
|
31810
|
-
|
|
31811
|
-
|
|
31812
|
-
|
|
31813
|
-
|
|
31814
|
-
d += `${i === 0 ? `M` : `L`}${(p.x * RENDER_SCALE).toFixed(2)},${(p.y * RENDER_SCALE).toFixed(2)} `;
|
|
31815
|
-
}
|
|
31816
|
-
return svg`<path
|
|
31817
|
-
class="edge"
|
|
31818
|
-
d=${d}
|
|
31819
|
-
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
31820
|
-
data-from=${edge.fromKey}
|
|
31821
|
-
data-to=${edge.toKey}
|
|
31822
|
-
style="--edge-weight: ${edge.weightNormalized}"
|
|
31823
|
-
/>`;
|
|
33607
|
+
/**
|
|
33608
|
+
* Any pointer interaction (a node press, or a pan) moves keyboard focus to
|
|
33609
|
+
* the tree, so the user can carry straight on with the arrow keys.
|
|
33610
|
+
* Focuses the stable `<svg role="tree">` — not the node, which paint-order
|
|
33611
|
+
* re-sorting may move in the DOM (dropping its focus) — without scrolling.
|
|
33612
|
+
*/
|
|
33613
|
+
#focusTree() {
|
|
33614
|
+
this.renderRoot.querySelector(`svg.hyperbolictree`)?.focus({ preventScroll: true });
|
|
31824
33615
|
}
|
|
31825
|
-
#
|
|
31826
|
-
|
|
31827
|
-
|
|
31828
|
-
|
|
31829
|
-
|
|
31830
|
-
|
|
31831
|
-
|
|
31832
|
-
|
|
31833
|
-
|
|
31834
|
-
|
|
31835
|
-
|
|
31836
|
-
}
|
|
31837
|
-
|
|
31838
|
-
|
|
31839
|
-
|
|
31840
|
-
|
|
31841
|
-
|
|
31842
|
-
|
|
31843
|
-
|
|
31844
|
-
|
|
31845
|
-
|
|
31846
|
-
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
31850
|
-
|
|
31851
|
-
|
|
31852
|
-
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
31856
|
-
|
|
31857
|
-
|
|
31858
|
-
|
|
31859
|
-
|
|
31860
|
-
|
|
31861
|
-
|
|
31862
|
-
|
|
31863
|
-
* first, then larger on-screen radius) and a label is kept only if its
|
|
31864
|
-
* position isn't too close to an already-kept one.
|
|
31865
|
-
*/
|
|
31866
|
-
#computeLabelKeys(nodes) {
|
|
31867
|
-
const minSpacing = .07;
|
|
31868
|
-
const prioritised = [...nodes.filter((n) => this.#nodeRenderRadius(n) >= this.minNodeRadius)].sort((a, b) => {
|
|
31869
|
-
if (a.ring !== b.ring) return a.ring - b.ring;
|
|
31870
|
-
return Math.hypot(a.position.x, a.position.y) - Math.hypot(b.position.x, b.position.y);
|
|
33616
|
+
#handleNodePointerDown(event) {
|
|
33617
|
+
event.stopPropagation();
|
|
33618
|
+
this.#focusTree();
|
|
33619
|
+
}
|
|
33620
|
+
#pickPreviewChild(child) {
|
|
33621
|
+
this.#preview.hide();
|
|
33622
|
+
if (this.interactionMode !== `manual`) this.select(child);
|
|
33623
|
+
const id = this.#idOf(child);
|
|
33624
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
33625
|
+
this.focusOn(child);
|
|
33626
|
+
this.#focusTree();
|
|
33627
|
+
}
|
|
33628
|
+
#previewFor(nodes) {
|
|
33629
|
+
const n = this._previewId === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === this._previewId);
|
|
33630
|
+
if (!n) return void 0;
|
|
33631
|
+
const { width, height } = this._hostSize;
|
|
33632
|
+
const anchor = diskToHostPx(n.position, this.#zoomState(), width || 420, height || 420);
|
|
33633
|
+
return {
|
|
33634
|
+
node: n.node,
|
|
33635
|
+
anchor,
|
|
33636
|
+
above: anchor.y > (height || 420) / 2
|
|
33637
|
+
};
|
|
33638
|
+
}
|
|
33639
|
+
#nodeContext(nodes, branches) {
|
|
33640
|
+
return buildNodeRenderContext(nodes, {
|
|
33641
|
+
nodeShape: this.nodeShape,
|
|
33642
|
+
radialLabels: !this.disableRadialLabels,
|
|
33643
|
+
zoom: this.#zoomState().zoom,
|
|
33644
|
+
font: fontMetrics(this, Math.min(this._hostSize.width, this._hostSize.height), this.zoom),
|
|
33645
|
+
branches,
|
|
33646
|
+
boxMaxChars: this.boxMaxChars,
|
|
33647
|
+
minLabelSize: this.minLabelSize,
|
|
33648
|
+
nodeRadius: this.nodeRadius,
|
|
33649
|
+
minNodeRadius: this.minNodeRadius,
|
|
33650
|
+
minWeightScale: this.minWeightScale,
|
|
33651
|
+
maxWeightScale: this.maxWeightScale,
|
|
33652
|
+
isSelected: (node) => this._selectedNodes.has(node),
|
|
33653
|
+
isTickled: (id) => this.tickled.isTickled(id)
|
|
31871
33654
|
});
|
|
31872
|
-
const kept = [];
|
|
31873
|
-
const keys = /* @__PURE__ */ new Set();
|
|
31874
|
-
for (const n of prioritised) if (!kept.some((k) => Math.hypot(k.position.x - n.position.x, k.position.y - n.position.y) < minSpacing)) {
|
|
31875
|
-
kept.push(n);
|
|
31876
|
-
keys.add(n.key);
|
|
31877
|
-
}
|
|
31878
|
-
return keys;
|
|
31879
33655
|
}
|
|
31880
|
-
#
|
|
31881
|
-
|
|
31882
|
-
|
|
31883
|
-
|
|
31884
|
-
|
|
31885
|
-
|
|
31886
|
-
|
|
31887
|
-
|
|
31888
|
-
|
|
31889
|
-
|
|
31890
|
-
|
|
31891
|
-
|
|
31892
|
-
|
|
31893
|
-
|
|
31894
|
-
|
|
31895
|
-
|
|
31896
|
-
|
|
31897
|
-
|
|
31898
|
-
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
|
|
31902
|
-
|
|
31903
|
-
aria-label=${node.item.label}
|
|
31904
|
-
part="node node-${partToken(key)}"
|
|
31905
|
-
style=${weightStyle}
|
|
31906
|
-
transform="translate(${position.x * RENDER_SCALE} ${position.y * RENDER_SCALE})"
|
|
31907
|
-
@pointerdown=${(e) => e.stopPropagation()}
|
|
31908
|
-
@click=${(e) => {
|
|
31909
|
-
e.stopPropagation();
|
|
31910
|
-
this.#handleItemClick(n, e);
|
|
31911
|
-
}}
|
|
31912
|
-
@dblclick=${(e) => {
|
|
31913
|
-
e.stopPropagation();
|
|
31914
|
-
this.#handleItemDblClick(n);
|
|
31915
|
-
}}
|
|
31916
|
-
@pointerenter=${() => this.tickled.handlePointerEnter(key)}
|
|
31917
|
-
>
|
|
31918
|
-
<circle class="node-hit" r=${Math.max(scaledRadius, .035 * RENDER_SCALE)} part="hit" />
|
|
31919
|
-
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
31920
|
-
${showLabel ? svg`<text class="hyperbolictree-label" part="label" x=${scaledRadius + .01 * RENDER_SCALE} y="0" dominant-baseline="middle">${node.item.label}</text>` : nothing}
|
|
31921
|
-
</g>
|
|
31922
|
-
`;
|
|
33656
|
+
#renderNodes(nodes, ctx) {
|
|
33657
|
+
return renderNodes(nodes, this.nodeShape, {
|
|
33658
|
+
ctx,
|
|
33659
|
+
state: {
|
|
33660
|
+
isSelected: (node) => this._selectedNodes.has(node),
|
|
33661
|
+
isTickled: (id) => this.tickled.isTickled(id),
|
|
33662
|
+
showTitle: (n) => this.disableChildPreview || !n.node.children?.length,
|
|
33663
|
+
isFiltered: (n) => !this.#matchesFilter(n.node, n.depth),
|
|
33664
|
+
isPending: (node) => this.#lazy.isPending(node)
|
|
33665
|
+
},
|
|
33666
|
+
handlers: {
|
|
33667
|
+
onPointerDown: (e) => this.#handleNodePointerDown(e),
|
|
33668
|
+
onClick: (n, e) => {
|
|
33669
|
+
e.stopPropagation();
|
|
33670
|
+
this.#handleItemClick(n, e);
|
|
33671
|
+
},
|
|
33672
|
+
onDblClick: (n, e) => {
|
|
33673
|
+
e.stopPropagation();
|
|
33674
|
+
this.#handleItemDblClick(n);
|
|
33675
|
+
},
|
|
33676
|
+
onPointerEnter: (id) => this.tickled.handlePointerEnter(id)
|
|
33677
|
+
}
|
|
33678
|
+
});
|
|
31923
33679
|
}
|
|
31924
33680
|
render() {
|
|
31925
33681
|
const { nodes, edges } = this.#layout();
|
|
31926
|
-
const
|
|
31927
|
-
const
|
|
31928
|
-
|
|
31929
|
-
|
|
31930
|
-
|
|
31931
|
-
|
|
31932
|
-
}
|
|
31933
|
-
const labelKeys = this.#computeLabelKeys(nodes);
|
|
33682
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
33683
|
+
const branches = branchIndices(this.#effectiveRoot);
|
|
33684
|
+
const ctx = this.#nodeContext(nodes, branches);
|
|
33685
|
+
this.#lastLabelIds = ctx.labelIds;
|
|
33686
|
+
const { zoom, offset } = this.#zoomState();
|
|
33687
|
+
const pan = offset.x === 0 && offset.y === 0 ? `` : `translate(${offset.x * 500} ${offset.y * 500}) `;
|
|
31934
33688
|
return html`
|
|
31935
33689
|
<svg
|
|
31936
33690
|
class="hyperbolictree"
|
|
31937
|
-
viewBox="${-
|
|
33691
|
+
viewBox="${-525} ${-525} ${1050} ${1050}"
|
|
31938
33692
|
preserveAspectRatio="xMidYMid meet"
|
|
31939
33693
|
role="tree"
|
|
31940
33694
|
tabindex="0"
|
|
31941
33695
|
data-input-mode=${this.tickled.inputMode}
|
|
33696
|
+
data-node-shape=${this.nodeShape}
|
|
31942
33697
|
@keydown=${(e) => this.#handleKeyDown(e)}
|
|
31943
|
-
@pointerdown=${(e) => this.#
|
|
31944
|
-
@pointermove=${(e) => this.#
|
|
31945
|
-
@pointerup=${(e) => this.#
|
|
33698
|
+
@pointerdown=${(e) => this.#gestures.onPointerDown(e)}
|
|
33699
|
+
@pointermove=${(e) => this.#gestures.onPointerMove(e)}
|
|
33700
|
+
@pointerup=${(e) => this.#gestures.onPointerUp(e)}
|
|
33701
|
+
@pointercancel=${(e) => this.#gestures.onPointerUp(e)}
|
|
33702
|
+
@wheel=${(e) => this.#gestures.onWheel(e)}
|
|
31946
33703
|
@pointerleave=${() => this.tickled.clearTickled()}
|
|
31947
33704
|
>
|
|
31948
|
-
<g class="zoom" transform="scale(${
|
|
31949
|
-
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${
|
|
33705
|
+
<g class="zoom" transform="${pan}scale(${zoom})">
|
|
33706
|
+
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${500} />
|
|
31950
33707
|
<g class="edges" part="edges">
|
|
31951
|
-
${
|
|
33708
|
+
${renderEdges(edges, byId, branches, this.edgeRenderer)}
|
|
31952
33709
|
</g>
|
|
31953
33710
|
<g class="nodes" part="nodes">
|
|
31954
|
-
${
|
|
33711
|
+
${this.#renderNodes(nodes, ctx)}
|
|
31955
33712
|
</g>
|
|
31956
33713
|
</g>
|
|
31957
33714
|
</svg>
|
|
33715
|
+
${renderChildPreview(this.#previewFor(nodes), {
|
|
33716
|
+
onPointerEnter: () => this.#preview.pointerEnter(),
|
|
33717
|
+
onPointerLeave: () => this.#preview.pointerLeave(this.tickled.tickledId),
|
|
33718
|
+
onPick: (child) => this.#pickPreviewChild(child)
|
|
33719
|
+
})}
|
|
31958
33720
|
${this.search.renderField()}
|
|
31959
33721
|
`;
|
|
31960
33722
|
}
|
|
@@ -31962,102 +33724,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31962
33724
|
this.styles = [
|
|
31963
33725
|
themeFallbacks,
|
|
31964
33726
|
listSearchStyles,
|
|
31965
|
-
|
|
31966
|
-
:host {
|
|
31967
|
-
--hyperbolictree-node-color: var(--surface-4);
|
|
31968
|
-
--hyperbolictree-node-text: var(--surface-3-text);
|
|
31969
|
-
--hyperbolictree-edge-color: var(--surface-6);
|
|
31970
|
-
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
31971
|
-
--hyperbolictree-font-size: 25px;
|
|
31972
|
-
|
|
31973
|
-
display: block;
|
|
31974
|
-
overflow: hidden;
|
|
31975
|
-
outline: none;
|
|
31976
|
-
overscroll-behavior: contain;
|
|
31977
|
-
}
|
|
31978
|
-
|
|
31979
|
-
/*
|
|
31980
|
-
* delegatesFocus means the host itself reports as focused whenever
|
|
31981
|
-
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
31982
|
-
* the host also picks up the browser's own default focus ring/fill
|
|
31983
|
-
* sized to the whole box.
|
|
31984
|
-
*/
|
|
31985
|
-
:host(:focus),
|
|
31986
|
-
:host(:focus-visible) {
|
|
31987
|
-
outline: none;
|
|
31988
|
-
}
|
|
31989
|
-
|
|
31990
|
-
.hyperbolictree {
|
|
31991
|
-
display: block;
|
|
31992
|
-
width: 100%;
|
|
31993
|
-
height: 100%;
|
|
31994
|
-
outline: none;
|
|
31995
|
-
touch-action: none;
|
|
31996
|
-
overscroll-behavior: contain;
|
|
31997
|
-
}
|
|
31998
|
-
|
|
31999
|
-
.disk-boundary {
|
|
32000
|
-
fill: none;
|
|
32001
|
-
stroke: var(--hyperbolictree-boundary-color);
|
|
32002
|
-
stroke-width: 1;
|
|
32003
|
-
}
|
|
32004
|
-
|
|
32005
|
-
.edge {
|
|
32006
|
-
fill: none;
|
|
32007
|
-
stroke: var(--hyperbolictree-edge-color);
|
|
32008
|
-
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
32009
|
-
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
32010
|
-
}
|
|
32011
|
-
|
|
32012
|
-
.node {
|
|
32013
|
-
cursor: pointer;
|
|
32014
|
-
}
|
|
32015
|
-
|
|
32016
|
-
.node-hit {
|
|
32017
|
-
fill: transparent;
|
|
32018
|
-
pointer-events: all;
|
|
32019
|
-
}
|
|
32020
|
-
|
|
32021
|
-
.node-dot {
|
|
32022
|
-
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
32023
|
-
fill: color-mix(in oklab, var(--hyperbolictree-node-color), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
32024
|
-
stroke: var(--surface-2);
|
|
32025
|
-
stroke-width: 1;
|
|
32026
|
-
pointer-events: none;
|
|
32027
|
-
}
|
|
32028
|
-
|
|
32029
|
-
.node.tickled .node-dot {
|
|
32030
|
-
fill: var(--item-bg-tickled);
|
|
32031
|
-
}
|
|
32032
|
-
|
|
32033
|
-
.node.selected .node-dot {
|
|
32034
|
-
fill: var(--item-bg-selected);
|
|
32035
|
-
}
|
|
32036
|
-
|
|
32037
|
-
.node.selected.tickled .node-dot {
|
|
32038
|
-
fill: var(--item-bg-selected-tickled);
|
|
32039
|
-
}
|
|
32040
|
-
|
|
32041
|
-
.node.focused .node-dot {
|
|
32042
|
-
stroke: var(--accent);
|
|
32043
|
-
stroke-width: 3;
|
|
32044
|
-
}
|
|
32045
|
-
|
|
32046
|
-
.node.filtered {
|
|
32047
|
-
opacity: 0.3;
|
|
32048
|
-
}
|
|
32049
|
-
|
|
32050
|
-
.node.loading .node-dot {
|
|
32051
|
-
opacity: 0.6;
|
|
32052
|
-
}
|
|
32053
|
-
|
|
32054
|
-
.hyperbolictree-label {
|
|
32055
|
-
font-size: var(--hyperbolictree-font-size);
|
|
32056
|
-
fill: var(--hyperbolictree-node-text);
|
|
32057
|
-
cursor: pointer;
|
|
32058
|
-
user-select: none;
|
|
32059
|
-
}
|
|
32060
|
-
`
|
|
33727
|
+
hyperbolicTreeStyles
|
|
32061
33728
|
];
|
|
32062
33729
|
}
|
|
32063
33730
|
};
|
|
@@ -32080,6 +33747,24 @@ __decorate([property({
|
|
|
32080
33747
|
type: Number,
|
|
32081
33748
|
attribute: `max-radius`
|
|
32082
33749
|
})], HyperbolicTreeListElement.prototype, "maxRadius", void 0);
|
|
33750
|
+
__decorate([property({
|
|
33751
|
+
type: String,
|
|
33752
|
+
attribute: `node-shape`,
|
|
33753
|
+
reflect: true
|
|
33754
|
+
})], HyperbolicTreeListElement.prototype, "nodeShape", void 0);
|
|
33755
|
+
__decorate([property({
|
|
33756
|
+
type: Number,
|
|
33757
|
+
attribute: `box-max-chars`
|
|
33758
|
+
})], HyperbolicTreeListElement.prototype, "boxMaxChars", void 0);
|
|
33759
|
+
__decorate([property({
|
|
33760
|
+
type: Number,
|
|
33761
|
+
attribute: `min-label-size`
|
|
33762
|
+
})], HyperbolicTreeListElement.prototype, "minLabelSize", void 0);
|
|
33763
|
+
__decorate([property({
|
|
33764
|
+
type: Boolean,
|
|
33765
|
+
attribute: `color-by-branch`,
|
|
33766
|
+
reflect: true
|
|
33767
|
+
})], HyperbolicTreeListElement.prototype, "colorByBranch", void 0);
|
|
32083
33768
|
__decorate([property({
|
|
32084
33769
|
type: Number,
|
|
32085
33770
|
attribute: `node-radius`
|
|
@@ -32097,7 +33782,23 @@ __decorate([property({
|
|
|
32097
33782
|
attribute: `max-weight-scale`
|
|
32098
33783
|
})], HyperbolicTreeListElement.prototype, "maxWeightScale", void 0);
|
|
32099
33784
|
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "zoom", void 0);
|
|
33785
|
+
__decorate([property({
|
|
33786
|
+
type: Boolean,
|
|
33787
|
+
attribute: `disable-zoom-gestures`
|
|
33788
|
+
})], HyperbolicTreeListElement.prototype, "disableZoomGestures", void 0);
|
|
33789
|
+
__decorate([property({
|
|
33790
|
+
type: Boolean,
|
|
33791
|
+
attribute: `disable-radial-labels`
|
|
33792
|
+
})], HyperbolicTreeListElement.prototype, "disableRadialLabels", void 0);
|
|
33793
|
+
__decorate([property({
|
|
33794
|
+
type: Boolean,
|
|
33795
|
+
attribute: `disable-child-preview`
|
|
33796
|
+
})], HyperbolicTreeListElement.prototype, "disableChildPreview", void 0);
|
|
32100
33797
|
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "curvature", void 0);
|
|
33798
|
+
__decorate([property({
|
|
33799
|
+
type: Number,
|
|
33800
|
+
attribute: `sibling-spacing`
|
|
33801
|
+
})], HyperbolicTreeListElement.prototype, "siblingSpacing", void 0);
|
|
32101
33802
|
__decorate([property({
|
|
32102
33803
|
type: Number,
|
|
32103
33804
|
attribute: `focus-animation-duration`
|
|
@@ -32116,9 +33817,12 @@ __decorate([property({
|
|
|
32116
33817
|
attribute: `search-modes`,
|
|
32117
33818
|
reflect: true
|
|
32118
33819
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
32119
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
32120
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
32121
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
33820
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
|
|
33821
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
|
|
33822
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
|
|
33823
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
|
|
33824
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_previewId", void 0);
|
|
33825
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_hostSize", void 0);
|
|
32122
33826
|
HyperbolicTreeListElement = __decorate([safeCustomElement(`ixfx-hyperbolictree-list`)], HyperbolicTreeListElement);
|
|
32123
33827
|
//#endregion
|
|
32124
33828
|
//#region src/icicle-tree/layout.ts
|
|
@@ -37886,6 +39590,9 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37886
39590
|
const open = this._isOpen(node, depth);
|
|
37887
39591
|
const isBranch = node.item.isLeaf !== true;
|
|
37888
39592
|
const icon = node.item.icon;
|
|
39593
|
+
const iconColor = appearanceColor(node.item.iconColor);
|
|
39594
|
+
const textColor = appearanceColor(node.item.textColor);
|
|
39595
|
+
const badge = appearanceAbsent(node.item.badge) ? void 0 : node.item.badge;
|
|
37889
39596
|
const tooltip = node.item.tooltip;
|
|
37890
39597
|
const isCheckedMode = this.interactionMode === `checked`;
|
|
37891
39598
|
let checkboxTpl = nothing;
|
|
@@ -37913,8 +39620,9 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37913
39620
|
@pointerenter=${() => this._handleItemPointerEnter(node, depth)}
|
|
37914
39621
|
>
|
|
37915
39622
|
${checkboxTpl}
|
|
37916
|
-
${icon ? html`<ixfx-icon class="item-icon" name="${icon}"></ixfx-icon>` : nothing}
|
|
37917
|
-
<span class="label">${node.item.label}</span>
|
|
39623
|
+
${icon ? html`<ixfx-icon class="item-icon" name="${icon}" style=${iconColor ? `color:${iconColor}` : nothing}></ixfx-icon>` : nothing}
|
|
39624
|
+
<span class="label" style=${textColor ? `color:${textColor}` : nothing}>${node.item.label}</span>
|
|
39625
|
+
${badge === void 0 ? nothing : html`<span class="item-badge" part="badge">${badge}</span>`}
|
|
37918
39626
|
${tooltip ? html`<ixfx-tooltip class="item-tooltip" anchor="--tooltip-miller-${depth}-${node.item.key}" .value=${tooltip}></ixfx-tooltip>` : nothing}
|
|
37919
39627
|
${isBranch ? html`<span class="caret" aria-hidden="true">›</span>` : nothing}
|
|
37920
39628
|
</li>
|
|
@@ -37968,6 +39676,17 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37968
39676
|
text-overflow: ellipsis;
|
|
37969
39677
|
}
|
|
37970
39678
|
|
|
39679
|
+
/* Finder-style badge: sits between the label and the caret, flush to
|
|
39680
|
+
the right edge of the row. The label is flex:1, so the gap opens up
|
|
39681
|
+
ahead of the badge automatically. */
|
|
39682
|
+
.item-badge {
|
|
39683
|
+
flex-shrink: 0;
|
|
39684
|
+
display: inline-flex;
|
|
39685
|
+
align-items: center;
|
|
39686
|
+
margin-inline-start: auto;
|
|
39687
|
+
padding-inline-start: var(--miller-list-badge-gap, 0.5em);
|
|
39688
|
+
}
|
|
39689
|
+
|
|
37971
39690
|
.item-tooltip {
|
|
37972
39691
|
position: absolute;
|
|
37973
39692
|
}
|
|
@@ -38512,6 +40231,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
38512
40231
|
--multicol-list-border: var(--border);
|
|
38513
40232
|
--multicol-list-min-column-width: ${MIN_COLUMN_WIDTH}px;
|
|
38514
40233
|
--multicol-list-max-column-width: ${MAX_COLUMN_WIDTH}px;
|
|
40234
|
+
--multicol-list-header-font-size: unset;
|
|
38515
40235
|
|
|
38516
40236
|
display: block;
|
|
38517
40237
|
overflow: hidden;
|
|
@@ -38577,7 +40297,7 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
38577
40297
|
text-overflow: ellipsis;
|
|
38578
40298
|
white-space: nowrap;
|
|
38579
40299
|
padding: 0 var(--space-s);
|
|
38580
|
-
font-size: var(--
|
|
40300
|
+
font-size: var(--multicol-list-header-font-size);
|
|
38581
40301
|
font-weight: 600;
|
|
38582
40302
|
background: transparent;
|
|
38583
40303
|
border: none;
|
|
@@ -42859,8 +44579,14 @@ let RangeInput = class RangeInput extends WheelNudgeMixin(LitElement) {
|
|
|
42859
44579
|
const clamped = Math.min(this.max, Math.max(this.min, candidate));
|
|
42860
44580
|
if (clamped === this.value) return;
|
|
42861
44581
|
input.value = String(clamped);
|
|
42862
|
-
input.dispatchEvent(new
|
|
42863
|
-
|
|
44582
|
+
input.dispatchEvent(new CustomEvent(`input`, {
|
|
44583
|
+
detail: { value: clamped },
|
|
44584
|
+
bubbles: true
|
|
44585
|
+
}));
|
|
44586
|
+
input.dispatchEvent(new CustomEvent(`change`, {
|
|
44587
|
+
detail: { value: clamped },
|
|
44588
|
+
bubbles: true
|
|
44589
|
+
}));
|
|
42864
44590
|
}
|
|
42865
44591
|
render() {
|
|
42866
44592
|
const tooltipEl = this.tooltip ? html`
|
|
@@ -44303,6 +46029,7 @@ let ShapefillValueElement = class ShapefillValueElement extends WheelNudgeMixin(
|
|
|
44303
46029
|
this.#dragging = false;
|
|
44304
46030
|
this.#dragStartY = 0;
|
|
44305
46031
|
this.#valueAtDragStart = 0;
|
|
46032
|
+
this.#lastPointerY = 0;
|
|
44306
46033
|
this.#onPointerDown = (event) => {
|
|
44307
46034
|
if (this.disabled) return;
|
|
44308
46035
|
event.preventDefault();
|
|
@@ -44310,7 +46037,9 @@ let ShapefillValueElement = class ShapefillValueElement extends WheelNudgeMixin(
|
|
|
44310
46037
|
svgEl?.focus();
|
|
44311
46038
|
this.#dragging = true;
|
|
44312
46039
|
this.#dragStartY = event.clientY;
|
|
46040
|
+
this.#lastPointerY = event.clientY;
|
|
44313
46041
|
this.#valueAtDragStart = this.value;
|
|
46042
|
+
this.#lastSetValue = this.value;
|
|
44314
46043
|
this.toggleAttribute(`dragging`, true);
|
|
44315
46044
|
if (svgEl) svgEl.setPointerCapture(event.pointerId);
|
|
44316
46045
|
window.addEventListener(`pointermove`, this.#onPointerMove);
|
|
@@ -44320,10 +46049,16 @@ let ShapefillValueElement = class ShapefillValueElement extends WheelNudgeMixin(
|
|
|
44320
46049
|
this.#onPointerMove = (event) => {
|
|
44321
46050
|
if (!this.#dragging) return;
|
|
44322
46051
|
event.preventDefault();
|
|
46052
|
+
if (this.value !== this.#lastSetValue) {
|
|
46053
|
+
this.#valueAtDragStart = this.value;
|
|
46054
|
+
this.#dragStartY = this.#lastPointerY;
|
|
46055
|
+
this.#lastSetValue = this.value;
|
|
46056
|
+
}
|
|
44323
46057
|
const effectiveSensitivity = this.clientHeight * 2 * (event.shiftKey ? 10 : 1);
|
|
44324
46058
|
const deltaValue = (this.#dragStartY - event.clientY) / effectiveSensitivity * (this.max - this.min);
|
|
44325
46059
|
const relativeValue = scale(this.#valueAtDragStart + deltaValue, this.min, this.max, 0, 1);
|
|
44326
46060
|
this.#setValueFromRelative(clampValue(relativeValue, 0, 1), true);
|
|
46061
|
+
this.#lastPointerY = event.clientY;
|
|
44327
46062
|
};
|
|
44328
46063
|
this.#onPointerUp = () => {
|
|
44329
46064
|
this.#dragging = false;
|
|
@@ -44366,6 +46101,10 @@ let ShapefillValueElement = class ShapefillValueElement extends WheelNudgeMixin(
|
|
|
44366
46101
|
#dragging;
|
|
44367
46102
|
#dragStartY;
|
|
44368
46103
|
#valueAtDragStart;
|
|
46104
|
+
/** Client Y of the last processed pointer event, for external-write re-baselining. */
|
|
46105
|
+
#lastPointerY;
|
|
46106
|
+
/** Value at the drag's last write — detects external `value` writes mid-drag. */
|
|
46107
|
+
#lastSetValue;
|
|
44369
46108
|
#changeDebounceTimer;
|
|
44370
46109
|
#getRelativeValue() {
|
|
44371
46110
|
return scale(this.value, this.min, this.max, 0, 1);
|
|
@@ -44383,6 +46122,7 @@ let ShapefillValueElement = class ShapefillValueElement extends WheelNudgeMixin(
|
|
|
44383
46122
|
if (clampedValue === this.value) return;
|
|
44384
46123
|
const previousValue = this.value;
|
|
44385
46124
|
this.value = clampedValue;
|
|
46125
|
+
this.#lastSetValue = clampedValue;
|
|
44386
46126
|
if (this.hasAttribute(`data-tooltip-live`)) {
|
|
44387
46127
|
const percentage = Math.round(this.#getRelativeValue() * 100);
|
|
44388
46128
|
this.setAttribute(`data-tooltip`, `${percentage}%`);
|
|
@@ -51538,6 +53278,6 @@ function init() {
|
|
|
51538
53278
|
console.log(`Init`);
|
|
51539
53279
|
}
|
|
51540
53280
|
//#endregion
|
|
51541
|
-
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 };
|
|
53281
|
+
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 };
|
|
51542
53282
|
|
|
51543
53283
|
//# sourceMappingURL=index.js.map
|