@ixfx/components 0.11.4 → 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 +442 -120
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2230 -538
- 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/{data-grid-CfiaBqTA.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 +66 -2
- 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-CDllxjVG.d.ts → index-CnyoNA6m.d.ts} +4 -4
- package/dist/{index-CDllxjVG.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 +2036 -644
- 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-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-range-input.d.ts +1 -1
- package/dist/labelled-slider-input.d.ts +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-item-DlxAb0Xr.d.ts → menu-item-DT7bKAIw.d.ts} +2 -2
- package/dist/{menu-item-DlxAb0Xr.d.ts.map → menu-item-DT7bKAIw.d.ts.map} +1 -1
- package/dist/menu.d.ts +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/{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/vertical-list.md +63 -0
- package/package.json +1 -1
- package/dist/data-grid-CfiaBqTA.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/types-3Jfcaxvo.d.ts.map +0 -1
- package/dist/vertical-list-R25WqAVO.d.ts.map +0 -1
package/bundle/index.js
CHANGED
|
@@ -12013,6 +12013,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12013
12013
|
this.path = ``;
|
|
12014
12014
|
this.separator = `/`;
|
|
12015
12015
|
this.pathRoot = ``;
|
|
12016
|
+
this.hintPrevious = false;
|
|
12016
12017
|
this._openIdx = null;
|
|
12017
12018
|
this._editing = false;
|
|
12018
12019
|
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
@@ -12024,6 +12025,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12024
12025
|
this._openCumPath = null;
|
|
12025
12026
|
this._dragCumPath = null;
|
|
12026
12027
|
this._dropCumPath = null;
|
|
12028
|
+
this._hintSegments = null;
|
|
12027
12029
|
this.#uid = Math.random().toString(36).slice(2, 8);
|
|
12028
12030
|
this.#clickSuppressUntil = 0;
|
|
12029
12031
|
this.#onPopoverToggle = (e) => {
|
|
@@ -12159,6 +12161,19 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12159
12161
|
#clicksSuppressed() {
|
|
12160
12162
|
return Date.now() < this.#clickSuppressUntil || this._dragCumPath !== null;
|
|
12161
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
|
+
}
|
|
12162
12177
|
#resolveSegmentIcon(fullPathSegments, index) {
|
|
12163
12178
|
const direct = this.segmentIconLookup?.(fullPathSegments, index);
|
|
12164
12179
|
if (typeof direct === `string` && direct.length > 0) return direct;
|
|
@@ -12304,12 +12319,63 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12304
12319
|
composed: true
|
|
12305
12320
|
}));
|
|
12306
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
|
+
}
|
|
12307
12371
|
#onLabelClick(index, event) {
|
|
12308
12372
|
if (this.#clicksSuppressed()) return;
|
|
12309
12373
|
event.preventDefault();
|
|
12310
12374
|
event.stopPropagation();
|
|
12311
12375
|
this.#closeDropdown();
|
|
12312
|
-
const
|
|
12376
|
+
const segments = this.#getSegments();
|
|
12377
|
+
const truncatedSegments = segments.slice(0, index + 1);
|
|
12378
|
+
this.#recordHintOnShorten(segments, truncatedSegments);
|
|
12313
12379
|
const hadLeading = this.path.startsWith(this.separator);
|
|
12314
12380
|
const newPath = truncatedSegments.join(this.separator);
|
|
12315
12381
|
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
@@ -12327,6 +12393,31 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12327
12393
|
composed: true
|
|
12328
12394
|
}));
|
|
12329
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
|
+
}
|
|
12330
12421
|
#selectSibling(index, segment) {
|
|
12331
12422
|
const cumPath = this._openCumPath;
|
|
12332
12423
|
this.#closeDropdown();
|
|
@@ -12402,7 +12493,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12402
12493
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
12403
12494
|
<label title=${label}
|
|
12404
12495
|
part="label"
|
|
12405
|
-
|
|
12496
|
+
draggable=${draggable ? `true` : `false`}
|
|
12406
12497
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
12407
12498
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
12408
12499
|
@dragend=${() => this.#onLabelDragEnd()}>
|
|
@@ -12419,6 +12510,26 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12419
12510
|
</div>
|
|
12420
12511
|
` : nothing}
|
|
12421
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>
|
|
12422
12533
|
`;
|
|
12423
12534
|
}
|
|
12424
12535
|
#renderRootSegment(label, index) {
|
|
@@ -12440,7 +12551,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12440
12551
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
12441
12552
|
<label title=${label}
|
|
12442
12553
|
part="label"
|
|
12443
|
-
|
|
12554
|
+
draggable=${draggable ? `true` : `false`}
|
|
12444
12555
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
12445
12556
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
12446
12557
|
@dragend=${() => this.#onLabelDragEnd()}>${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}${label}</label>
|
|
@@ -12546,6 +12657,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12546
12657
|
#onHostKeyDown;
|
|
12547
12658
|
render() {
|
|
12548
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) : [];
|
|
12549
12662
|
if (this._editing) {
|
|
12550
12663
|
const editValue = this.path;
|
|
12551
12664
|
return html`
|
|
@@ -12568,10 +12681,15 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12568
12681
|
${this.separator}
|
|
12569
12682
|
</div>` : nothing;
|
|
12570
12683
|
return html`
|
|
12571
|
-
<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)}>
|
|
12572
12689
|
${rootSegments.length > 0 ? rootSegments.map((label, i) => this.#renderRootSegment(label, i)) : nothing}
|
|
12573
12690
|
${leadingCaret}
|
|
12574
12691
|
${segments.map((label, i) => this.#renderSegment(label, i, i === segments.length - 1))}
|
|
12692
|
+
${virtualSegments.map((label, i) => this.#renderVirtualSegment(label, segments.length + i))}
|
|
12575
12693
|
<div class="trailing" @click=${this.#onTrailingClick}></div>
|
|
12576
12694
|
</div>
|
|
12577
12695
|
${this._openIdx !== null ? html`
|
|
@@ -12598,6 +12716,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12598
12716
|
--padding-m: var(--space-s);
|
|
12599
12717
|
--hover-bg: var(--surface-5);
|
|
12600
12718
|
--hover-fg: var(--surface-5-text);
|
|
12719
|
+
--crumb-hint-opacity: 0.35;
|
|
12720
|
+
--crumb-hint-hover-opacity: 0.6;
|
|
12601
12721
|
|
|
12602
12722
|
display: flex;
|
|
12603
12723
|
width: 100%;
|
|
@@ -12709,6 +12829,14 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12709
12829
|
background: var(--crumb-drop-into-bg, color-mix(in srgb, var(--accent) 14%, transparent));
|
|
12710
12830
|
}
|
|
12711
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
|
+
|
|
12712
12840
|
.crumb-icon {
|
|
12713
12841
|
width: 1em;
|
|
12714
12842
|
height: 1em;
|
|
@@ -12793,6 +12921,10 @@ __decorate([property({ type: Object })], CrumbPathElement.prototype, "siblingLoo
|
|
|
12793
12921
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentIconLookup", void 0);
|
|
12794
12922
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentDragLookup", void 0);
|
|
12795
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);
|
|
12796
12928
|
__decorate([state()], CrumbPathElement.prototype, "_openIdx", void 0);
|
|
12797
12929
|
__decorate([state()], CrumbPathElement.prototype, "_editing", void 0);
|
|
12798
12930
|
__decorate([state()], CrumbPathElement.prototype, "_cachedSiblings", void 0);
|
|
@@ -12804,6 +12936,7 @@ __decorate([state()], CrumbPathElement.prototype, "_editStartIndex", void 0);
|
|
|
12804
12936
|
__decorate([state()], CrumbPathElement.prototype, "_openCumPath", void 0);
|
|
12805
12937
|
__decorate([state()], CrumbPathElement.prototype, "_dragCumPath", void 0);
|
|
12806
12938
|
__decorate([state()], CrumbPathElement.prototype, "_dropCumPath", void 0);
|
|
12939
|
+
__decorate([state()], CrumbPathElement.prototype, "_hintSegments", void 0);
|
|
12807
12940
|
CrumbPathElement = __decorate([safeCustomElement(`ixfx-crumb-path`)], CrumbPathElement);
|
|
12808
12941
|
//#endregion
|
|
12809
12942
|
//#region src/data-display/data-display.ts
|
|
@@ -13127,6 +13260,34 @@ let DataDisplayComponent = class DataDisplayComponent extends LitElement {
|
|
|
13127
13260
|
__decorate([property({ type: Boolean })], DataDisplayComponent.prototype, "automap", void 0);
|
|
13128
13261
|
DataDisplayComponent = __decorate([safeCustomElement(`ixfx-data-display`)], DataDisplayComponent);
|
|
13129
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
|
|
13130
13291
|
//#region src/util/drag-ghost.ts
|
|
13131
13292
|
/**
|
|
13132
13293
|
* Theme-aware floating element helper.
|
|
@@ -14251,6 +14412,12 @@ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
|
|
|
14251
14412
|
PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
|
|
14252
14413
|
//#endregion
|
|
14253
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
|
+
};
|
|
14254
14421
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
14255
14422
|
function sameKeys$3(a, b) {
|
|
14256
14423
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -14275,6 +14442,9 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
14275
14442
|
this.allowGrouping = false;
|
|
14276
14443
|
this.crossGroupSelection = false;
|
|
14277
14444
|
this.columnMenu = `auto`;
|
|
14445
|
+
this.textColorColumn = ``;
|
|
14446
|
+
this.iconColumn = ``;
|
|
14447
|
+
this.badgeColumn = ``;
|
|
14278
14448
|
this.itemsDraggable = false;
|
|
14279
14449
|
this._columnFormatters = /* @__PURE__ */ new Map();
|
|
14280
14450
|
this._resizingColKey = void 0;
|
|
@@ -14694,6 +14864,25 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
14694
14864
|
}
|
|
14695
14865
|
.cell.editable-text { cursor: text; }
|
|
14696
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
|
+
}
|
|
14697
14886
|
.checkbox-col { width: 28px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
14698
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; }
|
|
14699
14888
|
.header-cell[drag-source] { opacity: 0.4; }
|
|
@@ -15465,6 +15654,27 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15465
15654
|
if (this.globalFormatter) return this.globalFormatter(value, col.key, row);
|
|
15466
15655
|
return value;
|
|
15467
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
|
+
}
|
|
15468
15678
|
_handleCellSecondClick(rowKey, col) {
|
|
15469
15679
|
if (this.editability !== `user`) return;
|
|
15470
15680
|
const now = Date.now();
|
|
@@ -15922,7 +16132,9 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15922
16132
|
const textEditable = this._isEditable(row, col) && this._isTextEditor(col);
|
|
15923
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));` : ``;
|
|
15924
16134
|
const canPan = this.cellOverflow === `pan-text` && !col.renderer && (selected || tickled);
|
|
15925
|
-
|
|
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}"
|
|
15926
16138
|
@click=${(e) => {
|
|
15927
16139
|
this._lastClickedCell = {
|
|
15928
16140
|
rowKey,
|
|
@@ -15944,7 +16156,7 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15944
16156
|
composed: true
|
|
15945
16157
|
}));
|
|
15946
16158
|
}}
|
|
15947
|
-
>${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>`;
|
|
15948
16160
|
})}
|
|
15949
16161
|
</div>
|
|
15950
16162
|
`;
|
|
@@ -16004,6 +16216,18 @@ __decorate([property({
|
|
|
16004
16216
|
attribute: `column-menu`
|
|
16005
16217
|
})], DataGridElement.prototype, "columnMenu", void 0);
|
|
16006
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);
|
|
16007
16231
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
|
|
16008
16232
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
|
|
16009
16233
|
__decorate([property({
|
|
@@ -16304,6 +16528,14 @@ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableE
|
|
|
16304
16528
|
const ITEM_SELECTOR$3 = `:scope > li`;
|
|
16305
16529
|
const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
|
|
16306
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)`;
|
|
16307
16539
|
function pointInRect(x, y, rect) {
|
|
16308
16540
|
return rect.width > 0 && rect.height > 0 && x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
|
16309
16541
|
}
|
|
@@ -16337,6 +16569,9 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16337
16569
|
this.items = [];
|
|
16338
16570
|
this.displayProperty = ``;
|
|
16339
16571
|
this.iconProperty = ``;
|
|
16572
|
+
this.iconColorProperty = ``;
|
|
16573
|
+
this.textColorProperty = ``;
|
|
16574
|
+
this.badgeProperty = ``;
|
|
16340
16575
|
this.keyProperty = ``;
|
|
16341
16576
|
this.itemWrap = `stop`;
|
|
16342
16577
|
this.itemsDraggable = false;
|
|
@@ -16538,7 +16773,8 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16538
16773
|
};
|
|
16539
16774
|
this._onWheel = (e) => {
|
|
16540
16775
|
e.preventDefault();
|
|
16541
|
-
|
|
16776
|
+
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
16777
|
+
this._listEl.scrollLeft += delta;
|
|
16542
16778
|
};
|
|
16543
16779
|
}
|
|
16544
16780
|
#itemData;
|
|
@@ -16743,7 +16979,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16743
16979
|
});
|
|
16744
16980
|
return;
|
|
16745
16981
|
}
|
|
16746
|
-
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);
|
|
16747
16983
|
this.#syncFromProvider();
|
|
16748
16984
|
if (changed.has(`sort`) || changed.has(`sortBy`) || changed.has(`sortComparator`)) this.#applySort();
|
|
16749
16985
|
if (changed.has(`filterPredicate`)) {
|
|
@@ -16948,24 +17184,36 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16948
17184
|
const li = document.createElement(`li`);
|
|
16949
17185
|
this.#itemData.set(li, item);
|
|
16950
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);
|
|
16951
17190
|
if (typeof content === `string`) {
|
|
16952
|
-
this._setLabel(li, content);
|
|
17191
|
+
const span = this._setLabel(li, content);
|
|
17192
|
+
if (textColor !== void 0) span.style.color = textColor;
|
|
16953
17193
|
if (this.iconProperty) {
|
|
16954
17194
|
const icon = this.#propertyValue(item, this.iconProperty);
|
|
16955
17195
|
if (typeof icon === `string` && icon.length > 0) li.setAttribute(`data-icon`, icon);
|
|
16956
17196
|
}
|
|
16957
|
-
} 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
|
+
}
|
|
16958
17201
|
if (this.keyProperty) {
|
|
16959
17202
|
const key = this.#propertyValue(item, this.keyProperty);
|
|
16960
17203
|
if (key !== void 0 && key !== null) li.setAttribute(`data-key`, String(key));
|
|
16961
17204
|
}
|
|
16962
17205
|
this.#ensureIcon(li);
|
|
17206
|
+
this.#ensureBadge(li, this.#propertyValue(item, this.badgeProperty));
|
|
16963
17207
|
return li;
|
|
16964
17208
|
}
|
|
16965
17209
|
#propertyValue(item, prop) {
|
|
16966
17210
|
if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
|
|
16967
17211
|
return item[prop];
|
|
16968
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
|
+
}
|
|
16969
17217
|
#syncFromProvider() {
|
|
16970
17218
|
if (this.provider === void 0) return;
|
|
16971
17219
|
const items = this.#data.items;
|
|
@@ -17223,12 +17471,14 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17223
17471
|
if (this.interactionMode !== `checked`) return true;
|
|
17224
17472
|
return !item.hasAttribute(`not-checked`);
|
|
17225
17473
|
}
|
|
17474
|
+
/** Appends the caption span; returns it so callers can style it per item. */
|
|
17226
17475
|
_setLabel(li, label) {
|
|
17227
17476
|
const span = document.createElement(`span`);
|
|
17228
17477
|
span.style.cssText = LABEL_SPAN_CSS;
|
|
17229
17478
|
span.textContent = label;
|
|
17230
17479
|
li.appendChild(span);
|
|
17231
17480
|
li.setAttribute(`data-search-label`, label);
|
|
17481
|
+
return span;
|
|
17232
17482
|
}
|
|
17233
17483
|
/**
|
|
17234
17484
|
* Inject a leading `<ixfx-icon>` into an item carrying `data-icon`.
|
|
@@ -17242,8 +17492,28 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17242
17492
|
icon.setAttribute(`name`, name);
|
|
17243
17493
|
icon.style.marginInlineEnd = `var(--detail-list-icon-gap, 6px)`;
|
|
17244
17494
|
icon.style.flexShrink = `0`;
|
|
17495
|
+
const color = li.getAttribute(`data-icon-color`);
|
|
17496
|
+
if (color) icon.style.color = color;
|
|
17245
17497
|
li.prepend(icon);
|
|
17246
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
|
+
}
|
|
17247
17517
|
/** Scan light-DOM children for unprocessed `data-icon` items. */
|
|
17248
17518
|
#processChildren() {
|
|
17249
17519
|
for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#ensureIcon(el);
|
|
@@ -17517,6 +17787,10 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17517
17787
|
opacity: 0.6;
|
|
17518
17788
|
}
|
|
17519
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
|
+
|
|
17520
17794
|
/* checked mode: the injected checkbox communicates selection — suppress bg */
|
|
17521
17795
|
:host([interaction-mode="checked"]) ::slotted([data-selected]) {
|
|
17522
17796
|
background: transparent;
|
|
@@ -17591,6 +17865,18 @@ __decorate([property({
|
|
|
17591
17865
|
type: String,
|
|
17592
17866
|
attribute: `icon-property`
|
|
17593
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);
|
|
17594
17880
|
__decorate([property({
|
|
17595
17881
|
type: String,
|
|
17596
17882
|
attribute: `key-property`
|
|
@@ -23322,6 +23608,10 @@ var ElementGridListSource = class {
|
|
|
23322
23608
|
if (eff === void 0) return void 0;
|
|
23323
23609
|
return html`<slot name=${this.slotNameFor(this.#items[eff])}></slot>`;
|
|
23324
23610
|
}
|
|
23611
|
+
iconAt(_index) {}
|
|
23612
|
+
iconColorAt(_index) {}
|
|
23613
|
+
textColorAt(_index) {}
|
|
23614
|
+
badgeAt(_index) {}
|
|
23325
23615
|
setFilters(opts) {
|
|
23326
23616
|
if (`predicate` in opts) this.#predicate = opts.predicate;
|
|
23327
23617
|
if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
|
|
@@ -23438,6 +23728,22 @@ var DataGridListSource = class {
|
|
|
23438
23728
|
const eff = this.#effectiveIndices[index];
|
|
23439
23729
|
return eff === void 0 ? void 0 : this.#adapter.render(this.#items[eff], eff);
|
|
23440
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
|
+
}
|
|
23441
23747
|
setFilters(opts) {
|
|
23442
23748
|
if (`predicate` in opts) this.#predicate = opts.predicate;
|
|
23443
23749
|
if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
|
|
@@ -23978,6 +24284,50 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
23978
24284
|
color: inherit;
|
|
23979
24285
|
}
|
|
23980
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
|
+
|
|
23981
24331
|
/* ---- Box modes (hidden / under / overlaid): the cell is one box with a
|
|
23982
24332
|
single selection outline ---- */
|
|
23983
24333
|
:host(:not([caption-style="separate"])) .cell {
|
|
@@ -24986,6 +25336,8 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
24986
25336
|
const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
|
|
24987
25337
|
const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
|
|
24988
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;
|
|
24989
25341
|
return html`
|
|
24990
25342
|
<div
|
|
24991
25343
|
class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
|
|
@@ -25003,8 +25355,11 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
25003
25355
|
@dblclick=${() => this.#onCellActivate(key)}
|
|
25004
25356
|
@pointerenter=${() => this.#handleCellPointerEnter(key)}
|
|
25005
25357
|
>
|
|
25006
|
-
<div class="content" part="content"
|
|
25007
|
-
|
|
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)) : ``}
|
|
25008
25363
|
</div>
|
|
25009
25364
|
`;
|
|
25010
25365
|
}
|
|
@@ -25012,10 +25367,25 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
25012
25367
|
* A tickled cell swaps its plain label for an `ixfx-pan-text` in loop mode
|
|
25013
25368
|
* so a truncated label scrolls instead of staying an ellipsis. Unticked
|
|
25014
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.
|
|
25015
25374
|
*/
|
|
25016
|
-
#renderCaption(caption, captionCss, tickled) {
|
|
25017
|
-
|
|
25018
|
-
|
|
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};`;
|
|
25019
25389
|
}
|
|
25020
25390
|
/** Stop the active caption pan, if any, and forget it. */
|
|
25021
25391
|
#stopActiveCaptionPan() {
|
|
@@ -28218,6 +28588,16 @@ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data
|
|
|
28218
28588
|
* vertical rows are shorter relative to the viewport, so 3.
|
|
28219
28589
|
*/
|
|
28220
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)`;
|
|
28221
28601
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
28222
28602
|
function sameKeys(a, b) {
|
|
28223
28603
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -28627,7 +29007,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28627
29007
|
}
|
|
28628
29008
|
this.#syncSearchCommands();
|
|
28629
29009
|
}
|
|
28630
|
-
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) {
|
|
28631
29011
|
if (this.#suspendAdapterRebuild) this.#suspendAdapterRebuild = false;
|
|
28632
29012
|
else if (this.#virtualActive()) this.#rebuildAdapterVirtual();
|
|
28633
29013
|
else this.#rebuildFromAdapter();
|
|
@@ -28897,7 +29277,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28897
29277
|
const fresh = this.formatter ? this.formatter(data, index) : data;
|
|
28898
29278
|
if (el instanceof HTMLLIElement) {
|
|
28899
29279
|
for (const child of [...el.childNodes]) child.remove();
|
|
28900
|
-
this.#fillAdapterItem(el, fresh, data);
|
|
29280
|
+
this.#fillAdapterItem(el, fresh, data, index);
|
|
28901
29281
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
28902
29282
|
this._syncItemStates();
|
|
28903
29283
|
} else if (fresh instanceof Element && this.#isRowElement(fresh)) {
|
|
@@ -28952,17 +29332,47 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28952
29332
|
return content;
|
|
28953
29333
|
}
|
|
28954
29334
|
const li = document.createElement(`li`);
|
|
28955
|
-
this.#fillAdapterItem(li, content, item);
|
|
29335
|
+
this.#fillAdapterItem(li, content, item, index);
|
|
28956
29336
|
return li;
|
|
28957
29337
|
}
|
|
28958
|
-
#fillAdapterItem(li, content, item) {
|
|
29338
|
+
#fillAdapterItem(li, content, item, index) {
|
|
28959
29339
|
if (typeof content === `string`) li.textContent = content;
|
|
28960
29340
|
else if (content instanceof Node) li.appendChild(content);
|
|
28961
29341
|
else if (this.formatter && content !== void 0 && content !== null) render(content, li);
|
|
29342
|
+
this.#decorateAdapterItem(li, item, index);
|
|
28962
29343
|
this.#stampAdapterKey(li, item);
|
|
28963
29344
|
this.#itemData.set(li, item);
|
|
28964
29345
|
}
|
|
28965
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
|
+
/**
|
|
28966
29376
|
* Whether a formatter-produced element is a whole row (used directly)
|
|
28967
29377
|
* rather than row content (rendered into a `<li>`): an `<li>`, already
|
|
28968
29378
|
* marked `[data-list-item]`, or a custom element.
|
|
@@ -29484,6 +29894,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
29484
29894
|
outline: 1px solid transparent;
|
|
29485
29895
|
}
|
|
29486
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
|
+
|
|
29487
29901
|
/* Custom shadow-DOM components handle their own internal layout/padding.
|
|
29488
29902
|
Wrapped in :where() so the selector stays at zero specificity and a
|
|
29489
29903
|
consumer's own :host rule (e.g. display: flex) wins without
|
|
@@ -29630,6 +30044,10 @@ __decorate([property({
|
|
|
29630
30044
|
attribute: `key-property`
|
|
29631
30045
|
})], VerticalListElement.prototype, "keyProperty", void 0);
|
|
29632
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);
|
|
29633
30051
|
__decorate([property({
|
|
29634
30052
|
type: Boolean,
|
|
29635
30053
|
attribute: `virtualize`,
|
|
@@ -30905,6 +31323,90 @@ __decorate([state()], IxfxHexEditorElement.prototype, "_containerH", void 0);
|
|
|
30905
31323
|
__decorate([state()], IxfxHexEditorElement.prototype, "_shadeMap", void 0);
|
|
30906
31324
|
__decorate([state()], IxfxHexEditorElement.prototype, "_focusSide", void 0);
|
|
30907
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
|
+
}
|
|
30908
31410
|
//#endregion
|
|
30909
31411
|
//#region src/hyperbolic-tree/poincare-math.ts
|
|
30910
31412
|
const EPSILON = 1e-9;
|
|
@@ -31009,8 +31511,625 @@ function geodesicInterpolate(a, b, t) {
|
|
|
31009
31511
|
if (localNorm < EPSILON) return a;
|
|
31010
31512
|
return mobiusFromOrigin(complexScale(complexScale(bLocal, 1 / localNorm), euclideanRadiusForHyperbolicDistance(hyperbolicDistanceForEuclideanRadius(localNorm) * t)), a);
|
|
31011
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
|
+
`;
|
|
31012
32058
|
//#endregion
|
|
31013
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
|
+
}
|
|
31014
32133
|
/** Depth-first search for the node with `key`. Returns `undefined` when absent. */
|
|
31015
32134
|
function findHyperbolicNode(root, key) {
|
|
31016
32135
|
if (!root) return void 0;
|
|
@@ -31085,21 +32204,58 @@ function stableUnitJitter(key) {
|
|
|
31085
32204
|
* radially).
|
|
31086
32205
|
*/
|
|
31087
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
|
+
}
|
|
31088
32230
|
/**
|
|
31089
32231
|
* Canonical (true-root-anchored) hyperbolic position of every currently-loaded
|
|
31090
|
-
* node, keyed by
|
|
32232
|
+
* node, keyed by layout id (see {@link hyperbolicChildIds}). Positions never depend on the current focus —
|
|
31091
32233
|
* refocusing is purely a Möbius re-projection of these fixed coordinates, so
|
|
31092
32234
|
* the tree never needs to be re-laid-out when the focus changes.
|
|
31093
32235
|
*
|
|
31094
32236
|
* Each node holds an angular wedge — inherited from its parent, expressed in
|
|
31095
32237
|
* its *own* local frame (root starts with the full `[-π, π]`) — across which
|
|
31096
|
-
* its children are placed at hyperbolic distance
|
|
31097
|
-
*
|
|
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
|
|
31098
32242
|
* subtree size rather than split evenly, and each child's own share is
|
|
31099
32243
|
* carried into its local frame via {@link boundaryAngleInLocalFrame} before
|
|
31100
|
-
* recursing. This is a strict partition
|
|
31101
|
-
*
|
|
31102
|
-
*
|
|
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.
|
|
31103
32259
|
*
|
|
31104
32260
|
* `curvature` scales how far apart (in hyperbolic distance) each generation
|
|
31105
32261
|
* sits. Raising it pushes each generation further out, so the Poincaré
|
|
@@ -31108,7 +32264,7 @@ const CHAIN_JITTER_RADIANS = .22;
|
|
|
31108
32264
|
* in" on the focus. Lowering it spreads generations out more evenly,
|
|
31109
32265
|
* flattening the view toward a Euclidean-feeling layout.
|
|
31110
32266
|
*/
|
|
31111
|
-
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
32267
|
+
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING) {
|
|
31112
32268
|
const map = /* @__PURE__ */ new Map();
|
|
31113
32269
|
if (!root) return map;
|
|
31114
32270
|
const origin = {
|
|
@@ -31117,70 +32273,122 @@ function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
|
31117
32273
|
};
|
|
31118
32274
|
map.set(root.item.key, {
|
|
31119
32275
|
node: root,
|
|
32276
|
+
id: root.item.key,
|
|
31120
32277
|
depth: 0,
|
|
31121
32278
|
parentKey: void 0,
|
|
32279
|
+
parentId: void 0,
|
|
31122
32280
|
position: origin,
|
|
32281
|
+
toParent: identityIsometry,
|
|
31123
32282
|
angularShare: 2 * Math.PI
|
|
31124
32283
|
});
|
|
31125
|
-
const place = (node,
|
|
32284
|
+
const place = (node, id, frame, wedgeStart, wedgeEnd, depth) => {
|
|
31126
32285
|
if (depth >= maxDepth) return;
|
|
31127
32286
|
const children = node.children ?? [];
|
|
31128
32287
|
const n = children.length;
|
|
31129
32288
|
if (n === 0) return;
|
|
31130
|
-
const r0 = euclideanRadiusForHyperbolicDistance(curvature);
|
|
31131
32289
|
const angularWidth = wedgeEnd - wedgeStart;
|
|
31132
|
-
const weights = children.map((child) => subtreeSize(child, maxDepth - depth - 1));
|
|
32290
|
+
const weights = children.map((child) => Math.sqrt(subtreeSize(child, maxDepth - depth - 1)));
|
|
31133
32291
|
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
32292
|
+
const ids = hyperbolicChildIds(id, children);
|
|
31134
32293
|
let cursor = wedgeStart;
|
|
31135
32294
|
children.forEach((child, index) => {
|
|
31136
32295
|
const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
|
|
31137
|
-
const
|
|
31138
|
-
|
|
31139
|
-
|
|
31140
|
-
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));
|
|
31141
32300
|
const local = {
|
|
31142
|
-
x:
|
|
31143
|
-
y:
|
|
32301
|
+
x: r * Math.cos(theta),
|
|
32302
|
+
y: r * Math.sin(theta)
|
|
31144
32303
|
};
|
|
31145
|
-
const
|
|
31146
|
-
|
|
32304
|
+
const toParent = translationFromOrigin(local);
|
|
32305
|
+
const childFrame = composeIsometry(frame, toParent);
|
|
32306
|
+
map.set(ids[index], {
|
|
31147
32307
|
node: child,
|
|
32308
|
+
id: ids[index],
|
|
31148
32309
|
depth: depth + 1,
|
|
31149
32310
|
parentKey: node.item.key,
|
|
31150
|
-
|
|
32311
|
+
parentId: id,
|
|
32312
|
+
position: applyIsometry(childFrame, origin),
|
|
32313
|
+
toParent,
|
|
31151
32314
|
angularShare: share
|
|
31152
32315
|
});
|
|
31153
|
-
const
|
|
31154
|
-
|
|
32316
|
+
const halfWedge = Math.min(share, Math.PI) / 2;
|
|
32317
|
+
const localStart = boundaryAngleInLocalFrame(bisector - halfWedge, local);
|
|
32318
|
+
let localWidth = boundaryAngleInLocalFrame(bisector + halfWedge, local) - localStart;
|
|
31155
32319
|
if (localWidth <= 0) localWidth += 2 * Math.PI;
|
|
31156
|
-
place(child,
|
|
32320
|
+
place(child, ids[index], childFrame, localStart, localStart + localWidth, depth + 1);
|
|
31157
32321
|
});
|
|
31158
32322
|
};
|
|
31159
|
-
place(root,
|
|
32323
|
+
place(root, root.item.key, identityIsometry, -Math.PI, Math.PI, 0);
|
|
31160
32324
|
return map;
|
|
31161
32325
|
}
|
|
31162
|
-
/**
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
|
|
31167
|
-
|
|
31168
|
-
|
|
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++) {
|
|
31169
32342
|
const next = [];
|
|
31170
|
-
for (const
|
|
31171
|
-
const entry =
|
|
31172
|
-
|
|
31173
|
-
const
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31178
|
-
|
|
31179
|
-
|
|
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)));
|
|
31180
32356
|
}
|
|
31181
32357
|
frontier = next;
|
|
31182
32358
|
}
|
|
31183
|
-
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;
|
|
31184
32392
|
}
|
|
31185
32393
|
/**
|
|
31186
32394
|
* Compute the visible nodes/edges of a hyperbolic tree view: every
|
|
@@ -31188,58 +32396,69 @@ function ringDistances(canonical, focusedKey, maxRadius) {
|
|
|
31188
32396
|
* node, projected onto the Poincaré disk with the focus at its center.
|
|
31189
32397
|
*
|
|
31190
32398
|
* @param root True tree root. `undefined` yields an empty layout.
|
|
31191
|
-
* @param options `focusedKey` (
|
|
31192
|
-
* to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
31193
|
-
* cap on
|
|
31194
|
-
* (
|
|
31195
|
-
*
|
|
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).
|
|
31196
32406
|
*/
|
|
31197
32407
|
function computeHyperbolicLayout(root, options = {}) {
|
|
31198
32408
|
if (!root) return {
|
|
31199
32409
|
nodes: [],
|
|
31200
32410
|
edges: []
|
|
31201
32411
|
};
|
|
31202
|
-
const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1 } = options;
|
|
31203
|
-
const canonical = computeCanonicalPositions(root, maxDepth, curvature);
|
|
31204
|
-
const
|
|
31205
|
-
|
|
31206
|
-
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 {
|
|
31207
32416
|
nodes: [],
|
|
31208
32417
|
edges: []
|
|
31209
32418
|
};
|
|
31210
|
-
const
|
|
31211
|
-
|
|
31212
|
-
|
|
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`);
|
|
31213
32425
|
const minWeight = rawWeights.length > 0 ? Math.min(...rawWeights) : void 0;
|
|
31214
32426
|
const maxWeight = rawWeights.length > 0 ? Math.max(...rawWeights) : void 0;
|
|
31215
32427
|
const normalizeWeight = (weight) => {
|
|
31216
32428
|
if (weight === void 0 || minWeight === void 0 || maxWeight === void 0 || maxWeight === minWeight) return .5;
|
|
31217
32429
|
return (weight - minWeight) / (maxWeight - minWeight);
|
|
31218
32430
|
};
|
|
31219
|
-
const nodes = [...
|
|
31220
|
-
const entry = canonical.get(
|
|
32431
|
+
const nodes = [...frames.entries()].map(([id, { ring, frame }]) => {
|
|
32432
|
+
const entry = canonical.get(id);
|
|
31221
32433
|
const weight = entry.node.item.weight;
|
|
31222
32434
|
return {
|
|
31223
32435
|
node: entry.node,
|
|
31224
|
-
key,
|
|
32436
|
+
key: entry.node.item.key,
|
|
32437
|
+
id,
|
|
31225
32438
|
depth: entry.depth,
|
|
31226
|
-
ring
|
|
32439
|
+
ring,
|
|
31227
32440
|
parentKey: entry.parentKey,
|
|
31228
|
-
|
|
32441
|
+
parentId: entry.parentId,
|
|
32442
|
+
position: applyIsometry(composeIsometry(view, frame), {
|
|
32443
|
+
x: 0,
|
|
32444
|
+
y: 0
|
|
32445
|
+
}),
|
|
31229
32446
|
angularShare: entry.angularShare,
|
|
31230
32447
|
weight,
|
|
31231
32448
|
weightNormalized: normalizeWeight(weight)
|
|
31232
32449
|
};
|
|
31233
32450
|
}).sort((a, b) => a.ring - b.ring);
|
|
31234
|
-
const
|
|
32451
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
31235
32452
|
const edges = [];
|
|
31236
32453
|
for (const n of nodes) {
|
|
31237
|
-
if (n.
|
|
31238
|
-
const parent =
|
|
32454
|
+
if (n.parentId === void 0) continue;
|
|
32455
|
+
const parent = byId.get(n.parentId);
|
|
31239
32456
|
if (!parent) continue;
|
|
31240
32457
|
edges.push({
|
|
31241
32458
|
fromKey: parent.key,
|
|
31242
32459
|
toKey: n.key,
|
|
32460
|
+
fromId: parent.id,
|
|
32461
|
+
toId: n.id,
|
|
31243
32462
|
from: parent.position,
|
|
31244
32463
|
to: n.position,
|
|
31245
32464
|
weightNormalized: (parent.weightNormalized + n.weightNormalized) / 2
|
|
@@ -31251,17 +32470,476 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
31251
32470
|
};
|
|
31252
32471
|
}
|
|
31253
32472
|
//#endregion
|
|
31254
|
-
//#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
|
+
}
|
|
31255
32485
|
/**
|
|
31256
|
-
*
|
|
31257
|
-
*
|
|
31258
|
-
*
|
|
31259
|
-
*
|
|
31260
|
-
*
|
|
31261
|
-
*
|
|
31262
|
-
* system (viewBox radius {@link RENDER_SCALE} instead of `1`) avoids it.
|
|
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)
|
|
31263
32492
|
*/
|
|
31264
|
-
|
|
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
|
+
}
|
|
32932
|
+
/**
|
|
32933
|
+
* Paint order: far-from-focus first so nearer (larger) nodes sit on top;
|
|
32934
|
+
* then labelled over unlabelled; the hovered node above everything.
|
|
32935
|
+
*/
|
|
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
|
|
31265
32943
|
/**
|
|
31266
32944
|
* Sanitizes a node key into a token safe to use inside a `part` attribute /
|
|
31267
32945
|
* `::part()` selector — arbitrary key characters (spaces, quotes, etc.)
|
|
@@ -31270,13 +32948,121 @@ const RENDER_SCALE = 500;
|
|
|
31270
32948
|
function partToken(key) {
|
|
31271
32949
|
return key.replace(/[^\w-]/g, `-`);
|
|
31272
32950
|
}
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
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
|
+
`;
|
|
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
|
+
/>`;
|
|
31279
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
|
|
31280
33066
|
let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitElement {
|
|
31281
33067
|
constructor(..._args) {
|
|
31282
33068
|
super(..._args);
|
|
@@ -31284,37 +33070,63 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31284
33070
|
this.selectionFilter = `all`;
|
|
31285
33071
|
this.interactionMode = `standard`;
|
|
31286
33072
|
this.maxRadius = 4;
|
|
33073
|
+
this.nodeShape = `dot`;
|
|
33074
|
+
this.boxMaxChars = 24;
|
|
33075
|
+
this.minLabelSize = 9;
|
|
33076
|
+
this.colorByBranch = false;
|
|
31287
33077
|
this.nodeRadius = .05;
|
|
31288
33078
|
this.minNodeRadius = .015;
|
|
31289
33079
|
this.minWeightScale = .6;
|
|
31290
33080
|
this.maxWeightScale = 1.6;
|
|
31291
33081
|
this.zoom = 1;
|
|
33082
|
+
this.disableZoomGestures = false;
|
|
33083
|
+
this.disableRadialLabels = false;
|
|
33084
|
+
this.disableChildPreview = false;
|
|
31292
33085
|
this.curvature = 1;
|
|
33086
|
+
this.siblingSpacing = DEFAULT_SIBLING_SPACING;
|
|
31293
33087
|
this.focusAnimationDuration = 400;
|
|
31294
33088
|
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
31295
33089
|
this.disableKeyboardNavigation = false;
|
|
31296
33090
|
this.registry = void 0;
|
|
31297
|
-
this.
|
|
31298
|
-
this.
|
|
31299
|
-
this.
|
|
33091
|
+
this._focusId = void 0;
|
|
33092
|
+
this._viewOverride = void 0;
|
|
33093
|
+
this._focusRotation = 0;
|
|
33094
|
+
this._zoomOffset = {
|
|
31300
33095
|
x: 0,
|
|
31301
33096
|
y: 0
|
|
31302
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();
|
|
31303
33114
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
31304
33115
|
labelSelector: `.hyperbolictree-label`,
|
|
31305
33116
|
highlightKey: `incr-search-highlight`
|
|
31306
33117
|
}) });
|
|
31307
33118
|
this.tickled = new TickledController(this, {
|
|
31308
33119
|
getItems: () => this.#layout().nodes.map((n) => ({
|
|
31309
|
-
id: n.
|
|
33120
|
+
id: n.id,
|
|
31310
33121
|
enabled: true
|
|
31311
33122
|
})),
|
|
31312
33123
|
onStateChange: () => {
|
|
31313
33124
|
this.setAttribute(`data-input-mode`, this.tickled.inputMode);
|
|
31314
33125
|
},
|
|
31315
33126
|
onTickle: (id) => {
|
|
33127
|
+
this.#preview.update(id);
|
|
31316
33128
|
if (id === void 0) return;
|
|
31317
|
-
const n = this.#layout().nodes.find((n) => n.
|
|
33129
|
+
const n = this.#layout().nodes.find((n) => n.id === id);
|
|
31318
33130
|
if (!n) return;
|
|
31319
33131
|
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
31320
33132
|
detail: {
|
|
@@ -31326,8 +33138,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31326
33138
|
}));
|
|
31327
33139
|
}
|
|
31328
33140
|
});
|
|
31329
|
-
this
|
|
31330
|
-
|
|
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
|
+
});
|
|
31331
33165
|
}
|
|
31332
33166
|
static {
|
|
31333
33167
|
this.shadowRootOptions = {
|
|
@@ -31340,37 +33174,55 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31340
33174
|
}
|
|
31341
33175
|
/** The node currently centered on the disk. Pure view-state — `root` is never mutated by focus changes. */
|
|
31342
33176
|
get focusedNode() {
|
|
31343
|
-
if (this.
|
|
31344
|
-
return
|
|
33177
|
+
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
33178
|
+
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
31345
33179
|
}
|
|
33180
|
+
#preview;
|
|
33181
|
+
#lazy;
|
|
33182
|
+
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
33183
|
+
#lastLabelIds;
|
|
31346
33184
|
#modelUnsubscribe;
|
|
31347
|
-
#pendingKeys;
|
|
31348
|
-
#aborters;
|
|
31349
33185
|
#ownedRegistry;
|
|
31350
33186
|
#unregisterSearchCommands;
|
|
31351
33187
|
#animFrame;
|
|
31352
|
-
#
|
|
33188
|
+
#resizeObserver;
|
|
33189
|
+
#gestures;
|
|
31353
33190
|
get #effectiveRoot() {
|
|
31354
33191
|
return this.model?.root ?? this.root;
|
|
31355
33192
|
}
|
|
31356
33193
|
#layout() {
|
|
31357
33194
|
return computeHyperbolicLayout(this.#effectiveRoot, {
|
|
31358
|
-
focusedKey: this.
|
|
33195
|
+
focusedKey: this._focusId,
|
|
31359
33196
|
maxRadius: this.maxRadius,
|
|
31360
33197
|
curvature: this.curvature,
|
|
31361
|
-
|
|
33198
|
+
siblingSpacing: this.siblingSpacing,
|
|
33199
|
+
view: this.#currentView()
|
|
31362
33200
|
});
|
|
31363
33201
|
}
|
|
31364
|
-
#
|
|
31365
|
-
|
|
31366
|
-
|
|
31367
|
-
|
|
31368
|
-
|
|
31369
|
-
|
|
31370
|
-
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
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;
|
|
31374
33226
|
}
|
|
31375
33227
|
connectedCallback() {
|
|
31376
33228
|
super.connectedCallback();
|
|
@@ -31390,14 +33242,25 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31390
33242
|
});
|
|
31391
33243
|
this.registry = registry;
|
|
31392
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
|
+
}
|
|
31393
33255
|
}
|
|
31394
33256
|
disconnectedCallback() {
|
|
31395
33257
|
super.disconnectedCallback();
|
|
33258
|
+
this.#resizeObserver?.disconnect();
|
|
33259
|
+
this.#resizeObserver = void 0;
|
|
31396
33260
|
this.#modelUnsubscribe?.();
|
|
31397
33261
|
this.#modelUnsubscribe = void 0;
|
|
31398
|
-
|
|
31399
|
-
this.#
|
|
31400
|
-
this.#pendingKeys.clear();
|
|
33262
|
+
this.#preview.dispose();
|
|
33263
|
+
this.#lazy.abortAll();
|
|
31401
33264
|
if (this.#animFrame !== void 0) {
|
|
31402
33265
|
cancelAnimationFrame(this.#animFrame);
|
|
31403
33266
|
this.#animFrame = void 0;
|
|
@@ -31438,58 +33301,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31438
33301
|
}
|
|
31439
33302
|
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
31440
33303
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
31441
|
-
if (this.
|
|
31442
|
-
this.
|
|
31443
|
-
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;
|
|
31444
33308
|
}
|
|
31445
|
-
this.#
|
|
33309
|
+
this.#lazy.ensure(this.#layout().nodes);
|
|
31446
33310
|
}
|
|
31447
33311
|
#resetViewState() {
|
|
31448
33312
|
this.#clearSelectionInternal();
|
|
31449
33313
|
this.tickled.clearTickled();
|
|
31450
|
-
this.
|
|
31451
|
-
this.
|
|
31452
|
-
this.
|
|
33314
|
+
this._focusId = void 0;
|
|
33315
|
+
this._viewOverride = void 0;
|
|
33316
|
+
this._focusRotation = 0;
|
|
33317
|
+
this._zoomOffset = {
|
|
31453
33318
|
x: 0,
|
|
31454
33319
|
y: 0
|
|
31455
33320
|
};
|
|
33321
|
+
this._previewId = void 0;
|
|
33322
|
+
this.#gestures.cancel();
|
|
31456
33323
|
if (this.#animFrame !== void 0) {
|
|
31457
33324
|
cancelAnimationFrame(this.#animFrame);
|
|
31458
33325
|
this.#animFrame = void 0;
|
|
31459
33326
|
}
|
|
31460
33327
|
}
|
|
31461
|
-
#ensureVisibleChildren() {
|
|
31462
|
-
if (!this.model && !this.loadChildren) return;
|
|
31463
|
-
for (const n of this.#layout().nodes) {
|
|
31464
|
-
const { node } = n;
|
|
31465
|
-
if (node.children !== void 0 || this.#pendingKeys.has(node.item.key)) continue;
|
|
31466
|
-
this.#loadChildren(node, n.depth);
|
|
31467
|
-
}
|
|
31468
|
-
}
|
|
31469
|
-
async #loadChildren(node, depth) {
|
|
31470
|
-
const key = node.item.key;
|
|
31471
|
-
this.#pendingKeys.add(key);
|
|
31472
|
-
const controller = new AbortController();
|
|
31473
|
-
this.#aborters.set(key, controller);
|
|
31474
|
-
this.requestUpdate();
|
|
31475
|
-
try {
|
|
31476
|
-
let children;
|
|
31477
|
-
if (this.model) children = await this.model.getChildren(node, depth, controller.signal);
|
|
31478
|
-
else children = [...await collectDataResult(this.loadChildren({
|
|
31479
|
-
node,
|
|
31480
|
-
depth
|
|
31481
|
-
}, controller.signal), controller.signal)];
|
|
31482
|
-
if (controller.signal.aborted) return;
|
|
31483
|
-
node.children = children;
|
|
31484
|
-
} catch {
|
|
31485
|
-
if (controller.signal.aborted) return;
|
|
31486
|
-
node.children = [];
|
|
31487
|
-
} finally {
|
|
31488
|
-
this.#pendingKeys.delete(key);
|
|
31489
|
-
this.#aborters.delete(key);
|
|
31490
|
-
if (!controller.signal.aborted) this.requestUpdate();
|
|
31491
|
-
}
|
|
31492
|
-
}
|
|
31493
33328
|
#canSelect(node) {
|
|
31494
33329
|
if (this.selectionFilter === `none`) return false;
|
|
31495
33330
|
if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
|
|
@@ -31565,83 +33400,83 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31565
33400
|
* the previous one, for interop with generic `TreeComponent` listeners.
|
|
31566
33401
|
*/
|
|
31567
33402
|
focusOn(node) {
|
|
31568
|
-
const
|
|
31569
|
-
const
|
|
31570
|
-
|
|
31571
|
-
|
|
31572
|
-
const
|
|
31573
|
-
const
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
y: 0
|
|
31580
|
-
};
|
|
31581
|
-
this._focusKey = targetKey;
|
|
31582
|
-
this._panOffset = {
|
|
31583
|
-
x: 0,
|
|
31584
|
-
y: 0
|
|
31585
|
-
};
|
|
31586
|
-
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);
|
|
31587
33414
|
this.dispatchEvent(new CustomEvent(`focus`, {
|
|
31588
33415
|
detail: {
|
|
31589
|
-
node,
|
|
33416
|
+
node: targetNode,
|
|
31590
33417
|
previous: previousNode
|
|
31591
33418
|
},
|
|
31592
33419
|
bubbles: true,
|
|
31593
33420
|
composed: true
|
|
31594
33421
|
}));
|
|
31595
|
-
if (previousNode) {
|
|
31596
|
-
if (
|
|
31597
|
-
|
|
31598
|
-
|
|
31599
|
-
|
|
31600
|
-
|
|
31601
|
-
|
|
31602
|
-
|
|
31603
|
-
|
|
31604
|
-
|
|
31605
|
-
|
|
31606
|
-
|
|
31607
|
-
|
|
31608
|
-
|
|
31609
|
-
|
|
31610
|
-
|
|
31611
|
-
|
|
31612
|
-
},
|
|
31613
|
-
bubbles: true,
|
|
31614
|
-
composed: true
|
|
31615
|
-
}));
|
|
31616
|
-
}
|
|
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
|
+
}));
|
|
31617
33439
|
}
|
|
31618
33440
|
this.requestUpdate();
|
|
31619
33441
|
}
|
|
31620
|
-
#
|
|
31621
|
-
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
31622
|
-
}
|
|
31623
|
-
#animateTo(from, to) {
|
|
33442
|
+
#cancelAnimation() {
|
|
31624
33443
|
if (this.#animFrame !== void 0) {
|
|
31625
33444
|
cancelAnimationFrame(this.#animFrame);
|
|
31626
33445
|
this.#animFrame = void 0;
|
|
31627
33446
|
}
|
|
31628
|
-
|
|
31629
|
-
|
|
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
|
+
};
|
|
31630
33460
|
return;
|
|
31631
33461
|
}
|
|
31632
33462
|
const start = performance.now();
|
|
31633
33463
|
const duration = this.focusAnimationDuration;
|
|
31634
33464
|
const step = (now) => {
|
|
31635
33465
|
const t = Math.min(1, (now - start) / duration);
|
|
31636
|
-
|
|
31637
|
-
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);
|
|
31638
33472
|
this.#animFrame = t < 1 ? requestAnimationFrame(step) : void 0;
|
|
31639
33473
|
};
|
|
31640
33474
|
this.#animFrame = requestAnimationFrame(step);
|
|
31641
33475
|
}
|
|
31642
33476
|
#focusAndMaybeSelect(node) {
|
|
31643
33477
|
this.focusOn(node);
|
|
31644
|
-
this
|
|
33478
|
+
const id = this.#idOf(node);
|
|
33479
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
31645
33480
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
31646
33481
|
}
|
|
31647
33482
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -31656,7 +33491,8 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31656
33491
|
}
|
|
31657
33492
|
this.focusOn(current);
|
|
31658
33493
|
this.select(current);
|
|
31659
|
-
this
|
|
33494
|
+
const id = this.#idOf(current);
|
|
33495
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
31660
33496
|
this.requestUpdate();
|
|
31661
33497
|
return true;
|
|
31662
33498
|
}
|
|
@@ -31664,7 +33500,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31664
33500
|
navigateToNode(predicate) {
|
|
31665
33501
|
const root = this.#effectiveRoot;
|
|
31666
33502
|
if (!root?.children) return false;
|
|
31667
|
-
const keyPath =
|
|
33503
|
+
const keyPath = findHyperbolicKeyPath(root.children, predicate);
|
|
31668
33504
|
if (!keyPath) return false;
|
|
31669
33505
|
return this.navigateTo(keyPath);
|
|
31670
33506
|
}
|
|
@@ -31675,22 +33511,13 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31675
33511
|
*/
|
|
31676
33512
|
revealKey(key) {
|
|
31677
33513
|
const node = findHyperbolicNode(this.#effectiveRoot, key);
|
|
31678
|
-
|
|
31679
|
-
if (!
|
|
31680
|
-
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);
|
|
31681
33518
|
this.requestUpdate();
|
|
31682
33519
|
return true;
|
|
31683
33520
|
}
|
|
31684
|
-
#findKeyPath(nodes, predicate, prefix = []) {
|
|
31685
|
-
for (const node of nodes) {
|
|
31686
|
-
const path = [...prefix, node.item.key];
|
|
31687
|
-
if (predicate(node)) return path;
|
|
31688
|
-
if (node.children?.length) {
|
|
31689
|
-
const hit = this.#findKeyPath(node.children, predicate, path);
|
|
31690
|
-
if (hit) return hit;
|
|
31691
|
-
}
|
|
31692
|
-
}
|
|
31693
|
-
}
|
|
31694
33521
|
/**
|
|
31695
33522
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
31696
33523
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -31727,7 +33554,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31727
33554
|
this.requestUpdate();
|
|
31728
33555
|
}
|
|
31729
33556
|
} else if (this.interactionMode !== `manual`) this.select(n.node);
|
|
31730
|
-
this.tickled.setTickled(n.
|
|
33557
|
+
this.tickled.setTickled(n.id);
|
|
31731
33558
|
this.focusOn(n.node);
|
|
31732
33559
|
}
|
|
31733
33560
|
#handleItemDblClick(n) {
|
|
@@ -31749,239 +33576,147 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31749
33576
|
}));
|
|
31750
33577
|
}
|
|
31751
33578
|
#handleKeyDown(event) {
|
|
31752
|
-
if (this.disableKeyboardNavigation) return;
|
|
31753
33579
|
const root = this.#effectiveRoot;
|
|
31754
|
-
|
|
31755
|
-
if (
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
|
|
31763
|
-
|
|
31764
|
-
|
|
31765
|
-
|
|
31766
|
-
|
|
31767
|
-
|
|
31768
|
-
|
|
31769
|
-
|
|
31770
|
-
|
|
31771
|
-
|
|
31772
|
-
|
|
31773
|
-
}
|
|
31774
|
-
case `ArrowLeft`:
|
|
31775
|
-
case `ArrowRight`: {
|
|
31776
|
-
event.preventDefault();
|
|
31777
|
-
const path = findHyperbolicPath(root, focusNode.item.key);
|
|
31778
|
-
const siblings = (path && path.length >= 2 ? path[path.length - 2] : void 0)?.children ?? (focusNode === root ? [focusNode] : []);
|
|
31779
|
-
const index = siblings.findIndex((n) => n.item.key === focusNode.item.key);
|
|
31780
|
-
if (index === -1 || siblings.length <= 1) return;
|
|
31781
|
-
const next = siblings[(index + (event.key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length];
|
|
31782
|
-
this.#focusAndMaybeSelect(next);
|
|
31783
|
-
return;
|
|
31784
|
-
}
|
|
31785
|
-
case `Enter`: {
|
|
31786
|
-
event.preventDefault();
|
|
31787
|
-
if (this.interactionMode !== `manual`) this.select(focusNode);
|
|
31788
|
-
const depth = (findHyperbolicPath(root, focusNode.item.key)?.length ?? 1) - 1;
|
|
31789
|
-
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
31790
|
-
detail: {
|
|
31791
|
-
node: focusNode,
|
|
31792
|
-
depth
|
|
31793
|
-
},
|
|
31794
|
-
bubbles: true,
|
|
31795
|
-
composed: true
|
|
31796
|
-
}));
|
|
31797
|
-
return;
|
|
31798
|
-
}
|
|
31799
|
-
case `Escape`: if (this._focusKey !== void 0 && this._focusKey !== root.item.key) {
|
|
31800
|
-
event.preventDefault();
|
|
31801
|
-
this.focusOn(root);
|
|
31802
|
-
}
|
|
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
|
+
}));
|
|
31803
33599
|
}
|
|
31804
33600
|
}
|
|
31805
|
-
#
|
|
31806
|
-
|
|
31807
|
-
|
|
31808
|
-
|
|
31809
|
-
|
|
31810
|
-
if (!this.#isBackgroundTarget(event)) return;
|
|
31811
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
31812
|
-
this.#panStart = {
|
|
31813
|
-
pointerId: event.pointerId,
|
|
31814
|
-
startClient: {
|
|
31815
|
-
x: event.clientX,
|
|
31816
|
-
y: event.clientY
|
|
31817
|
-
},
|
|
31818
|
-
baseOffset: this._panOffset
|
|
31819
|
-
};
|
|
31820
|
-
}
|
|
31821
|
-
#handleBackgroundPointerMove(event) {
|
|
31822
|
-
if (!this.#panStart || event.pointerId !== this.#panStart.pointerId) return;
|
|
31823
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
31824
|
-
const scale = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
31825
|
-
const dx = (event.clientX - this.#panStart.startClient.x) * scale;
|
|
31826
|
-
const dy = (event.clientY - this.#panStart.startClient.y) * scale;
|
|
31827
|
-
this._panOffset = clampToDisk({
|
|
31828
|
-
x: this.#panStart.baseOffset.x - dx,
|
|
31829
|
-
y: this.#panStart.baseOffset.y - dy
|
|
31830
|
-
}, .9);
|
|
31831
|
-
this.requestUpdate();
|
|
33601
|
+
#zoomState() {
|
|
33602
|
+
return clampZoomState({
|
|
33603
|
+
zoom: this.zoom,
|
|
33604
|
+
offset: this._zoomOffset
|
|
33605
|
+
});
|
|
31832
33606
|
}
|
|
31833
|
-
|
|
31834
|
-
|
|
31835
|
-
|
|
31836
|
-
|
|
31837
|
-
|
|
31838
|
-
|
|
31839
|
-
|
|
31840
|
-
|
|
31841
|
-
d += `${i === 0 ? `M` : `L`}${(p.x * RENDER_SCALE).toFixed(2)},${(p.y * RENDER_SCALE).toFixed(2)} `;
|
|
31842
|
-
}
|
|
31843
|
-
return svg`<path
|
|
31844
|
-
class="edge"
|
|
31845
|
-
d=${d}
|
|
31846
|
-
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
31847
|
-
data-from=${edge.fromKey}
|
|
31848
|
-
data-to=${edge.toKey}
|
|
31849
|
-
style="--edge-weight: ${edge.weightNormalized}"
|
|
31850
|
-
/>`;
|
|
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 });
|
|
31851
33615
|
}
|
|
31852
|
-
#
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
31856
|
-
|
|
31857
|
-
|
|
31858
|
-
|
|
31859
|
-
|
|
31860
|
-
|
|
31861
|
-
|
|
31862
|
-
|
|
31863
|
-
}
|
|
31864
|
-
|
|
31865
|
-
|
|
31866
|
-
|
|
31867
|
-
|
|
31868
|
-
|
|
31869
|
-
|
|
31870
|
-
|
|
31871
|
-
|
|
31872
|
-
|
|
31873
|
-
|
|
31874
|
-
|
|
31875
|
-
|
|
31876
|
-
|
|
31877
|
-
|
|
31878
|
-
|
|
31879
|
-
|
|
31880
|
-
|
|
31881
|
-
|
|
31882
|
-
|
|
31883
|
-
|
|
31884
|
-
|
|
31885
|
-
|
|
31886
|
-
|
|
31887
|
-
|
|
31888
|
-
|
|
31889
|
-
|
|
31890
|
-
* first, then larger on-screen radius) and a label is kept only if its
|
|
31891
|
-
* position isn't too close to an already-kept one.
|
|
31892
|
-
*/
|
|
31893
|
-
#computeLabelKeys(nodes) {
|
|
31894
|
-
const minSpacing = .07;
|
|
31895
|
-
const prioritised = [...nodes.filter((n) => this.#nodeRenderRadius(n) >= this.minNodeRadius)].sort((a, b) => {
|
|
31896
|
-
if (a.ring !== b.ring) return a.ring - b.ring;
|
|
31897
|
-
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)
|
|
31898
33654
|
});
|
|
31899
|
-
const kept = [];
|
|
31900
|
-
const keys = /* @__PURE__ */ new Set();
|
|
31901
|
-
for (const n of prioritised) if (!kept.some((k) => Math.hypot(k.position.x - n.position.x, k.position.y - n.position.y) < minSpacing)) {
|
|
31902
|
-
kept.push(n);
|
|
31903
|
-
keys.add(n.key);
|
|
31904
|
-
}
|
|
31905
|
-
return keys;
|
|
31906
33655
|
}
|
|
31907
|
-
#
|
|
31908
|
-
|
|
31909
|
-
|
|
31910
|
-
|
|
31911
|
-
|
|
31912
|
-
|
|
31913
|
-
|
|
31914
|
-
|
|
31915
|
-
|
|
31916
|
-
|
|
31917
|
-
|
|
31918
|
-
|
|
31919
|
-
|
|
31920
|
-
|
|
31921
|
-
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
|
|
31927
|
-
|
|
31928
|
-
|
|
31929
|
-
|
|
31930
|
-
aria-label=${node.item.label}
|
|
31931
|
-
part="node node-${partToken(key)}"
|
|
31932
|
-
style=${weightStyle}
|
|
31933
|
-
transform="translate(${position.x * RENDER_SCALE} ${position.y * RENDER_SCALE})"
|
|
31934
|
-
@pointerdown=${(e) => e.stopPropagation()}
|
|
31935
|
-
@click=${(e) => {
|
|
31936
|
-
e.stopPropagation();
|
|
31937
|
-
this.#handleItemClick(n, e);
|
|
31938
|
-
}}
|
|
31939
|
-
@dblclick=${(e) => {
|
|
31940
|
-
e.stopPropagation();
|
|
31941
|
-
this.#handleItemDblClick(n);
|
|
31942
|
-
}}
|
|
31943
|
-
@pointerenter=${() => this.tickled.handlePointerEnter(key)}
|
|
31944
|
-
>
|
|
31945
|
-
<circle class="node-hit" r=${Math.max(scaledRadius, .035 * RENDER_SCALE)} part="hit" />
|
|
31946
|
-
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
31947
|
-
${showLabel ? svg`<text class="hyperbolictree-label" part="label" x=${scaledRadius + .01 * RENDER_SCALE} y="0" dominant-baseline="middle">${node.item.label}</text>` : nothing}
|
|
31948
|
-
</g>
|
|
31949
|
-
`;
|
|
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
|
+
});
|
|
31950
33679
|
}
|
|
31951
33680
|
render() {
|
|
31952
33681
|
const { nodes, edges } = this.#layout();
|
|
31953
|
-
const
|
|
31954
|
-
const
|
|
31955
|
-
|
|
31956
|
-
|
|
31957
|
-
|
|
31958
|
-
|
|
31959
|
-
}
|
|
31960
|
-
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}) `;
|
|
31961
33688
|
return html`
|
|
31962
33689
|
<svg
|
|
31963
33690
|
class="hyperbolictree"
|
|
31964
|
-
viewBox="${-
|
|
33691
|
+
viewBox="${-525} ${-525} ${1050} ${1050}"
|
|
31965
33692
|
preserveAspectRatio="xMidYMid meet"
|
|
31966
33693
|
role="tree"
|
|
31967
33694
|
tabindex="0"
|
|
31968
33695
|
data-input-mode=${this.tickled.inputMode}
|
|
33696
|
+
data-node-shape=${this.nodeShape}
|
|
31969
33697
|
@keydown=${(e) => this.#handleKeyDown(e)}
|
|
31970
|
-
@pointerdown=${(e) => this.#
|
|
31971
|
-
@pointermove=${(e) => this.#
|
|
31972
|
-
@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)}
|
|
31973
33703
|
@pointerleave=${() => this.tickled.clearTickled()}
|
|
31974
33704
|
>
|
|
31975
|
-
<g class="zoom" transform="scale(${
|
|
31976
|
-
<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} />
|
|
31977
33707
|
<g class="edges" part="edges">
|
|
31978
|
-
${
|
|
33708
|
+
${renderEdges(edges, byId, branches, this.edgeRenderer)}
|
|
31979
33709
|
</g>
|
|
31980
33710
|
<g class="nodes" part="nodes">
|
|
31981
|
-
${
|
|
33711
|
+
${this.#renderNodes(nodes, ctx)}
|
|
31982
33712
|
</g>
|
|
31983
33713
|
</g>
|
|
31984
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
|
+
})}
|
|
31985
33720
|
${this.search.renderField()}
|
|
31986
33721
|
`;
|
|
31987
33722
|
}
|
|
@@ -31989,102 +33724,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31989
33724
|
this.styles = [
|
|
31990
33725
|
themeFallbacks,
|
|
31991
33726
|
listSearchStyles,
|
|
31992
|
-
|
|
31993
|
-
:host {
|
|
31994
|
-
--hyperbolictree-node-color: var(--surface-4);
|
|
31995
|
-
--hyperbolictree-node-text: var(--surface-3-text);
|
|
31996
|
-
--hyperbolictree-edge-color: var(--surface-6);
|
|
31997
|
-
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
31998
|
-
--hyperbolictree-font-size: 25px;
|
|
31999
|
-
|
|
32000
|
-
display: block;
|
|
32001
|
-
overflow: hidden;
|
|
32002
|
-
outline: none;
|
|
32003
|
-
overscroll-behavior: contain;
|
|
32004
|
-
}
|
|
32005
|
-
|
|
32006
|
-
/*
|
|
32007
|
-
* delegatesFocus means the host itself reports as focused whenever
|
|
32008
|
-
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
32009
|
-
* the host also picks up the browser's own default focus ring/fill
|
|
32010
|
-
* sized to the whole box.
|
|
32011
|
-
*/
|
|
32012
|
-
:host(:focus),
|
|
32013
|
-
:host(:focus-visible) {
|
|
32014
|
-
outline: none;
|
|
32015
|
-
}
|
|
32016
|
-
|
|
32017
|
-
.hyperbolictree {
|
|
32018
|
-
display: block;
|
|
32019
|
-
width: 100%;
|
|
32020
|
-
height: 100%;
|
|
32021
|
-
outline: none;
|
|
32022
|
-
touch-action: none;
|
|
32023
|
-
overscroll-behavior: contain;
|
|
32024
|
-
}
|
|
32025
|
-
|
|
32026
|
-
.disk-boundary {
|
|
32027
|
-
fill: none;
|
|
32028
|
-
stroke: var(--hyperbolictree-boundary-color);
|
|
32029
|
-
stroke-width: 1;
|
|
32030
|
-
}
|
|
32031
|
-
|
|
32032
|
-
.edge {
|
|
32033
|
-
fill: none;
|
|
32034
|
-
stroke: var(--hyperbolictree-edge-color);
|
|
32035
|
-
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
32036
|
-
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
32037
|
-
}
|
|
32038
|
-
|
|
32039
|
-
.node {
|
|
32040
|
-
cursor: pointer;
|
|
32041
|
-
}
|
|
32042
|
-
|
|
32043
|
-
.node-hit {
|
|
32044
|
-
fill: transparent;
|
|
32045
|
-
pointer-events: all;
|
|
32046
|
-
}
|
|
32047
|
-
|
|
32048
|
-
.node-dot {
|
|
32049
|
-
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
32050
|
-
fill: color-mix(in oklab, var(--hyperbolictree-node-color), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
32051
|
-
stroke: var(--surface-2);
|
|
32052
|
-
stroke-width: 1;
|
|
32053
|
-
pointer-events: none;
|
|
32054
|
-
}
|
|
32055
|
-
|
|
32056
|
-
.node.tickled .node-dot {
|
|
32057
|
-
fill: var(--item-bg-tickled);
|
|
32058
|
-
}
|
|
32059
|
-
|
|
32060
|
-
.node.selected .node-dot {
|
|
32061
|
-
fill: var(--item-bg-selected);
|
|
32062
|
-
}
|
|
32063
|
-
|
|
32064
|
-
.node.selected.tickled .node-dot {
|
|
32065
|
-
fill: var(--item-bg-selected-tickled);
|
|
32066
|
-
}
|
|
32067
|
-
|
|
32068
|
-
.node.focused .node-dot {
|
|
32069
|
-
stroke: var(--accent);
|
|
32070
|
-
stroke-width: 3;
|
|
32071
|
-
}
|
|
32072
|
-
|
|
32073
|
-
.node.filtered {
|
|
32074
|
-
opacity: 0.3;
|
|
32075
|
-
}
|
|
32076
|
-
|
|
32077
|
-
.node.loading .node-dot {
|
|
32078
|
-
opacity: 0.6;
|
|
32079
|
-
}
|
|
32080
|
-
|
|
32081
|
-
.hyperbolictree-label {
|
|
32082
|
-
font-size: var(--hyperbolictree-font-size);
|
|
32083
|
-
fill: var(--hyperbolictree-node-text);
|
|
32084
|
-
cursor: pointer;
|
|
32085
|
-
user-select: none;
|
|
32086
|
-
}
|
|
32087
|
-
`
|
|
33727
|
+
hyperbolicTreeStyles
|
|
32088
33728
|
];
|
|
32089
33729
|
}
|
|
32090
33730
|
};
|
|
@@ -32107,6 +33747,24 @@ __decorate([property({
|
|
|
32107
33747
|
type: Number,
|
|
32108
33748
|
attribute: `max-radius`
|
|
32109
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);
|
|
32110
33768
|
__decorate([property({
|
|
32111
33769
|
type: Number,
|
|
32112
33770
|
attribute: `node-radius`
|
|
@@ -32124,7 +33782,23 @@ __decorate([property({
|
|
|
32124
33782
|
attribute: `max-weight-scale`
|
|
32125
33783
|
})], HyperbolicTreeListElement.prototype, "maxWeightScale", void 0);
|
|
32126
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);
|
|
32127
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);
|
|
32128
33802
|
__decorate([property({
|
|
32129
33803
|
type: Number,
|
|
32130
33804
|
attribute: `focus-animation-duration`
|
|
@@ -32143,9 +33817,12 @@ __decorate([property({
|
|
|
32143
33817
|
attribute: `search-modes`,
|
|
32144
33818
|
reflect: true
|
|
32145
33819
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
32146
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
32147
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
32148
|
-
__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);
|
|
32149
33826
|
HyperbolicTreeListElement = __decorate([safeCustomElement(`ixfx-hyperbolictree-list`)], HyperbolicTreeListElement);
|
|
32150
33827
|
//#endregion
|
|
32151
33828
|
//#region src/icicle-tree/layout.ts
|
|
@@ -37913,6 +39590,9 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37913
39590
|
const open = this._isOpen(node, depth);
|
|
37914
39591
|
const isBranch = node.item.isLeaf !== true;
|
|
37915
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;
|
|
37916
39596
|
const tooltip = node.item.tooltip;
|
|
37917
39597
|
const isCheckedMode = this.interactionMode === `checked`;
|
|
37918
39598
|
let checkboxTpl = nothing;
|
|
@@ -37940,8 +39620,9 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37940
39620
|
@pointerenter=${() => this._handleItemPointerEnter(node, depth)}
|
|
37941
39621
|
>
|
|
37942
39622
|
${checkboxTpl}
|
|
37943
|
-
${icon ? html`<ixfx-icon class="item-icon" name="${icon}"></ixfx-icon>` : nothing}
|
|
37944
|
-
<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>`}
|
|
37945
39626
|
${tooltip ? html`<ixfx-tooltip class="item-tooltip" anchor="--tooltip-miller-${depth}-${node.item.key}" .value=${tooltip}></ixfx-tooltip>` : nothing}
|
|
37946
39627
|
${isBranch ? html`<span class="caret" aria-hidden="true">›</span>` : nothing}
|
|
37947
39628
|
</li>
|
|
@@ -37995,6 +39676,17 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37995
39676
|
text-overflow: ellipsis;
|
|
37996
39677
|
}
|
|
37997
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
|
+
|
|
37998
39690
|
.item-tooltip {
|
|
37999
39691
|
position: absolute;
|
|
38000
39692
|
}
|
|
@@ -51586,6 +53278,6 @@ function init() {
|
|
|
51586
53278
|
console.log(`Init`);
|
|
51587
53279
|
}
|
|
51588
53280
|
//#endregion
|
|
51589
|
-
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 };
|
|
51590
53282
|
|
|
51591
53283
|
//# sourceMappingURL=index.js.map
|