@ixfx/components 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.d.ts +451 -120
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2267 -540
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.d.ts +1 -1
- package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
- package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/{button-CTL45aBl.d.ts → button-BYuf72aR.d.ts} +2 -2
- package/dist/{button-CTL45aBl.d.ts.map → button-BYuf72aR.d.ts.map} +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/crumbs.d.ts +3 -3
- package/dist/crumbs.js +1 -1
- package/dist/{data-grid-CfiaBqTA.d.ts → data-grid-C6EjCe5O.d.ts} +48 -6
- package/dist/data-grid-C6EjCe5O.d.ts.map +1 -0
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +67 -3
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-DxQZkK-C.d.ts → detail-list-CmdCxddo.d.ts} +22 -5
- package/dist/detail-list-CmdCxddo.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -3
- package/dist/detail-list.js +67 -4
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-k89JpiNy.js → grid-list-3fGhdaFA.js} +92 -7
- package/dist/grid-list-3fGhdaFA.js.map +1 -0
- package/dist/{grid-list-ChgYpUUq.d.ts → grid-list-D6AfWfwn.d.ts} +27 -4
- package/dist/grid-list-D6AfWfwn.d.ts.map +1 -0
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +2 -2
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts.map → grouped-item-lister-DETte0Dh.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
- package/dist/icon-CDGprSdE.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -2
- package/dist/incr-search.d.ts +1 -1
- package/dist/{index-CHbuPyLL.d.ts → index-CMsmZ6Or.d.ts} +4 -4
- package/dist/{index-CHbuPyLL.d.ts.map → index-CMsmZ6Or.d.ts.map} +1 -1
- package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +2 -2
- package/dist/{index-garxxqgV.d.ts.map → index-CYndvTWH.d.ts.map} +1 -1
- package/dist/{index-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-CDllxjVG.d.ts → index-DyTSt9OK.d.ts} +4 -4
- package/dist/{index-CDllxjVG.d.ts.map → index-DyTSt9OK.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 +2038 -646
- 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-BHT6HRND.js → menu-CsBUyUuS.js} +2 -2
- package/dist/{menu-BHT6HRND.js.map → menu-CsBUyUuS.js.map} +1 -1
- package/dist/{menu-item-DlxAb0Xr.d.ts → menu-item-BbgdcwBX.d.ts} +11 -2
- package/dist/menu-item-BbgdcwBX.d.ts.map +1 -0
- package/dist/{menu-item-JcpHCyF0.js → menu-item-CfndBGO8.js} +38 -3
- package/dist/menu-item-CfndBGO8.js.map +1 -0
- package/dist/menu.d.ts +2 -2
- package/dist/menu.js +2 -2
- package/dist/miller.d.ts +3 -3
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +18 -2
- package/dist/miller.js.map +1 -1
- package/dist/panel.d.ts +1 -1
- package/dist/{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/menu.md +30 -9
- 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/menu-item-DlxAb0Xr.d.ts.map +0 -1
- package/dist/menu-item-JcpHCyF0.js.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
|
@@ -7248,6 +7248,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7248
7248
|
this._hasContextTarget = false;
|
|
7249
7249
|
this._anchorName = `--mt-${_MenuTrigger._count++}`;
|
|
7250
7250
|
this._clickListener = this._handleClickOutside.bind(this);
|
|
7251
|
+
this._focusBeforeOpen = null;
|
|
7251
7252
|
this._handlePopoverToggle = (e) => {
|
|
7252
7253
|
if (e.newState === `closed`) {
|
|
7253
7254
|
this._isOpen = false;
|
|
@@ -7264,10 +7265,10 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7264
7265
|
e.preventDefault();
|
|
7265
7266
|
e.stopPropagation();
|
|
7266
7267
|
this._closeMenu();
|
|
7267
|
-
this.
|
|
7268
|
+
this._restoreFocus();
|
|
7268
7269
|
} else if (e.key === `Tab`) {
|
|
7269
7270
|
this._closeMenu();
|
|
7270
|
-
this.
|
|
7271
|
+
this._restoreFocus();
|
|
7271
7272
|
}
|
|
7272
7273
|
};
|
|
7273
7274
|
}
|
|
@@ -7305,6 +7306,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7305
7306
|
}
|
|
7306
7307
|
updated(changedProperties) {
|
|
7307
7308
|
if (changedProperties.has(`registry`) || changedProperties.has(`keyboardManager`)) this._propagateContext();
|
|
7309
|
+
if (changedProperties.has(`mode`) || changedProperties.has(`_hasContextTarget`)) this.toggleAttribute(`has-context-target`, this.mode === `context` && this._hasContextTarget);
|
|
7308
7310
|
if (changedProperties.has(`mode`)) this._updatePosition();
|
|
7309
7311
|
}
|
|
7310
7312
|
_updatePosition() {
|
|
@@ -7370,6 +7372,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7370
7372
|
}
|
|
7371
7373
|
_openMenu() {
|
|
7372
7374
|
if (this._isOpen || !this._popup?.popover) return;
|
|
7375
|
+
this._focusBeforeOpen = deepActiveElement();
|
|
7373
7376
|
try {
|
|
7374
7377
|
this._popup.showPopover({ source: this });
|
|
7375
7378
|
this._isOpen = true;
|
|
@@ -7411,6 +7414,22 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7411
7414
|
_getMenuContainer() {
|
|
7412
7415
|
return ((this.shadowRoot?.querySelector(`slot[name="menu"]`))?.assignedElements({ flatten: true }) ?? []).find((el) => el.tagName === `IXFX-MENU-CONTAINER`) ?? null;
|
|
7413
7416
|
}
|
|
7417
|
+
/**
|
|
7418
|
+
* Return focus after a keyboard close. The trigger button is the natural
|
|
7419
|
+
* target, but it is not rendered for a context target — then fall back to
|
|
7420
|
+
* whatever was focused at open (don't rely on the browser's popover focus
|
|
7421
|
+
* restoration), and finally the host.
|
|
7422
|
+
*/
|
|
7423
|
+
_restoreFocus() {
|
|
7424
|
+
if (this._triggerBtn) {
|
|
7425
|
+
this._triggerBtn.focus();
|
|
7426
|
+
return;
|
|
7427
|
+
}
|
|
7428
|
+
const previous = this._focusBeforeOpen;
|
|
7429
|
+
this._focusBeforeOpen = null;
|
|
7430
|
+
if (previous?.isConnected && previous !== document.body) previous.focus();
|
|
7431
|
+
else this.focus();
|
|
7432
|
+
}
|
|
7414
7433
|
_handleCommand(e) {
|
|
7415
7434
|
if (this._popup?.popover) try {
|
|
7416
7435
|
this._popup.hidePopover();
|
|
@@ -7454,6 +7473,16 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
7454
7473
|
--ixfx-button-padding: var(--ixfx-button-padding, var(--space-s) var(--space-m));
|
|
7455
7474
|
}
|
|
7456
7475
|
|
|
7476
|
+
/* Context mode with a context-target: dissolve the host's box so the
|
|
7477
|
+
slotted surface is itself the item in the consumer's flex/grid layout.
|
|
7478
|
+
Otherwise the host sizes to the surface's full un-scrolled content and
|
|
7479
|
+
a height-constrained parent never clips it. Safe because context opens
|
|
7480
|
+
position the popup at the pointer (fixed), not via the anchor.
|
|
7481
|
+
Override with --menu-trigger-display (e.g. block) to keep a box. */
|
|
7482
|
+
:host([mode="context"][has-context-target]) {
|
|
7483
|
+
display: var(--menu-trigger-display, contents);
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7457
7486
|
.trigger-area {
|
|
7458
7487
|
anchor-name: var(--anchor-name);
|
|
7459
7488
|
/* Block flex (not inline-flex): as an inline-level box this would join
|
|
@@ -7581,6 +7610,12 @@ __decorate([state()], MenuTrigger.prototype, "_hasContextTarget", void 0);
|
|
|
7581
7610
|
__decorate([query(`.trigger-btn`)], MenuTrigger.prototype, "_triggerBtn", void 0);
|
|
7582
7611
|
__decorate([query(`.popup`)], MenuTrigger.prototype, "_popup", void 0);
|
|
7583
7612
|
MenuTrigger = _MenuTrigger = __decorate([safeCustomElement(`ixfx-menu-trigger`)], MenuTrigger);
|
|
7613
|
+
/** `document.activeElement`, followed down through open shadow roots. */
|
|
7614
|
+
function deepActiveElement() {
|
|
7615
|
+
let el = document.activeElement;
|
|
7616
|
+
while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
|
|
7617
|
+
return el instanceof HTMLElement ? el : null;
|
|
7618
|
+
}
|
|
7584
7619
|
//#endregion
|
|
7585
7620
|
//#region src/menu/menu-item.ts
|
|
7586
7621
|
let MenuItem = class MenuItem extends LitElement {
|
|
@@ -12013,6 +12048,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12013
12048
|
this.path = ``;
|
|
12014
12049
|
this.separator = `/`;
|
|
12015
12050
|
this.pathRoot = ``;
|
|
12051
|
+
this.hintPrevious = false;
|
|
12016
12052
|
this._openIdx = null;
|
|
12017
12053
|
this._editing = false;
|
|
12018
12054
|
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
@@ -12024,6 +12060,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12024
12060
|
this._openCumPath = null;
|
|
12025
12061
|
this._dragCumPath = null;
|
|
12026
12062
|
this._dropCumPath = null;
|
|
12063
|
+
this._hintSegments = null;
|
|
12027
12064
|
this.#uid = Math.random().toString(36).slice(2, 8);
|
|
12028
12065
|
this.#clickSuppressUntil = 0;
|
|
12029
12066
|
this.#onPopoverToggle = (e) => {
|
|
@@ -12159,6 +12196,19 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12159
12196
|
#clicksSuppressed() {
|
|
12160
12197
|
return Date.now() < this.#clickSuppressUntil || this._dragCumPath !== null;
|
|
12161
12198
|
}
|
|
12199
|
+
#isSegmentPrefix(prefix, full) {
|
|
12200
|
+
return prefix.length <= full.length && prefix.every((seg, i) => seg === full[i]);
|
|
12201
|
+
}
|
|
12202
|
+
/**
|
|
12203
|
+
* Called when a real segment click shortens the path. Keeps the deepest
|
|
12204
|
+
* known branch: only overwrites `_hintSegments` when the current hint no
|
|
12205
|
+
* longer covers where we're navigating to.
|
|
12206
|
+
*/
|
|
12207
|
+
#recordHintOnShorten(oldSegments, newSegments) {
|
|
12208
|
+
if (!this.hintPrevious) return;
|
|
12209
|
+
const hint = this._hintSegments;
|
|
12210
|
+
if (!(hint !== null && this.#isSegmentPrefix(newSegments, hint))) this._hintSegments = oldSegments;
|
|
12211
|
+
}
|
|
12162
12212
|
#resolveSegmentIcon(fullPathSegments, index) {
|
|
12163
12213
|
const direct = this.segmentIconLookup?.(fullPathSegments, index);
|
|
12164
12214
|
if (typeof direct === `string` && direct.length > 0) return direct;
|
|
@@ -12304,12 +12354,63 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12304
12354
|
composed: true
|
|
12305
12355
|
}));
|
|
12306
12356
|
}
|
|
12357
|
+
/**
|
|
12358
|
+
* The current leaf segment (deepest visible segment), used as the drop target
|
|
12359
|
+
* when a drag is released over blank space rather than a specific `.seg`.
|
|
12360
|
+
*/
|
|
12361
|
+
#leafSegment() {
|
|
12362
|
+
const segments = this.#getSegments();
|
|
12363
|
+
if (segments.length > 0) {
|
|
12364
|
+
const index = segments.length - 1;
|
|
12365
|
+
return {
|
|
12366
|
+
cumPath: this.#getCumulativePath(index, false),
|
|
12367
|
+
fullPathSegments: this.#resolveFullPathSegments(index, false)
|
|
12368
|
+
};
|
|
12369
|
+
}
|
|
12370
|
+
const rootSegments = this.#getRootSegments();
|
|
12371
|
+
if (rootSegments.length > 0) {
|
|
12372
|
+
const index = rootSegments.length - 1;
|
|
12373
|
+
return {
|
|
12374
|
+
cumPath: this.#getCumulativePath(index, true),
|
|
12375
|
+
fullPathSegments: this.#resolveFullPathSegments(index, true)
|
|
12376
|
+
};
|
|
12377
|
+
}
|
|
12378
|
+
}
|
|
12379
|
+
/**
|
|
12380
|
+
* Is `event` already being handled by a `.seg`'s own drag handlers? Virtual
|
|
12381
|
+
* (hint) segments don't have drag handlers of their own, so a drag released
|
|
12382
|
+
* over one falls through to the host-level fallback (the real leaf).
|
|
12383
|
+
*/
|
|
12384
|
+
#eventTargetsSegment(event) {
|
|
12385
|
+
return Boolean(event.target?.closest?.(`.seg:not(.seg-virtual)`));
|
|
12386
|
+
}
|
|
12387
|
+
#onHostDragOver(event) {
|
|
12388
|
+
if (!this.parseExternalDrop || !event.dataTransfer) return;
|
|
12389
|
+
if (this.#eventTargetsSegment(event)) return;
|
|
12390
|
+
const leaf = this.#leafSegment();
|
|
12391
|
+
if (!leaf) return;
|
|
12392
|
+
event.preventDefault();
|
|
12393
|
+
if (this._dropCumPath !== leaf.cumPath) this._dropCumPath = leaf.cumPath;
|
|
12394
|
+
}
|
|
12395
|
+
#onHostDragLeave(event) {
|
|
12396
|
+
if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
|
|
12397
|
+
const leaf = this.#leafSegment();
|
|
12398
|
+
if (leaf && this._dropCumPath === leaf.cumPath) this._dropCumPath = null;
|
|
12399
|
+
}
|
|
12400
|
+
#onHostDrop(event) {
|
|
12401
|
+
if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
|
|
12402
|
+
const leaf = this.#leafSegment();
|
|
12403
|
+
if (!leaf) return;
|
|
12404
|
+
this.#onSegDrop(leaf.fullPathSegments, event);
|
|
12405
|
+
}
|
|
12307
12406
|
#onLabelClick(index, event) {
|
|
12308
12407
|
if (this.#clicksSuppressed()) return;
|
|
12309
12408
|
event.preventDefault();
|
|
12310
12409
|
event.stopPropagation();
|
|
12311
12410
|
this.#closeDropdown();
|
|
12312
|
-
const
|
|
12411
|
+
const segments = this.#getSegments();
|
|
12412
|
+
const truncatedSegments = segments.slice(0, index + 1);
|
|
12413
|
+
this.#recordHintOnShorten(segments, truncatedSegments);
|
|
12313
12414
|
const hadLeading = this.path.startsWith(this.separator);
|
|
12314
12415
|
const newPath = truncatedSegments.join(this.separator);
|
|
12315
12416
|
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
@@ -12327,6 +12428,31 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12327
12428
|
composed: true
|
|
12328
12429
|
}));
|
|
12329
12430
|
}
|
|
12431
|
+
#onVirtualSegmentClick(hintIndex, event) {
|
|
12432
|
+
if (this.#clicksSuppressed()) return;
|
|
12433
|
+
event.preventDefault();
|
|
12434
|
+
event.stopPropagation();
|
|
12435
|
+
this.#closeDropdown();
|
|
12436
|
+
const hint = this._hintSegments;
|
|
12437
|
+
if (!hint) return;
|
|
12438
|
+
const truncatedSegments = hint.slice(0, hintIndex + 1);
|
|
12439
|
+
const hadLeading = this.path.startsWith(this.separator);
|
|
12440
|
+
const newPath = truncatedSegments.join(this.separator);
|
|
12441
|
+
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
12442
|
+
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
12443
|
+
this._leafSegmentIndex = null;
|
|
12444
|
+
const parentPath = truncatedSegments.slice(0, -1).join(this.separator);
|
|
12445
|
+
this.dispatchEvent(new CustomEvent(`navigate`, {
|
|
12446
|
+
detail: {
|
|
12447
|
+
index: hintIndex,
|
|
12448
|
+
label: truncatedSegments[hintIndex],
|
|
12449
|
+
parentPath,
|
|
12450
|
+
fullPath: newPath
|
|
12451
|
+
},
|
|
12452
|
+
bubbles: true,
|
|
12453
|
+
composed: true
|
|
12454
|
+
}));
|
|
12455
|
+
}
|
|
12330
12456
|
#selectSibling(index, segment) {
|
|
12331
12457
|
const cumPath = this._openCumPath;
|
|
12332
12458
|
this.#closeDropdown();
|
|
@@ -12402,7 +12528,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12402
12528
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
12403
12529
|
<label title=${label}
|
|
12404
12530
|
part="label"
|
|
12405
|
-
|
|
12531
|
+
draggable=${draggable ? `true` : `false`}
|
|
12406
12532
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
12407
12533
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
12408
12534
|
@dragend=${() => this.#onLabelDragEnd()}>
|
|
@@ -12419,6 +12545,26 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12419
12545
|
</div>
|
|
12420
12546
|
` : nothing}
|
|
12421
12547
|
</div>
|
|
12548
|
+
`;
|
|
12549
|
+
}
|
|
12550
|
+
/**
|
|
12551
|
+
* A translucent, click-to-restore segment beyond the current path, shown
|
|
12552
|
+
* while `hintPrevious` is enabled and the current path is a prefix of the
|
|
12553
|
+
* deepest path visited. Doesn't participate in drag'n'drop and isn't
|
|
12554
|
+
* keyboard-focusable.
|
|
12555
|
+
*/
|
|
12556
|
+
#renderVirtualSegment(label, hintIndex) {
|
|
12557
|
+
const fullPathSegments = [...this.#getRootSegments(), ...(this._hintSegments ?? []).slice(0, hintIndex + 1)];
|
|
12558
|
+
const icon = this.#resolveSegmentIcon(fullPathSegments, fullPathSegments.length - 1);
|
|
12559
|
+
return html`
|
|
12560
|
+
<div class="seg seg-virtual" part="segment virtual">
|
|
12561
|
+
<label title=${label}
|
|
12562
|
+
part="label"
|
|
12563
|
+
@click=${(e) => this.#onVirtualSegmentClick(hintIndex, e)}>
|
|
12564
|
+
${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}
|
|
12565
|
+
${label}
|
|
12566
|
+
</label>
|
|
12567
|
+
</div>
|
|
12422
12568
|
`;
|
|
12423
12569
|
}
|
|
12424
12570
|
#renderRootSegment(label, index) {
|
|
@@ -12440,7 +12586,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12440
12586
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
12441
12587
|
<label title=${label}
|
|
12442
12588
|
part="label"
|
|
12443
|
-
|
|
12589
|
+
draggable=${draggable ? `true` : `false`}
|
|
12444
12590
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
12445
12591
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
12446
12592
|
@dragend=${() => this.#onLabelDragEnd()}>${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}${label}</label>
|
|
@@ -12546,6 +12692,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12546
12692
|
#onHostKeyDown;
|
|
12547
12693
|
render() {
|
|
12548
12694
|
const segments = this.#getSegments();
|
|
12695
|
+
const hintSegments = this.hintPrevious ? this._hintSegments : null;
|
|
12696
|
+
const virtualSegments = hintSegments !== null && hintSegments.length > segments.length && this.#isSegmentPrefix(segments, hintSegments) ? hintSegments.slice(segments.length) : [];
|
|
12549
12697
|
if (this._editing) {
|
|
12550
12698
|
const editValue = this.path;
|
|
12551
12699
|
return html`
|
|
@@ -12568,10 +12716,15 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12568
12716
|
${this.separator}
|
|
12569
12717
|
</div>` : nothing;
|
|
12570
12718
|
return html`
|
|
12571
|
-
<div class="crumbs scroll-fade-x" tabindex="0"
|
|
12719
|
+
<div class="crumbs scroll-fade-x" tabindex="0"
|
|
12720
|
+
@keydown=${this.#onHostKeyDown}
|
|
12721
|
+
@dragover=${(e) => this.#onHostDragOver(e)}
|
|
12722
|
+
@dragleave=${(e) => this.#onHostDragLeave(e)}
|
|
12723
|
+
@drop=${(e) => this.#onHostDrop(e)}>
|
|
12572
12724
|
${rootSegments.length > 0 ? rootSegments.map((label, i) => this.#renderRootSegment(label, i)) : nothing}
|
|
12573
12725
|
${leadingCaret}
|
|
12574
12726
|
${segments.map((label, i) => this.#renderSegment(label, i, i === segments.length - 1))}
|
|
12727
|
+
${virtualSegments.map((label, i) => this.#renderVirtualSegment(label, segments.length + i))}
|
|
12575
12728
|
<div class="trailing" @click=${this.#onTrailingClick}></div>
|
|
12576
12729
|
</div>
|
|
12577
12730
|
${this._openIdx !== null ? html`
|
|
@@ -12598,6 +12751,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12598
12751
|
--padding-m: var(--space-s);
|
|
12599
12752
|
--hover-bg: var(--surface-5);
|
|
12600
12753
|
--hover-fg: var(--surface-5-text);
|
|
12754
|
+
--crumb-hint-opacity: 0.35;
|
|
12755
|
+
--crumb-hint-hover-opacity: 0.6;
|
|
12601
12756
|
|
|
12602
12757
|
display: flex;
|
|
12603
12758
|
width: 100%;
|
|
@@ -12709,6 +12864,14 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
12709
12864
|
background: var(--crumb-drop-into-bg, color-mix(in srgb, var(--accent) 14%, transparent));
|
|
12710
12865
|
}
|
|
12711
12866
|
|
|
12867
|
+
.seg-virtual {
|
|
12868
|
+
opacity: var(--crumb-hint-opacity);
|
|
12869
|
+
}
|
|
12870
|
+
|
|
12871
|
+
.seg-virtual:hover {
|
|
12872
|
+
opacity: var(--crumb-hint-hover-opacity);
|
|
12873
|
+
}
|
|
12874
|
+
|
|
12712
12875
|
.crumb-icon {
|
|
12713
12876
|
width: 1em;
|
|
12714
12877
|
height: 1em;
|
|
@@ -12793,6 +12956,10 @@ __decorate([property({ type: Object })], CrumbPathElement.prototype, "siblingLoo
|
|
|
12793
12956
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentIconLookup", void 0);
|
|
12794
12957
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentDragLookup", void 0);
|
|
12795
12958
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "parseExternalDrop", void 0);
|
|
12959
|
+
__decorate([property({
|
|
12960
|
+
type: Boolean,
|
|
12961
|
+
attribute: `hint-previous`
|
|
12962
|
+
})], CrumbPathElement.prototype, "hintPrevious", void 0);
|
|
12796
12963
|
__decorate([state()], CrumbPathElement.prototype, "_openIdx", void 0);
|
|
12797
12964
|
__decorate([state()], CrumbPathElement.prototype, "_editing", void 0);
|
|
12798
12965
|
__decorate([state()], CrumbPathElement.prototype, "_cachedSiblings", void 0);
|
|
@@ -12804,6 +12971,7 @@ __decorate([state()], CrumbPathElement.prototype, "_editStartIndex", void 0);
|
|
|
12804
12971
|
__decorate([state()], CrumbPathElement.prototype, "_openCumPath", void 0);
|
|
12805
12972
|
__decorate([state()], CrumbPathElement.prototype, "_dragCumPath", void 0);
|
|
12806
12973
|
__decorate([state()], CrumbPathElement.prototype, "_dropCumPath", void 0);
|
|
12974
|
+
__decorate([state()], CrumbPathElement.prototype, "_hintSegments", void 0);
|
|
12807
12975
|
CrumbPathElement = __decorate([safeCustomElement(`ixfx-crumb-path`)], CrumbPathElement);
|
|
12808
12976
|
//#endregion
|
|
12809
12977
|
//#region src/data-display/data-display.ts
|
|
@@ -13127,6 +13295,34 @@ let DataDisplayComponent = class DataDisplayComponent extends LitElement {
|
|
|
13127
13295
|
__decorate([property({ type: Boolean })], DataDisplayComponent.prototype, "automap", void 0);
|
|
13128
13296
|
DataDisplayComponent = __decorate([safeCustomElement(`ixfx-data-display`)], DataDisplayComponent);
|
|
13129
13297
|
//#endregion
|
|
13298
|
+
//#region src/common/item-appearance.ts
|
|
13299
|
+
/**
|
|
13300
|
+
* Shared helpers for the list components' per-item appearance hooks — the
|
|
13301
|
+
* per-item icon colour / text colour / badge that miller-list, detail-list,
|
|
13302
|
+
* grid-list, data-grid and vertical-list all accept.
|
|
13303
|
+
*
|
|
13304
|
+
* Every component takes these three features in its own idiom (plain fields
|
|
13305
|
+
* on a `TreeItem`, a property-name lookup, an adapter function, a nominated
|
|
13306
|
+
* column) but they all agree on what "not supplied" means, and on never
|
|
13307
|
+
* emitting an empty slot for an absent value.
|
|
13308
|
+
*/
|
|
13309
|
+
/**
|
|
13310
|
+
* Whether a per-item appearance value counts as "not supplied". `false` and
|
|
13311
|
+
* `''` are included so a consumer can clear a feature without rebuilding the
|
|
13312
|
+
* collection — e.g. `badge: flags && tagChip(flags.colour)`.
|
|
13313
|
+
*/
|
|
13314
|
+
function appearanceAbsent(value) {
|
|
13315
|
+
return value === void 0 || value === null || value === false || value === ``;
|
|
13316
|
+
}
|
|
13317
|
+
/**
|
|
13318
|
+
* A usable CSS colour from a per-item value, or `undefined`. Non-strings and
|
|
13319
|
+
* empty strings are ignored rather than stringified, so a stray number or
|
|
13320
|
+
* `0` never lands in a `style` attribute.
|
|
13321
|
+
*/
|
|
13322
|
+
function appearanceColor(value) {
|
|
13323
|
+
return typeof value === `string` && value.length > 0 ? value : void 0;
|
|
13324
|
+
}
|
|
13325
|
+
//#endregion
|
|
13130
13326
|
//#region src/util/drag-ghost.ts
|
|
13131
13327
|
/**
|
|
13132
13328
|
* Theme-aware floating element helper.
|
|
@@ -14251,6 +14447,12 @@ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
|
|
|
14251
14447
|
PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
|
|
14252
14448
|
//#endregion
|
|
14253
14449
|
//#region src/data-grid/data-grid.ts
|
|
14450
|
+
const EMPTY_CELL_LOOK = {
|
|
14451
|
+
textColor: void 0,
|
|
14452
|
+
icon: void 0,
|
|
14453
|
+
iconColor: void 0,
|
|
14454
|
+
badge: void 0
|
|
14455
|
+
};
|
|
14254
14456
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
14255
14457
|
function sameKeys$3(a, b) {
|
|
14256
14458
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -14275,6 +14477,9 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
14275
14477
|
this.allowGrouping = false;
|
|
14276
14478
|
this.crossGroupSelection = false;
|
|
14277
14479
|
this.columnMenu = `auto`;
|
|
14480
|
+
this.textColorColumn = ``;
|
|
14481
|
+
this.iconColumn = ``;
|
|
14482
|
+
this.badgeColumn = ``;
|
|
14278
14483
|
this.itemsDraggable = false;
|
|
14279
14484
|
this._columnFormatters = /* @__PURE__ */ new Map();
|
|
14280
14485
|
this._resizingColKey = void 0;
|
|
@@ -14694,6 +14899,25 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
14694
14899
|
}
|
|
14695
14900
|
.cell.editable-text { cursor: text; }
|
|
14696
14901
|
.cell input, .cell select { width: 100%; font: inherit; }
|
|
14902
|
+
/* Per-row icon / badge, enabled by nominating iconColumn / badgeColumn.
|
|
14903
|
+
The badge self-justifies to the cell's trailing edge; the value text
|
|
14904
|
+
keeps its own flex sizing so the cell still ellipsises. */
|
|
14905
|
+
.cell-icon {
|
|
14906
|
+
flex: none;
|
|
14907
|
+
display: inline-flex;
|
|
14908
|
+
align-items: center;
|
|
14909
|
+
justify-content: center;
|
|
14910
|
+
margin-inline-end: var(--data-grid-cell-icon-gap, 6px);
|
|
14911
|
+
width: var(--data-grid-cell-icon-size, 1em);
|
|
14912
|
+
height: var(--data-grid-cell-icon-size, 1em);
|
|
14913
|
+
}
|
|
14914
|
+
.cell-badge {
|
|
14915
|
+
flex: none;
|
|
14916
|
+
display: inline-flex;
|
|
14917
|
+
align-items: center;
|
|
14918
|
+
margin-inline-start: auto;
|
|
14919
|
+
padding-inline-start: var(--data-grid-badge-gap, 6px);
|
|
14920
|
+
}
|
|
14697
14921
|
.checkbox-col { width: 28px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
14698
14922
|
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
14923
|
.header-cell[drag-source] { opacity: 0.4; }
|
|
@@ -15465,6 +15689,27 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15465
15689
|
if (this.globalFormatter) return this.globalFormatter(value, col.key, row);
|
|
15466
15690
|
return value;
|
|
15467
15691
|
}
|
|
15692
|
+
/**
|
|
15693
|
+
* Resolve the per-row appearance for the cell at `colKey`, honouring the
|
|
15694
|
+
* nominated columns. Additive — `col.renderer` / `col.formatter` still own
|
|
15695
|
+
* the cell's value; this only decorates it. Returns all-`undefined` for
|
|
15696
|
+
* every cell when nothing is nominated (the common case).
|
|
15697
|
+
*/
|
|
15698
|
+
_cellLook(row, colKey) {
|
|
15699
|
+
if (this.textColorColumn === `` && this.iconColumn === `` && this.badgeColumn === ``) return EMPTY_CELL_LOOK;
|
|
15700
|
+
const rec = typeof row === `object` && row !== null ? row : {};
|
|
15701
|
+
const textColor = colKey === this.textColorColumn ? appearanceColor(rec.textColor) : void 0;
|
|
15702
|
+
const icon = colKey === this.iconColumn && !appearanceAbsent(rec.icon) ? rec.icon : void 0;
|
|
15703
|
+
const iconColor = icon === void 0 ? void 0 : appearanceColor(rec.iconColor);
|
|
15704
|
+
const badge = colKey === this.badgeColumn && !appearanceAbsent(rec.badge) ? rec.badge : void 0;
|
|
15705
|
+
if (textColor === void 0 && icon === void 0 && badge === void 0) return EMPTY_CELL_LOOK;
|
|
15706
|
+
return {
|
|
15707
|
+
textColor,
|
|
15708
|
+
icon,
|
|
15709
|
+
iconColor,
|
|
15710
|
+
badge
|
|
15711
|
+
};
|
|
15712
|
+
}
|
|
15468
15713
|
_handleCellSecondClick(rowKey, col) {
|
|
15469
15714
|
if (this.editability !== `user`) return;
|
|
15470
15715
|
const now = Date.now();
|
|
@@ -15922,7 +16167,9 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15922
16167
|
const textEditable = this._isEditable(row, col) && this._isTextEditor(col);
|
|
15923
16168
|
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
16169
|
const canPan = this.cellOverflow === `pan-text` && !col.renderer && (selected || tickled);
|
|
15925
|
-
|
|
16170
|
+
const look = this._cellLook(row, col.key);
|
|
16171
|
+
const cellStyle = look.textColor === void 0 ? `` : `color:${look.textColor};`;
|
|
16172
|
+
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
16173
|
@click=${(e) => {
|
|
15927
16174
|
this._lastClickedCell = {
|
|
15928
16175
|
rowKey,
|
|
@@ -15944,7 +16191,7 @@ let DataGridElement = class DataGridElement extends LitElement {
|
|
|
15944
16191
|
composed: true
|
|
15945
16192
|
}));
|
|
15946
16193
|
}}
|
|
15947
|
-
>${canPan ? html`<ixfx-pan-text mode="loop">${cellContent}</ixfx-pan-text>` : cellContent}</div>`;
|
|
16194
|
+
>${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
16195
|
})}
|
|
15949
16196
|
</div>
|
|
15950
16197
|
`;
|
|
@@ -16004,6 +16251,18 @@ __decorate([property({
|
|
|
16004
16251
|
attribute: `column-menu`
|
|
16005
16252
|
})], DataGridElement.prototype, "columnMenu", void 0);
|
|
16006
16253
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "globalFormatter", void 0);
|
|
16254
|
+
__decorate([property({
|
|
16255
|
+
type: String,
|
|
16256
|
+
attribute: `text-color-column`
|
|
16257
|
+
})], DataGridElement.prototype, "textColorColumn", void 0);
|
|
16258
|
+
__decorate([property({
|
|
16259
|
+
type: String,
|
|
16260
|
+
attribute: `icon-column`
|
|
16261
|
+
})], DataGridElement.prototype, "iconColumn", void 0);
|
|
16262
|
+
__decorate([property({
|
|
16263
|
+
type: String,
|
|
16264
|
+
attribute: `badge-column`
|
|
16265
|
+
})], DataGridElement.prototype, "badgeColumn", void 0);
|
|
16007
16266
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
|
|
16008
16267
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
|
|
16009
16268
|
__decorate([property({
|
|
@@ -16304,6 +16563,14 @@ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableE
|
|
|
16304
16563
|
const ITEM_SELECTOR$3 = `:scope > li`;
|
|
16305
16564
|
const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
|
|
16306
16565
|
const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
|
|
16566
|
+
/**
|
|
16567
|
+
* Layout for the per-item badge wrapper `#ensureBadge` adds. Applied as inline
|
|
16568
|
+
* `cssText` rather than shadow CSS because `::slotted()` only matches the
|
|
16569
|
+
* host's *direct* children — the badge lives inside a slotted `<li>`, so a
|
|
16570
|
+
* `::slotted()` rule could never reach it. The values still read the CSS
|
|
16571
|
+
* custom property, so it stays themeable from the outside.
|
|
16572
|
+
*/
|
|
16573
|
+
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
16574
|
function pointInRect(x, y, rect) {
|
|
16308
16575
|
return rect.width > 0 && rect.height > 0 && x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
|
16309
16576
|
}
|
|
@@ -16337,6 +16604,9 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16337
16604
|
this.items = [];
|
|
16338
16605
|
this.displayProperty = ``;
|
|
16339
16606
|
this.iconProperty = ``;
|
|
16607
|
+
this.iconColorProperty = ``;
|
|
16608
|
+
this.textColorProperty = ``;
|
|
16609
|
+
this.badgeProperty = ``;
|
|
16340
16610
|
this.keyProperty = ``;
|
|
16341
16611
|
this.itemWrap = `stop`;
|
|
16342
16612
|
this.itemsDraggable = false;
|
|
@@ -16538,7 +16808,8 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16538
16808
|
};
|
|
16539
16809
|
this._onWheel = (e) => {
|
|
16540
16810
|
e.preventDefault();
|
|
16541
|
-
|
|
16811
|
+
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
16812
|
+
this._listEl.scrollLeft += delta;
|
|
16542
16813
|
};
|
|
16543
16814
|
}
|
|
16544
16815
|
#itemData;
|
|
@@ -16743,7 +17014,7 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16743
17014
|
});
|
|
16744
17015
|
return;
|
|
16745
17016
|
}
|
|
16746
|
-
if (changed.has(`items`) && this.provider === void 0 && this.items.length > 0) this.#rebuildFromObjects(this.items);
|
|
17017
|
+
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
17018
|
this.#syncFromProvider();
|
|
16748
17019
|
if (changed.has(`sort`) || changed.has(`sortBy`) || changed.has(`sortComparator`)) this.#applySort();
|
|
16749
17020
|
if (changed.has(`filterPredicate`)) {
|
|
@@ -16948,24 +17219,36 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
16948
17219
|
const li = document.createElement(`li`);
|
|
16949
17220
|
this.#itemData.set(li, item);
|
|
16950
17221
|
const content = this.formatter ? this.formatter(item, index) : this.displayProperty ? this.#propertyValue(item, this.displayProperty) : item;
|
|
17222
|
+
const textColor = this.#colorValue(item, this.textColorProperty);
|
|
17223
|
+
const iconColor = this.#colorValue(item, this.iconColorProperty);
|
|
17224
|
+
if (iconColor !== void 0) li.setAttribute(`data-icon-color`, iconColor);
|
|
16951
17225
|
if (typeof content === `string`) {
|
|
16952
|
-
this._setLabel(li, content);
|
|
17226
|
+
const span = this._setLabel(li, content);
|
|
17227
|
+
if (textColor !== void 0) span.style.color = textColor;
|
|
16953
17228
|
if (this.iconProperty) {
|
|
16954
17229
|
const icon = this.#propertyValue(item, this.iconProperty);
|
|
16955
17230
|
if (typeof icon === `string` && icon.length > 0) li.setAttribute(`data-icon`, icon);
|
|
16956
17231
|
}
|
|
16957
|
-
} else if (content instanceof Node)
|
|
17232
|
+
} else if (content instanceof Node) {
|
|
17233
|
+
li.appendChild(content);
|
|
17234
|
+
if (textColor !== void 0) li.style.color = textColor;
|
|
17235
|
+
}
|
|
16958
17236
|
if (this.keyProperty) {
|
|
16959
17237
|
const key = this.#propertyValue(item, this.keyProperty);
|
|
16960
17238
|
if (key !== void 0 && key !== null) li.setAttribute(`data-key`, String(key));
|
|
16961
17239
|
}
|
|
16962
17240
|
this.#ensureIcon(li);
|
|
17241
|
+
this.#ensureBadge(li, this.#propertyValue(item, this.badgeProperty));
|
|
16963
17242
|
return li;
|
|
16964
17243
|
}
|
|
16965
17244
|
#propertyValue(item, prop) {
|
|
16966
17245
|
if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
|
|
16967
17246
|
return item[prop];
|
|
16968
17247
|
}
|
|
17248
|
+
/** A non-empty string from `item[prop]`, else `undefined`. */
|
|
17249
|
+
#colorValue(item, prop) {
|
|
17250
|
+
return prop ? appearanceColor(this.#propertyValue(item, prop)) : void 0;
|
|
17251
|
+
}
|
|
16969
17252
|
#syncFromProvider() {
|
|
16970
17253
|
if (this.provider === void 0) return;
|
|
16971
17254
|
const items = this.#data.items;
|
|
@@ -17223,12 +17506,14 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17223
17506
|
if (this.interactionMode !== `checked`) return true;
|
|
17224
17507
|
return !item.hasAttribute(`not-checked`);
|
|
17225
17508
|
}
|
|
17509
|
+
/** Appends the caption span; returns it so callers can style it per item. */
|
|
17226
17510
|
_setLabel(li, label) {
|
|
17227
17511
|
const span = document.createElement(`span`);
|
|
17228
17512
|
span.style.cssText = LABEL_SPAN_CSS;
|
|
17229
17513
|
span.textContent = label;
|
|
17230
17514
|
li.appendChild(span);
|
|
17231
17515
|
li.setAttribute(`data-search-label`, label);
|
|
17516
|
+
return span;
|
|
17232
17517
|
}
|
|
17233
17518
|
/**
|
|
17234
17519
|
* Inject a leading `<ixfx-icon>` into an item carrying `data-icon`.
|
|
@@ -17242,8 +17527,28 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17242
17527
|
icon.setAttribute(`name`, name);
|
|
17243
17528
|
icon.style.marginInlineEnd = `var(--detail-list-icon-gap, 6px)`;
|
|
17244
17529
|
icon.style.flexShrink = `0`;
|
|
17530
|
+
const color = li.getAttribute(`data-icon-color`);
|
|
17531
|
+
if (color) icon.style.color = color;
|
|
17245
17532
|
li.prepend(icon);
|
|
17246
17533
|
}
|
|
17534
|
+
/**
|
|
17535
|
+
* Append a right-justified Finder-style badge. `badge` may be a DOM `Node`
|
|
17536
|
+
* or anything Lit can render (typically a `TemplateResult`). Idempotent —
|
|
17537
|
+
* a second call replaces the existing badge.
|
|
17538
|
+
*/
|
|
17539
|
+
#ensureBadge(li, badge) {
|
|
17540
|
+
if (appearanceAbsent(badge)) return;
|
|
17541
|
+
let holder = li.querySelector(`:scope > .item-badge`);
|
|
17542
|
+
if (!holder) {
|
|
17543
|
+
holder = document.createElement(`span`);
|
|
17544
|
+
holder.className = `item-badge`;
|
|
17545
|
+
holder.style.cssText = BADGE_EL_CSS;
|
|
17546
|
+
li.appendChild(holder);
|
|
17547
|
+
}
|
|
17548
|
+
holder.replaceChildren();
|
|
17549
|
+
if (badge instanceof Node) holder.appendChild(badge);
|
|
17550
|
+
else render(badge, holder);
|
|
17551
|
+
}
|
|
17247
17552
|
/** Scan light-DOM children for unprocessed `data-icon` items. */
|
|
17248
17553
|
#processChildren() {
|
|
17249
17554
|
for (const el of this.querySelectorAll(ITEM_SELECTOR$3)) this.#ensureIcon(el);
|
|
@@ -17517,6 +17822,10 @@ let DetailListElement = class DetailListElement extends LitElement {
|
|
|
17517
17822
|
opacity: 0.6;
|
|
17518
17823
|
}
|
|
17519
17824
|
|
|
17825
|
+
/* Per-item icon / badge (adapter mode) are styled inline by
|
|
17826
|
+
#ensureBadge — ::slotted() only reaches the host's direct children,
|
|
17827
|
+
and these spans live inside a slotted <li>. */
|
|
17828
|
+
|
|
17520
17829
|
/* checked mode: the injected checkbox communicates selection — suppress bg */
|
|
17521
17830
|
:host([interaction-mode="checked"]) ::slotted([data-selected]) {
|
|
17522
17831
|
background: transparent;
|
|
@@ -17591,6 +17900,18 @@ __decorate([property({
|
|
|
17591
17900
|
type: String,
|
|
17592
17901
|
attribute: `icon-property`
|
|
17593
17902
|
})], DetailListElement.prototype, "iconProperty", void 0);
|
|
17903
|
+
__decorate([property({
|
|
17904
|
+
type: String,
|
|
17905
|
+
attribute: `icon-color-property`
|
|
17906
|
+
})], DetailListElement.prototype, "iconColorProperty", void 0);
|
|
17907
|
+
__decorate([property({
|
|
17908
|
+
type: String,
|
|
17909
|
+
attribute: `text-color-property`
|
|
17910
|
+
})], DetailListElement.prototype, "textColorProperty", void 0);
|
|
17911
|
+
__decorate([property({
|
|
17912
|
+
type: String,
|
|
17913
|
+
attribute: `badge-property`
|
|
17914
|
+
})], DetailListElement.prototype, "badgeProperty", void 0);
|
|
17594
17915
|
__decorate([property({
|
|
17595
17916
|
type: String,
|
|
17596
17917
|
attribute: `key-property`
|
|
@@ -23322,6 +23643,10 @@ var ElementGridListSource = class {
|
|
|
23322
23643
|
if (eff === void 0) return void 0;
|
|
23323
23644
|
return html`<slot name=${this.slotNameFor(this.#items[eff])}></slot>`;
|
|
23324
23645
|
}
|
|
23646
|
+
iconAt(_index) {}
|
|
23647
|
+
iconColorAt(_index) {}
|
|
23648
|
+
textColorAt(_index) {}
|
|
23649
|
+
badgeAt(_index) {}
|
|
23325
23650
|
setFilters(opts) {
|
|
23326
23651
|
if (`predicate` in opts) this.#predicate = opts.predicate;
|
|
23327
23652
|
if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
|
|
@@ -23438,6 +23763,22 @@ var DataGridListSource = class {
|
|
|
23438
23763
|
const eff = this.#effectiveIndices[index];
|
|
23439
23764
|
return eff === void 0 ? void 0 : this.#adapter.render(this.#items[eff], eff);
|
|
23440
23765
|
}
|
|
23766
|
+
iconAt(index) {
|
|
23767
|
+
const eff = this.#effectiveIndices[index];
|
|
23768
|
+
return eff === void 0 || this.#adapter.icon === void 0 ? void 0 : this.#adapter.icon(this.#items[eff], eff);
|
|
23769
|
+
}
|
|
23770
|
+
iconColorAt(index) {
|
|
23771
|
+
const eff = this.#effectiveIndices[index];
|
|
23772
|
+
return eff === void 0 ? void 0 : this.#adapter.iconColor?.(this.#items[eff], eff);
|
|
23773
|
+
}
|
|
23774
|
+
textColorAt(index) {
|
|
23775
|
+
const eff = this.#effectiveIndices[index];
|
|
23776
|
+
return eff === void 0 ? void 0 : this.#adapter.textColor?.(this.#items[eff], eff);
|
|
23777
|
+
}
|
|
23778
|
+
badgeAt(index) {
|
|
23779
|
+
const eff = this.#effectiveIndices[index];
|
|
23780
|
+
return eff === void 0 || this.#adapter.badge === void 0 ? void 0 : this.#adapter.badge(this.#items[eff], eff);
|
|
23781
|
+
}
|
|
23441
23782
|
setFilters(opts) {
|
|
23442
23783
|
if (`predicate` in opts) this.#predicate = opts.predicate;
|
|
23443
23784
|
if (`searchKeys` in opts) this.#searchKeys = opts.searchKeys;
|
|
@@ -23978,6 +24319,50 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
23978
24319
|
color: inherit;
|
|
23979
24320
|
}
|
|
23980
24321
|
|
|
24322
|
+
/* ---- Per-item icon (data mode) ----
|
|
24323
|
+
Only [has-icon] switches to a column, so a cell without an icon keeps
|
|
24324
|
+
exactly the layout it had before. The main content stays stretched;
|
|
24325
|
+
the icon centres itself above it. */
|
|
24326
|
+
.content.has-icon {
|
|
24327
|
+
display: flex;
|
|
24328
|
+
flex-direction: column;
|
|
24329
|
+
min-height: 0;
|
|
24330
|
+
gap: var(--grid-list-icon-gap, 2px);
|
|
24331
|
+
}
|
|
24332
|
+
.cell-icon {
|
|
24333
|
+
flex: none;
|
|
24334
|
+
align-self: center;
|
|
24335
|
+
display: inline-flex;
|
|
24336
|
+
align-items: center;
|
|
24337
|
+
justify-content: center;
|
|
24338
|
+
width: var(--grid-list-icon-size, 1.25em);
|
|
24339
|
+
height: var(--grid-list-icon-size, 1.25em);
|
|
24340
|
+
}
|
|
24341
|
+
|
|
24342
|
+
/* ---- Per-item badge (data mode) ----
|
|
24343
|
+
Finder thumbnails put the tag inline, immediately before the name, so
|
|
24344
|
+
the caption becomes a flex row. The text is wrapped in its own
|
|
24345
|
+
shrinkable box so the ellipsis survives. */
|
|
24346
|
+
.caption.has-badge {
|
|
24347
|
+
display: flex;
|
|
24348
|
+
align-items: center;
|
|
24349
|
+
gap: var(--grid-list-badge-gap, 3px);
|
|
24350
|
+
text-align: initial;
|
|
24351
|
+
}
|
|
24352
|
+
:host([caption-align="left"]) .caption.has-badge { justify-content: flex-start; }
|
|
24353
|
+
:host([caption-align="right"]) .caption.has-badge { justify-content: flex-end; }
|
|
24354
|
+
.caption.has-badge .caption-text {
|
|
24355
|
+
flex: 0 1 auto;
|
|
24356
|
+
min-width: 0;
|
|
24357
|
+
overflow: hidden;
|
|
24358
|
+
text-overflow: ellipsis;
|
|
24359
|
+
}
|
|
24360
|
+
.caption .badge {
|
|
24361
|
+
flex: none;
|
|
24362
|
+
display: inline-flex;
|
|
24363
|
+
align-items: center;
|
|
24364
|
+
}
|
|
24365
|
+
|
|
23981
24366
|
/* ---- Box modes (hidden / under / overlaid): the cell is one box with a
|
|
23982
24367
|
single selection outline ---- */
|
|
23983
24368
|
:host(:not([caption-style="separate"])) .cell {
|
|
@@ -24986,6 +25371,8 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
24986
25371
|
const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
|
|
24987
25372
|
const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
|
|
24988
25373
|
const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
|
|
25374
|
+
const rawIcon = this.#source.iconAt(index);
|
|
25375
|
+
const icon = appearanceAbsent(rawIcon) ? void 0 : rawIcon;
|
|
24989
25376
|
return html`
|
|
24990
25377
|
<div
|
|
24991
25378
|
class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
|
|
@@ -25003,8 +25390,11 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
25003
25390
|
@dblclick=${() => this.#onCellActivate(key)}
|
|
25004
25391
|
@pointerenter=${() => this.#handleCellPointerEnter(key)}
|
|
25005
25392
|
>
|
|
25006
|
-
<div class="content" part="content"
|
|
25007
|
-
|
|
25393
|
+
<div class="content${icon === void 0 ? `` : ` has-icon`}" part="content">
|
|
25394
|
+
${icon === void 0 ? nothing : html`<span class="cell-icon" part="icon" style=${this.#colorStyle(this.#source.iconColorAt(index))}>${icon}</span>`}
|
|
25395
|
+
${this.#source.renderCellContent(index)}
|
|
25396
|
+
</div>
|
|
25397
|
+
${showCaption ? this.#renderCaption(caption, captionCss, tickled, this.#source.badgeAt(index), this.#source.textColorAt(index)) : ``}
|
|
25008
25398
|
</div>
|
|
25009
25399
|
`;
|
|
25010
25400
|
}
|
|
@@ -25012,10 +25402,25 @@ let GridListElement = class GridListElement extends LitElement {
|
|
|
25012
25402
|
* A tickled cell swaps its plain label for an `ixfx-pan-text` in loop mode
|
|
25013
25403
|
* so a truncated label scrolls instead of staying an ellipsis. Unticked
|
|
25014
25404
|
* cells keep the cheap plain-text caption.
|
|
25405
|
+
*
|
|
25406
|
+
* With no `badge` and no `textColor` the DOM is exactly as it was — the
|
|
25407
|
+
* flex row (and its extra `<span>` around the text) only appears when a
|
|
25408
|
+
* badge needs to sit inline ahead of the name.
|
|
25015
25409
|
*/
|
|
25016
|
-
#renderCaption(caption, captionCss, tickled) {
|
|
25017
|
-
|
|
25018
|
-
|
|
25410
|
+
#renderCaption(caption, captionCss, tickled, badge, textColor) {
|
|
25411
|
+
const color = appearanceColor(textColor);
|
|
25412
|
+
const style = color === void 0 ? captionCss : `${captionCss}${captionCss.length > 0 ? `;` : ``}color:${color};`;
|
|
25413
|
+
if (appearanceAbsent(badge)) {
|
|
25414
|
+
if (tickled) return html`<div class="caption" part="caption" style=${style} title=${caption}><ixfx-pan-text mode="loop">${caption}</ixfx-pan-text></div>`;
|
|
25415
|
+
return html`<div class="caption" part="caption" style=${style} title=${caption}>${caption}</div>`;
|
|
25416
|
+
}
|
|
25417
|
+
const label = tickled ? html`<ixfx-pan-text mode="loop">${caption}</ixfx-pan-text>` : html`<span class="caption-text">${caption}</span>`;
|
|
25418
|
+
return html`<div class="caption has-badge" part="caption" style=${style} title=${caption}><span class="badge" part="badge">${badge}</span>${label}</div>`;
|
|
25419
|
+
}
|
|
25420
|
+
/** `color:<c>;` for a non-empty colour, else `nothing` (attribute removed). */
|
|
25421
|
+
#colorStyle(color) {
|
|
25422
|
+
const c = appearanceColor(color);
|
|
25423
|
+
return c === void 0 ? nothing : `color:${c};`;
|
|
25019
25424
|
}
|
|
25020
25425
|
/** Stop the active caption pan, if any, and forget it. */
|
|
25021
25426
|
#stopActiveCaptionPan() {
|
|
@@ -28218,6 +28623,16 @@ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data
|
|
|
28218
28623
|
* vertical rows are shorter relative to the viewport, so 3.
|
|
28219
28624
|
*/
|
|
28220
28625
|
const VIRTUAL_OVERSCAN = 3;
|
|
28626
|
+
/**
|
|
28627
|
+
* Layout for the per-item icon / badge wrappers `#decorateAdapterItem` adds.
|
|
28628
|
+
*
|
|
28629
|
+
* Applied as inline `cssText` rather than shadow CSS because `::slotted()` only
|
|
28630
|
+
* matches the host's *direct* children — these spans live inside a slotted
|
|
28631
|
+
* `<li>`, so a `::slotted()` rule could never reach them. The values still read
|
|
28632
|
+
* the CSS custom properties, so they stay themeable from the outside.
|
|
28633
|
+
*/
|
|
28634
|
+
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)`;
|
|
28635
|
+
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
28636
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
28222
28637
|
function sameKeys(a, b) {
|
|
28223
28638
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -28627,7 +29042,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28627
29042
|
}
|
|
28628
29043
|
this.#syncSearchCommands();
|
|
28629
29044
|
}
|
|
28630
|
-
if ((changed.has(`items`) || changed.has(`formatter`) || changed.has(`keyProperty`)) && this.items !== void 0) {
|
|
29045
|
+
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
29046
|
if (this.#suspendAdapterRebuild) this.#suspendAdapterRebuild = false;
|
|
28632
29047
|
else if (this.#virtualActive()) this.#rebuildAdapterVirtual();
|
|
28633
29048
|
else this.#rebuildFromAdapter();
|
|
@@ -28897,7 +29312,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28897
29312
|
const fresh = this.formatter ? this.formatter(data, index) : data;
|
|
28898
29313
|
if (el instanceof HTMLLIElement) {
|
|
28899
29314
|
for (const child of [...el.childNodes]) child.remove();
|
|
28900
|
-
this.#fillAdapterItem(el, fresh, data);
|
|
29315
|
+
this.#fillAdapterItem(el, fresh, data, index);
|
|
28901
29316
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
28902
29317
|
this._syncItemStates();
|
|
28903
29318
|
} else if (fresh instanceof Element && this.#isRowElement(fresh)) {
|
|
@@ -28952,17 +29367,47 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
28952
29367
|
return content;
|
|
28953
29368
|
}
|
|
28954
29369
|
const li = document.createElement(`li`);
|
|
28955
|
-
this.#fillAdapterItem(li, content, item);
|
|
29370
|
+
this.#fillAdapterItem(li, content, item, index);
|
|
28956
29371
|
return li;
|
|
28957
29372
|
}
|
|
28958
|
-
#fillAdapterItem(li, content, item) {
|
|
29373
|
+
#fillAdapterItem(li, content, item, index) {
|
|
28959
29374
|
if (typeof content === `string`) li.textContent = content;
|
|
28960
29375
|
else if (content instanceof Node) li.appendChild(content);
|
|
28961
29376
|
else if (this.formatter && content !== void 0 && content !== null) render(content, li);
|
|
29377
|
+
this.#decorateAdapterItem(li, item, index);
|
|
28962
29378
|
this.#stampAdapterKey(li, item);
|
|
28963
29379
|
this.#itemData.set(li, item);
|
|
28964
29380
|
}
|
|
28965
29381
|
/**
|
|
29382
|
+
* Apply the per-item icon / text colour / badge to a built row. Split out
|
|
29383
|
+
* from {@link #fillAdapterItem} so the parts stay readable.
|
|
29384
|
+
*/
|
|
29385
|
+
#decorateAdapterItem(li, item, index) {
|
|
29386
|
+
const textColor = appearanceColor(this.textColor?.(item, index));
|
|
29387
|
+
if (textColor !== void 0) li.style.color = textColor;
|
|
29388
|
+
else li.style.removeProperty(`color`);
|
|
29389
|
+
const icon = this.icon?.(item, index);
|
|
29390
|
+
if (!appearanceAbsent(icon)) {
|
|
29391
|
+
const holder = document.createElement(`span`);
|
|
29392
|
+
holder.className = `item-icon`;
|
|
29393
|
+
holder.style.cssText = ITEM_ICON_EL_CSS;
|
|
29394
|
+
const iconColor = appearanceColor(this.iconColor?.(item, index));
|
|
29395
|
+
if (iconColor !== void 0) holder.style.color = iconColor;
|
|
29396
|
+
if (icon instanceof Node) holder.appendChild(icon);
|
|
29397
|
+
else render(icon, holder);
|
|
29398
|
+
li.prepend(holder);
|
|
29399
|
+
}
|
|
29400
|
+
const badge = this.badge?.(item, index);
|
|
29401
|
+
if (!appearanceAbsent(badge)) {
|
|
29402
|
+
const holder = document.createElement(`span`);
|
|
29403
|
+
holder.className = `item-badge`;
|
|
29404
|
+
holder.style.cssText = ITEM_BADGE_EL_CSS;
|
|
29405
|
+
if (badge instanceof Node) holder.appendChild(badge);
|
|
29406
|
+
else render(badge, holder);
|
|
29407
|
+
li.appendChild(holder);
|
|
29408
|
+
}
|
|
29409
|
+
}
|
|
29410
|
+
/**
|
|
28966
29411
|
* Whether a formatter-produced element is a whole row (used directly)
|
|
28967
29412
|
* rather than row content (rendered into a `<li>`): an `<li>`, already
|
|
28968
29413
|
* marked `[data-list-item]`, or a custom element.
|
|
@@ -29484,6 +29929,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
29484
29929
|
outline: 1px solid transparent;
|
|
29485
29930
|
}
|
|
29486
29931
|
|
|
29932
|
+
/* Per-item icon / badge (adapter mode) are styled inline by
|
|
29933
|
+
#decorateAdapterItem — ::slotted() only reaches the host's direct
|
|
29934
|
+
children, and these spans live inside a slotted <li>. */
|
|
29935
|
+
|
|
29487
29936
|
/* Custom shadow-DOM components handle their own internal layout/padding.
|
|
29488
29937
|
Wrapped in :where() so the selector stays at zero specificity and a
|
|
29489
29938
|
consumer's own :host rule (e.g. display: flex) wins without
|
|
@@ -29630,6 +30079,10 @@ __decorate([property({
|
|
|
29630
30079
|
attribute: `key-property`
|
|
29631
30080
|
})], VerticalListElement.prototype, "keyProperty", void 0);
|
|
29632
30081
|
__decorate([property({ attribute: false })], VerticalListElement.prototype, "formatter", void 0);
|
|
30082
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "icon", void 0);
|
|
30083
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "iconColor", void 0);
|
|
30084
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "textColor", void 0);
|
|
30085
|
+
__decorate([property({ attribute: false })], VerticalListElement.prototype, "badge", void 0);
|
|
29633
30086
|
__decorate([property({
|
|
29634
30087
|
type: Boolean,
|
|
29635
30088
|
attribute: `virtualize`,
|
|
@@ -30905,6 +31358,90 @@ __decorate([state()], IxfxHexEditorElement.prototype, "_containerH", void 0);
|
|
|
30905
31358
|
__decorate([state()], IxfxHexEditorElement.prototype, "_shadeMap", void 0);
|
|
30906
31359
|
__decorate([state()], IxfxHexEditorElement.prototype, "_focusSide", void 0);
|
|
30907
31360
|
IxfxHexEditorElement = __decorate([safeCustomElement(`ixfx-hex-editor`)], IxfxHexEditorElement);
|
|
31361
|
+
/**
|
|
31362
|
+
* Whether `n` deserves a preview: it has children and at least one of them
|
|
31363
|
+
* isn't legible right now — laid out without a label, or not laid out at
|
|
31364
|
+
* all (outside the `maxRadius` window).
|
|
31365
|
+
*/
|
|
31366
|
+
function needsChildPreview(n, visible, labelIds) {
|
|
31367
|
+
const children = n.node.children ?? [];
|
|
31368
|
+
if (children.length === 0) return false;
|
|
31369
|
+
const shown = visible.filter((v) => v.parentId === n.id);
|
|
31370
|
+
return shown.length < children.length || shown.some((v) => !labelIds.has(v.id));
|
|
31371
|
+
}
|
|
31372
|
+
/** Where node position `p` (disk units) lands on screen, in CSS px from the host's top-left. */
|
|
31373
|
+
function diskToHostPx(p, zoom, width, height) {
|
|
31374
|
+
const pxPerUnit = Math.min(width, height) / 2.1;
|
|
31375
|
+
return {
|
|
31376
|
+
x: width / 2 + (zoom.offset.x + zoom.zoom * p.x) * pxPerUnit,
|
|
31377
|
+
y: height / 2 + (zoom.offset.y + zoom.zoom * p.y) * pxPerUnit
|
|
31378
|
+
};
|
|
31379
|
+
}
|
|
31380
|
+
function renderChildPreview(preview, handlers) {
|
|
31381
|
+
if (!preview) return nothing;
|
|
31382
|
+
const children = preview.node.children ?? [];
|
|
31383
|
+
const listed = children.slice(0, 12);
|
|
31384
|
+
const more = children.length - listed.length;
|
|
31385
|
+
return html`
|
|
31386
|
+
<div
|
|
31387
|
+
class="child-preview ${preview.above ? `above` : `below`}"
|
|
31388
|
+
part="child-preview"
|
|
31389
|
+
role="dialog"
|
|
31390
|
+
aria-label=${`Children of ${preview.node.item.label}`}
|
|
31391
|
+
style="left: ${preview.anchor.x}px; top: ${preview.anchor.y}px"
|
|
31392
|
+
@pointerenter=${handlers.onPointerEnter}
|
|
31393
|
+
@pointerleave=${handlers.onPointerLeave}
|
|
31394
|
+
@pointerdown=${(e) => e.stopPropagation()}
|
|
31395
|
+
>
|
|
31396
|
+
<div class="child-preview-title">${preview.node.item.label}</div>
|
|
31397
|
+
<ul>
|
|
31398
|
+
${listed.map((child) => html`<li>
|
|
31399
|
+
<button type="button" tabindex="-1" @click=${() => handlers.onPick(child)}>${child.item.label}</button>
|
|
31400
|
+
</li>`)}
|
|
31401
|
+
</ul>
|
|
31402
|
+
${more > 0 ? html`<div class="child-preview-more">+${more} more</div>` : nothing}
|
|
31403
|
+
</div>
|
|
31404
|
+
`;
|
|
31405
|
+
}
|
|
31406
|
+
/**
|
|
31407
|
+
* Show/hide timing for the preview: shows at once for a qualifying node;
|
|
31408
|
+
* otherwise hides after a short grace period, unless the pointer is over the
|
|
31409
|
+
* popover itself — so it can travel from the node onto the list.
|
|
31410
|
+
*/
|
|
31411
|
+
function createChildPreviewTiming(host) {
|
|
31412
|
+
let hovered = false;
|
|
31413
|
+
let timer;
|
|
31414
|
+
const update = (id) => {
|
|
31415
|
+
clearTimeout(timer);
|
|
31416
|
+
const candidate = host.candidate(id);
|
|
31417
|
+
if (candidate !== void 0) {
|
|
31418
|
+
host.setPreviewId(candidate);
|
|
31419
|
+
return;
|
|
31420
|
+
}
|
|
31421
|
+
timer = setTimeout(() => {
|
|
31422
|
+
if (!hovered) host.setPreviewId(void 0);
|
|
31423
|
+
}, 250);
|
|
31424
|
+
};
|
|
31425
|
+
return {
|
|
31426
|
+
update,
|
|
31427
|
+
pointerEnter() {
|
|
31428
|
+
hovered = true;
|
|
31429
|
+
clearTimeout(timer);
|
|
31430
|
+
},
|
|
31431
|
+
pointerLeave(tickledId) {
|
|
31432
|
+
hovered = false;
|
|
31433
|
+
update(tickledId);
|
|
31434
|
+
},
|
|
31435
|
+
hide() {
|
|
31436
|
+
hovered = false;
|
|
31437
|
+
clearTimeout(timer);
|
|
31438
|
+
host.setPreviewId(void 0);
|
|
31439
|
+
},
|
|
31440
|
+
dispose() {
|
|
31441
|
+
clearTimeout(timer);
|
|
31442
|
+
}
|
|
31443
|
+
};
|
|
31444
|
+
}
|
|
30908
31445
|
//#endregion
|
|
30909
31446
|
//#region src/hyperbolic-tree/poincare-math.ts
|
|
30910
31447
|
const EPSILON = 1e-9;
|
|
@@ -31009,8 +31546,625 @@ function geodesicInterpolate(a, b, t) {
|
|
|
31009
31546
|
if (localNorm < EPSILON) return a;
|
|
31010
31547
|
return mobiusFromOrigin(complexScale(complexScale(bLocal, 1 / localNorm), euclideanRadiusForHyperbolicDistance(hyperbolicDistanceForEuclideanRadius(localNorm) * t)), a);
|
|
31011
31548
|
}
|
|
31549
|
+
const identityIsometry = {
|
|
31550
|
+
a: {
|
|
31551
|
+
x: 1,
|
|
31552
|
+
y: 0
|
|
31553
|
+
},
|
|
31554
|
+
b: {
|
|
31555
|
+
x: 0,
|
|
31556
|
+
y: 0
|
|
31557
|
+
}
|
|
31558
|
+
};
|
|
31559
|
+
/** Apply `m` to `z`. */
|
|
31560
|
+
function applyIsometry(m, z) {
|
|
31561
|
+
return clampToDisk(complexDiv(complexAdd(complexMul(m.a, z), m.b), complexAdd(complexMul(complexConjugate(m.b), z), complexConjugate(m.a))));
|
|
31562
|
+
}
|
|
31563
|
+
/** `outer ∘ inner` — apply `inner` first, then `outer`. */
|
|
31564
|
+
function composeIsometry(outer, inner) {
|
|
31565
|
+
const a = complexAdd(complexMul(outer.a, inner.a), complexMul(outer.b, complexConjugate(inner.b)));
|
|
31566
|
+
const b = complexAdd(complexMul(outer.a, inner.b), complexMul(outer.b, complexConjugate(inner.a)));
|
|
31567
|
+
const det = Math.sqrt(Math.max(EPSILON, a.x * a.x + a.y * a.y - b.x * b.x - b.y * b.y));
|
|
31568
|
+
return {
|
|
31569
|
+
a: complexScale(a, 1 / det),
|
|
31570
|
+
b: complexScale(b, 1 / det)
|
|
31571
|
+
};
|
|
31572
|
+
}
|
|
31573
|
+
function invertIsometry(m) {
|
|
31574
|
+
return {
|
|
31575
|
+
a: complexConjugate(m.a),
|
|
31576
|
+
b: complexScale(m.b, -1)
|
|
31577
|
+
};
|
|
31578
|
+
}
|
|
31579
|
+
/** Pure translation carrying the origin to `p` (same map as {@link mobiusFromOrigin}). */
|
|
31580
|
+
function translationFromOrigin(p) {
|
|
31581
|
+
const scale = 1 / Math.sqrt(Math.max(EPSILON, 1 - p.x * p.x - p.y * p.y));
|
|
31582
|
+
return {
|
|
31583
|
+
a: {
|
|
31584
|
+
x: scale,
|
|
31585
|
+
y: 0
|
|
31586
|
+
},
|
|
31587
|
+
b: complexScale(p, scale)
|
|
31588
|
+
};
|
|
31589
|
+
}
|
|
31590
|
+
/** `z ↦ e^{iθ}·z`. */
|
|
31591
|
+
function rotationIsometry(theta) {
|
|
31592
|
+
return {
|
|
31593
|
+
a: {
|
|
31594
|
+
x: Math.cos(theta / 2),
|
|
31595
|
+
y: Math.sin(theta / 2)
|
|
31596
|
+
},
|
|
31597
|
+
b: {
|
|
31598
|
+
x: 0,
|
|
31599
|
+
y: 0
|
|
31600
|
+
}
|
|
31601
|
+
};
|
|
31602
|
+
}
|
|
31603
|
+
/**
|
|
31604
|
+
* The view map that sends canonical point `center` to the origin, then
|
|
31605
|
+
* rotates by `rotation` radians: `z ↦ e^{i·rotation}·mobiusToOrigin(z, center)`.
|
|
31606
|
+
*/
|
|
31607
|
+
function viewIsometry(center, rotation = 0) {
|
|
31608
|
+
return composeIsometry(rotationIsometry(rotation), invertIsometry(translationFromOrigin(center)));
|
|
31609
|
+
}
|
|
31610
|
+
/** Decompose `m` into the `{ center, rotation }` accepted by {@link viewIsometry}. */
|
|
31611
|
+
function decomposeViewIsometry(m) {
|
|
31612
|
+
return {
|
|
31613
|
+
center: applyIsometry(invertIsometry(m), {
|
|
31614
|
+
x: 0,
|
|
31615
|
+
y: 0
|
|
31616
|
+
}),
|
|
31617
|
+
rotation: 2 * Math.atan2(m.a.y, m.a.x)
|
|
31618
|
+
};
|
|
31619
|
+
}
|
|
31620
|
+
function geodesicSegment(p, q) {
|
|
31621
|
+
const det = p.x * q.y - p.y * q.x;
|
|
31622
|
+
const chord = Math.hypot(q.x - p.x, q.y - p.y);
|
|
31623
|
+
if (Math.abs(det) < 1e-6 * Math.max(chord, EPSILON)) return { kind: `line` };
|
|
31624
|
+
const rp = (p.x * p.x + p.y * p.y + 1) / 2;
|
|
31625
|
+
const rq = (q.x * q.x + q.y * q.y + 1) / 2;
|
|
31626
|
+
const center = {
|
|
31627
|
+
x: (rp * q.y - rq * p.y) / det,
|
|
31628
|
+
y: (p.x * rq - q.x * rp) / det
|
|
31629
|
+
};
|
|
31630
|
+
return {
|
|
31631
|
+
kind: `arc`,
|
|
31632
|
+
center,
|
|
31633
|
+
radius: Math.sqrt(Math.max(0, center.x * center.x + center.y * center.y - 1)),
|
|
31634
|
+
sweep: (p.x - center.x) * (q.y - center.y) - (p.y - center.y) * (q.x - center.x) > 0 ? 1 : 0
|
|
31635
|
+
};
|
|
31636
|
+
}
|
|
31637
|
+
/** SVG path `d` for the geodesic from `p` to `q`, with coordinates multiplied by `scale`. */
|
|
31638
|
+
function geodesicPath(p, q, scale = 1) {
|
|
31639
|
+
const fmt = (n) => (n * scale).toFixed(2);
|
|
31640
|
+
const segment = geodesicSegment(p, q);
|
|
31641
|
+
const start = `M${fmt(p.x)},${fmt(p.y)}`;
|
|
31642
|
+
if (segment.kind === `line` || segment.radius * scale > 1e6) return `${start} L${fmt(q.x)},${fmt(q.y)}`;
|
|
31643
|
+
return `${start} A${fmt(segment.radius)},${fmt(segment.radius)} 0 0 ${segment.sweep} ${fmt(q.x)},${fmt(q.y)}`;
|
|
31644
|
+
}
|
|
31645
|
+
//#endregion
|
|
31646
|
+
//#region src/hyperbolic-tree/view-motion.ts
|
|
31647
|
+
/** Wrap an angle into `(-π, π]`. */
|
|
31648
|
+
function wrapAngle(theta) {
|
|
31649
|
+
return Math.atan2(Math.sin(theta), Math.cos(theta));
|
|
31650
|
+
}
|
|
31651
|
+
function easeInOutCubic(t) {
|
|
31652
|
+
return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
31653
|
+
}
|
|
31654
|
+
function prefersReducedMotion$1() {
|
|
31655
|
+
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
31656
|
+
}
|
|
31657
|
+
/**
|
|
31658
|
+
* The view after dragging from `start` to `current` (both disk
|
|
31659
|
+
* coordinates), starting from `baseView`. Applies the hyperbolic translation
|
|
31660
|
+
* along the geodesic carrying the grabbed point to the pointer — a true
|
|
31661
|
+
* isometry, so the grabbed point tracks the pointer exactly and the tree
|
|
31662
|
+
* never distorts. (Adding a Euclidean offset to the canonical center
|
|
31663
|
+
* instead is not an isometry: it lurches badly near the disk boundary.)
|
|
31664
|
+
*/
|
|
31665
|
+
function panView(baseView, start, current) {
|
|
31666
|
+
const toStart = translationFromOrigin(start);
|
|
31667
|
+
return composeIsometry(composeIsometry(toStart, composeIsometry(translationFromOrigin(mobiusToOrigin(current, start)), invertIsometry(toStart))), baseView);
|
|
31668
|
+
}
|
|
31669
|
+
/**
|
|
31670
|
+
* A focus-change animation from view `from` — the old view, re-expressed in
|
|
31671
|
+
* the *new* focus's local frame, so the new focus currently sits somewhere
|
|
31672
|
+
* off-center — to a settled view with the new focus at the center. The
|
|
31673
|
+
* center travels along the hyperbolic geodesic; the rotation is held, so the
|
|
31674
|
+
* tree glides into place without spinning, and the settled view keeps
|
|
31675
|
+
* whatever orientation it arrived with (`settledRotation`).
|
|
31676
|
+
*/
|
|
31677
|
+
function focusTransition(from) {
|
|
31678
|
+
const { center, rotation } = decomposeViewIsometry(from);
|
|
31679
|
+
const settledRotation = wrapAngle(rotation);
|
|
31680
|
+
const origin = {
|
|
31681
|
+
x: 0,
|
|
31682
|
+
y: 0
|
|
31683
|
+
};
|
|
31684
|
+
return {
|
|
31685
|
+
settledRotation,
|
|
31686
|
+
viewAt: (t) => {
|
|
31687
|
+
const eased = easeInOutCubic(Math.min(1, Math.max(0, t)));
|
|
31688
|
+
return viewIsometry(geodesicInterpolate(center, origin, eased), settledRotation);
|
|
31689
|
+
}
|
|
31690
|
+
};
|
|
31691
|
+
}
|
|
31692
|
+
//#endregion
|
|
31693
|
+
//#region src/hyperbolic-tree/gestures.ts
|
|
31694
|
+
const MIN_ZOOM = .5;
|
|
31695
|
+
/**
|
|
31696
|
+
* Keep the view center over the disk (so you can't zoom off into empty
|
|
31697
|
+
* space), and re-center entirely once the whole disk fits again.
|
|
31698
|
+
*/
|
|
31699
|
+
function clampZoomState(state) {
|
|
31700
|
+
const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom));
|
|
31701
|
+
if (zoom <= 1) return {
|
|
31702
|
+
zoom,
|
|
31703
|
+
offset: {
|
|
31704
|
+
x: 0,
|
|
31705
|
+
y: 0
|
|
31706
|
+
}
|
|
31707
|
+
};
|
|
31708
|
+
const limit = zoom - 1;
|
|
31709
|
+
const length = Math.hypot(state.offset.x, state.offset.y);
|
|
31710
|
+
return {
|
|
31711
|
+
zoom,
|
|
31712
|
+
offset: length > limit ? {
|
|
31713
|
+
x: state.offset.x * limit / length,
|
|
31714
|
+
y: state.offset.y * limit / length
|
|
31715
|
+
} : state.offset
|
|
31716
|
+
};
|
|
31717
|
+
}
|
|
31718
|
+
/**
|
|
31719
|
+
* Zoom by `factor` keeping the disk point under `screenPoint` fixed on
|
|
31720
|
+
* screen — the "zoom where I'm pointing" behaviour of maps and image viewers.
|
|
31721
|
+
*/
|
|
31722
|
+
function zoomAt(state, screenPoint, factor) {
|
|
31723
|
+
const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom * factor));
|
|
31724
|
+
const disk = screenToDisk(screenPoint, state);
|
|
31725
|
+
return clampZoomState({
|
|
31726
|
+
zoom,
|
|
31727
|
+
offset: {
|
|
31728
|
+
x: screenPoint.x - zoom * disk.x,
|
|
31729
|
+
y: screenPoint.y - zoom * disk.y
|
|
31730
|
+
}
|
|
31731
|
+
});
|
|
31732
|
+
}
|
|
31733
|
+
/**
|
|
31734
|
+
* A client-space position in screen units: disk radii from the SVG's center,
|
|
31735
|
+
* before zoom. The disk fills the SVG's smaller dimension, with the
|
|
31736
|
+
* viewBox's 5% margin.
|
|
31737
|
+
*/
|
|
31738
|
+
function clientToScreen(rect, clientX, clientY) {
|
|
31739
|
+
const unitsPerPixel = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
31740
|
+
return {
|
|
31741
|
+
x: (clientX - (rect.left + rect.width / 2)) * unitsPerPixel,
|
|
31742
|
+
y: (clientY - (rect.top + rect.height / 2)) * unitsPerPixel
|
|
31743
|
+
};
|
|
31744
|
+
}
|
|
31745
|
+
function screenToDisk(p, state) {
|
|
31746
|
+
const zoom = state.zoom || 1;
|
|
31747
|
+
return {
|
|
31748
|
+
x: (p.x - state.offset.x) / zoom,
|
|
31749
|
+
y: (p.y - state.offset.y) / zoom
|
|
31750
|
+
};
|
|
31751
|
+
}
|
|
31752
|
+
/** Is `event` on empty disk space (the SVG itself or the boundary circle), rather than a node? */
|
|
31753
|
+
function isBackground(event) {
|
|
31754
|
+
const target = event.target;
|
|
31755
|
+
return target === event.currentTarget || target?.classList.contains(`disk-boundary`) === true;
|
|
31756
|
+
}
|
|
31757
|
+
function createGestureHandlers(host) {
|
|
31758
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
31759
|
+
let pan;
|
|
31760
|
+
let pinch;
|
|
31761
|
+
const screenOf = (event) => {
|
|
31762
|
+
const rect = host.svgRect();
|
|
31763
|
+
return rect ? clientToScreen(rect, event.clientX, event.clientY) : void 0;
|
|
31764
|
+
};
|
|
31765
|
+
const diskOf = (screen) => clampToDisk(screenToDisk(screen, host.zoomState()), .98);
|
|
31766
|
+
const midpoint = (a, b) => ({
|
|
31767
|
+
x: (a.x + b.x) / 2,
|
|
31768
|
+
y: (a.y + b.y) / 2
|
|
31769
|
+
});
|
|
31770
|
+
const startPan = (pointerId, screen) => {
|
|
31771
|
+
pan = {
|
|
31772
|
+
pointerId,
|
|
31773
|
+
start: diskOf(screen),
|
|
31774
|
+
baseView: host.takeView()
|
|
31775
|
+
};
|
|
31776
|
+
};
|
|
31777
|
+
const startPinch = () => {
|
|
31778
|
+
const [[idA, a], [idB, b]] = [...pointers.entries()];
|
|
31779
|
+
pan = void 0;
|
|
31780
|
+
const { zoom } = host.zoomState();
|
|
31781
|
+
pinch = {
|
|
31782
|
+
ids: [idA, idB],
|
|
31783
|
+
startDistance: Math.max(1e-6, Math.hypot(a.x - b.x, a.y - b.y)),
|
|
31784
|
+
startZoom: zoom,
|
|
31785
|
+
anchor: screenToDisk(midpoint(a, b), host.zoomState())
|
|
31786
|
+
};
|
|
31787
|
+
};
|
|
31788
|
+
return {
|
|
31789
|
+
onPointerDown(event) {
|
|
31790
|
+
if (!isBackground(event)) return;
|
|
31791
|
+
host.focusTree();
|
|
31792
|
+
const screen = screenOf(event);
|
|
31793
|
+
if (!screen) return;
|
|
31794
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
31795
|
+
pointers.set(event.pointerId, screen);
|
|
31796
|
+
if (pointers.size === 2 && host.zoomGesturesEnabled()) startPinch();
|
|
31797
|
+
else if (pointers.size === 1) startPan(event.pointerId, screen);
|
|
31798
|
+
},
|
|
31799
|
+
onPointerMove(event) {
|
|
31800
|
+
if (!pointers.has(event.pointerId)) return;
|
|
31801
|
+
const screen = screenOf(event);
|
|
31802
|
+
if (!screen) return;
|
|
31803
|
+
pointers.set(event.pointerId, screen);
|
|
31804
|
+
if (pinch) {
|
|
31805
|
+
const a = pointers.get(pinch.ids[0]);
|
|
31806
|
+
const b = pointers.get(pinch.ids[1]);
|
|
31807
|
+
if (!a || !b) return;
|
|
31808
|
+
const zoom = pinch.startZoom * Math.hypot(a.x - b.x, a.y - b.y) / pinch.startDistance;
|
|
31809
|
+
const mid = midpoint(a, b);
|
|
31810
|
+
const clampedZoom = Math.min(8, Math.max(MIN_ZOOM, zoom));
|
|
31811
|
+
host.setZoomState(clampZoomState({
|
|
31812
|
+
zoom: clampedZoom,
|
|
31813
|
+
offset: {
|
|
31814
|
+
x: mid.x - clampedZoom * pinch.anchor.x,
|
|
31815
|
+
y: mid.y - clampedZoom * pinch.anchor.y
|
|
31816
|
+
}
|
|
31817
|
+
}));
|
|
31818
|
+
} else if (pan && pan.pointerId === event.pointerId) host.setView(panView(pan.baseView, pan.start, diskOf(screen)));
|
|
31819
|
+
},
|
|
31820
|
+
onPointerUp(event) {
|
|
31821
|
+
if (!pointers.delete(event.pointerId)) return;
|
|
31822
|
+
if (pinch) {
|
|
31823
|
+
pinch = void 0;
|
|
31824
|
+
const [remaining] = [...pointers.entries()];
|
|
31825
|
+
if (remaining) startPan(remaining[0], remaining[1]);
|
|
31826
|
+
} else if (pan?.pointerId === event.pointerId) pan = void 0;
|
|
31827
|
+
},
|
|
31828
|
+
/**
|
|
31829
|
+
* Ctrl/⌘-wheel — which is also what a trackpad pinch produces — always
|
|
31830
|
+
* zooms while the pointer is over the tree. A plain wheel only zooms
|
|
31831
|
+
* once the tree has keyboard focus (the library's focus-gated wheel
|
|
31832
|
+
* convention), so scrolling past it doesn't get hijacked.
|
|
31833
|
+
*/
|
|
31834
|
+
onWheel(event) {
|
|
31835
|
+
const modified = event.ctrlKey || event.metaKey;
|
|
31836
|
+
if (!host.zoomGesturesEnabled() || !modified && !host.hasFocus()) return;
|
|
31837
|
+
const screen = screenOf(event);
|
|
31838
|
+
if (!screen) return;
|
|
31839
|
+
event.preventDefault();
|
|
31840
|
+
const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
|
|
31841
|
+
const factor = Math.exp(-delta * (modified ? .01 : .002));
|
|
31842
|
+
host.setZoomState(zoomAt(host.zoomState(), screen, factor));
|
|
31843
|
+
},
|
|
31844
|
+
/** Forget any in-progress pan (e.g. when a focus change takes over the view). */
|
|
31845
|
+
cancel() {
|
|
31846
|
+
pan = void 0;
|
|
31847
|
+
pinch = void 0;
|
|
31848
|
+
pointers.clear();
|
|
31849
|
+
}
|
|
31850
|
+
};
|
|
31851
|
+
}
|
|
31852
|
+
//#endregion
|
|
31853
|
+
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
31854
|
+
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
31855
|
+
const hyperbolicTreeStyles = css`
|
|
31856
|
+
:host {
|
|
31857
|
+
--hyperbolictree-node-color: var(--surface-4);
|
|
31858
|
+
--hyperbolictree-node-text: var(--surface-3-text);
|
|
31859
|
+
--hyperbolictree-edge-color: var(--surface-6);
|
|
31860
|
+
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
31861
|
+
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
31862
|
+
--hyperbolictree-font-size: 13px;
|
|
31863
|
+
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
31864
|
+
--hyperbolictree-box-text: var(--surface-4-text);
|
|
31865
|
+
--hyperbolictree-box-border: var(--surface-2);
|
|
31866
|
+
--hyperbolictree-branch-hue-start: 250;
|
|
31867
|
+
--hyperbolictree-branch-chroma: 0.13;
|
|
31868
|
+
--hyperbolictree-branch-lightness: 62%;
|
|
31869
|
+
--hyperbolictree-branch-mix: 55%;
|
|
31870
|
+
|
|
31871
|
+
display: block;
|
|
31872
|
+
position: relative;
|
|
31873
|
+
overflow: hidden;
|
|
31874
|
+
outline: none;
|
|
31875
|
+
overscroll-behavior: contain;
|
|
31876
|
+
}
|
|
31877
|
+
|
|
31878
|
+
/*
|
|
31879
|
+
* delegatesFocus means the host itself reports as focused whenever
|
|
31880
|
+
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
31881
|
+
* the host also picks up the browser's own default focus ring/fill
|
|
31882
|
+
* sized to the whole box.
|
|
31883
|
+
*/
|
|
31884
|
+
:host(:focus),
|
|
31885
|
+
:host(:focus-visible) {
|
|
31886
|
+
outline: none;
|
|
31887
|
+
}
|
|
31888
|
+
|
|
31889
|
+
.hyperbolictree {
|
|
31890
|
+
display: block;
|
|
31891
|
+
width: 100%;
|
|
31892
|
+
height: 100%;
|
|
31893
|
+
outline: none;
|
|
31894
|
+
touch-action: none;
|
|
31895
|
+
overscroll-behavior: contain;
|
|
31896
|
+
}
|
|
31897
|
+
|
|
31898
|
+
.disk-boundary {
|
|
31899
|
+
fill: none;
|
|
31900
|
+
stroke: var(--hyperbolictree-boundary-color);
|
|
31901
|
+
stroke-width: 1;
|
|
31902
|
+
}
|
|
31903
|
+
|
|
31904
|
+
.edge {
|
|
31905
|
+
fill: none;
|
|
31906
|
+
stroke: var(--hyperbolictree-edge-color);
|
|
31907
|
+
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
31908
|
+
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
31909
|
+
/* Constant on-screen width, however far the disk is zoomed. */
|
|
31910
|
+
vector-effect: non-scaling-stroke;
|
|
31911
|
+
}
|
|
31912
|
+
|
|
31913
|
+
.node {
|
|
31914
|
+
cursor: pointer;
|
|
31915
|
+
}
|
|
31916
|
+
|
|
31917
|
+
/*
|
|
31918
|
+
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
31919
|
+
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
31920
|
+
* so it stays in keeping with — and legible in — the current theme.
|
|
31921
|
+
* Only sets the base colour vars, so selected/tickled states still win.
|
|
31922
|
+
*/
|
|
31923
|
+
:host([color-by-branch]) .node,
|
|
31924
|
+
:host([color-by-branch]) .edge {
|
|
31925
|
+
--_branch-color: oklch(
|
|
31926
|
+
var(--hyperbolictree-branch-lightness)
|
|
31927
|
+
var(--hyperbolictree-branch-chroma)
|
|
31928
|
+
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
31929
|
+
);
|
|
31930
|
+
}
|
|
31931
|
+
|
|
31932
|
+
:host([color-by-branch]) .node {
|
|
31933
|
+
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
31934
|
+
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
31935
|
+
}
|
|
31936
|
+
|
|
31937
|
+
:host([color-by-branch]) .edge {
|
|
31938
|
+
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
31939
|
+
}
|
|
31940
|
+
|
|
31941
|
+
.node-hit {
|
|
31942
|
+
fill: transparent;
|
|
31943
|
+
pointer-events: all;
|
|
31944
|
+
}
|
|
31945
|
+
|
|
31946
|
+
.node-dot {
|
|
31947
|
+
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
31948
|
+
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
31949
|
+
stroke: var(--surface-2);
|
|
31950
|
+
stroke-width: 1;
|
|
31951
|
+
pointer-events: none;
|
|
31952
|
+
}
|
|
31953
|
+
|
|
31954
|
+
.node-box {
|
|
31955
|
+
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
31956
|
+
stroke: var(--hyperbolictree-box-border);
|
|
31957
|
+
stroke-width: 1.5;
|
|
31958
|
+
/* Keep the border crisp however far the box has been scaled down. */
|
|
31959
|
+
vector-effect: non-scaling-stroke;
|
|
31960
|
+
}
|
|
31961
|
+
|
|
31962
|
+
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
31963
|
+
.node-box.marker {
|
|
31964
|
+
stroke-width: 1;
|
|
31965
|
+
}
|
|
31966
|
+
|
|
31967
|
+
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
31968
|
+
.hyperbolictree-label {
|
|
31969
|
+
fill: var(--hyperbolictree-node-text);
|
|
31970
|
+
cursor: pointer;
|
|
31971
|
+
user-select: none;
|
|
31972
|
+
}
|
|
31973
|
+
|
|
31974
|
+
.box-label {
|
|
31975
|
+
fill: var(--hyperbolictree-box-text);
|
|
31976
|
+
pointer-events: none;
|
|
31977
|
+
}
|
|
31978
|
+
|
|
31979
|
+
/*
|
|
31980
|
+
* State fills come paired with their theme text tokens: a box label sits
|
|
31981
|
+
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
31982
|
+
* --item-text-selected on top, not the resting box text colour.
|
|
31983
|
+
*/
|
|
31984
|
+
.node.tickled .node-dot,
|
|
31985
|
+
.node.tickled .node-box {
|
|
31986
|
+
fill: var(--item-bg-tickled);
|
|
31987
|
+
}
|
|
31988
|
+
|
|
31989
|
+
.node.tickled .box-label {
|
|
31990
|
+
fill: var(--item-text-tickled);
|
|
31991
|
+
}
|
|
31992
|
+
|
|
31993
|
+
.node.selected .node-dot,
|
|
31994
|
+
.node.selected .node-box {
|
|
31995
|
+
fill: var(--item-bg-selected);
|
|
31996
|
+
}
|
|
31997
|
+
|
|
31998
|
+
.node.selected .box-label {
|
|
31999
|
+
fill: var(--item-text-selected);
|
|
32000
|
+
}
|
|
32001
|
+
|
|
32002
|
+
.node.selected.tickled .node-dot,
|
|
32003
|
+
.node.selected.tickled .node-box {
|
|
32004
|
+
fill: var(--item-bg-selected-tickled);
|
|
32005
|
+
}
|
|
32006
|
+
|
|
32007
|
+
.node.focused .node-dot,
|
|
32008
|
+
.node.focused .node-box {
|
|
32009
|
+
stroke: var(--accent);
|
|
32010
|
+
stroke-width: 3;
|
|
32011
|
+
}
|
|
32012
|
+
|
|
32013
|
+
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
32014
|
+
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
32015
|
+
paint-order: stroke;
|
|
32016
|
+
stroke: var(--surface-2);
|
|
32017
|
+
stroke-width: 4px;
|
|
32018
|
+
stroke-linejoin: round;
|
|
32019
|
+
}
|
|
32020
|
+
|
|
32021
|
+
.node.filtered {
|
|
32022
|
+
opacity: 0.3;
|
|
32023
|
+
}
|
|
32024
|
+
|
|
32025
|
+
.node.loading .node-dot,
|
|
32026
|
+
.node.loading .node-box {
|
|
32027
|
+
opacity: 0.6;
|
|
32028
|
+
}
|
|
32029
|
+
|
|
32030
|
+
|
|
32031
|
+
/* Child preview: a clickable list of a crowded node's children. */
|
|
32032
|
+
.child-preview {
|
|
32033
|
+
position: absolute;
|
|
32034
|
+
z-index: 1;
|
|
32035
|
+
min-width: 8rem;
|
|
32036
|
+
max-width: min(18rem, 60%);
|
|
32037
|
+
max-height: 45%;
|
|
32038
|
+
overflow: auto;
|
|
32039
|
+
padding: var(--space-xs, 4px);
|
|
32040
|
+
background: var(--surface-3);
|
|
32041
|
+
color: var(--surface-3-text);
|
|
32042
|
+
border: var(--border-subtle);
|
|
32043
|
+
border-radius: var(--radius-m, 6px);
|
|
32044
|
+
box-shadow: var(--shadow-m);
|
|
32045
|
+
font-size: var(--hyperbolictree-font-size);
|
|
32046
|
+
transform: translate(-50%, 14px);
|
|
32047
|
+
}
|
|
32048
|
+
|
|
32049
|
+
.child-preview.above {
|
|
32050
|
+
transform: translate(-50%, calc(-100% - 14px));
|
|
32051
|
+
}
|
|
32052
|
+
|
|
32053
|
+
.child-preview-title {
|
|
32054
|
+
padding: 2px 6px 4px;
|
|
32055
|
+
font-weight: 600;
|
|
32056
|
+
overflow: hidden;
|
|
32057
|
+
text-overflow: ellipsis;
|
|
32058
|
+
white-space: nowrap;
|
|
32059
|
+
}
|
|
32060
|
+
|
|
32061
|
+
.child-preview ul {
|
|
32062
|
+
margin: 0;
|
|
32063
|
+
padding: 0;
|
|
32064
|
+
list-style: none;
|
|
32065
|
+
}
|
|
32066
|
+
|
|
32067
|
+
.child-preview button {
|
|
32068
|
+
display: block;
|
|
32069
|
+
width: 100%;
|
|
32070
|
+
padding: 2px 6px;
|
|
32071
|
+
border: none;
|
|
32072
|
+
border-radius: var(--radius-s, 4px);
|
|
32073
|
+
background: none;
|
|
32074
|
+
color: inherit;
|
|
32075
|
+
font: inherit;
|
|
32076
|
+
text-align: start;
|
|
32077
|
+
overflow: hidden;
|
|
32078
|
+
text-overflow: ellipsis;
|
|
32079
|
+
white-space: nowrap;
|
|
32080
|
+
cursor: pointer;
|
|
32081
|
+
}
|
|
32082
|
+
|
|
32083
|
+
.child-preview button:hover {
|
|
32084
|
+
background: var(--item-bg-tickled);
|
|
32085
|
+
color: var(--item-text-tickled);
|
|
32086
|
+
}
|
|
32087
|
+
|
|
32088
|
+
.child-preview-more {
|
|
32089
|
+
padding: 2px 6px;
|
|
32090
|
+
color: var(--surface-muted-text);
|
|
32091
|
+
}
|
|
32092
|
+
`;
|
|
31012
32093
|
//#endregion
|
|
31013
32094
|
//#region src/hyperbolic-tree/layout.ts
|
|
32095
|
+
const ID_SEPARATOR = `\u001F`;
|
|
32096
|
+
/**
|
|
32097
|
+
* Path-derived ids for `children` of the node with id `parentId`. `TreeItem.key`
|
|
32098
|
+
* is meant to be unique, but real data (e.g. a file system, where `readme.md`
|
|
32099
|
+
* recurs in many folders) often only has keys unique among siblings. Keying
|
|
32100
|
+
* the layout on `item.key` alone would silently merge those nodes — edges get
|
|
32101
|
+
* attached to the wrong parent and clicks focus the wrong node — so the
|
|
32102
|
+
* layout identifies nodes by their key path instead. Repeated keys among
|
|
32103
|
+
* siblings are disambiguated by occurrence count. The root's id is its key.
|
|
32104
|
+
*/
|
|
32105
|
+
function hyperbolicChildIds(parentId, children) {
|
|
32106
|
+
const seen = /* @__PURE__ */ new Map();
|
|
32107
|
+
return children.map((child) => {
|
|
32108
|
+
const key = child.item.key;
|
|
32109
|
+
const count = seen.get(key) ?? 0;
|
|
32110
|
+
seen.set(key, count + 1);
|
|
32111
|
+
return `${parentId}${ID_SEPARATOR}${count === 0 ? key : `${key}#${count}`}`;
|
|
32112
|
+
});
|
|
32113
|
+
}
|
|
32114
|
+
/** Depth (root is 0) of the node with layout id `id`. */
|
|
32115
|
+
function hyperbolicIdDepth(id) {
|
|
32116
|
+
return id.split(ID_SEPARATOR).length - 1;
|
|
32117
|
+
}
|
|
32118
|
+
/**
|
|
32119
|
+
* Layout id of the root's child that `id` descends from (or is) — i.e. which
|
|
32120
|
+
* top-level branch it belongs to. `undefined` for the root itself.
|
|
32121
|
+
*/
|
|
32122
|
+
function hyperbolicBranchId(id) {
|
|
32123
|
+
const parts = id.split(ID_SEPARATOR);
|
|
32124
|
+
return parts.length < 2 ? void 0 : `${parts[0]}${ID_SEPARATOR}${parts[1]}`;
|
|
32125
|
+
}
|
|
32126
|
+
/** Whether layout id `id` is a strict descendant of `ancestorId`. */
|
|
32127
|
+
function isHyperbolicDescendantId(id, ancestorId) {
|
|
32128
|
+
return id.startsWith(`${ancestorId}${ID_SEPARATOR}`);
|
|
32129
|
+
}
|
|
32130
|
+
/** Chain of nodes from `root` down to the node with layout id `id`, or `undefined` if absent. */
|
|
32131
|
+
function findHyperbolicPathById(root, id) {
|
|
32132
|
+
if (!root) return void 0;
|
|
32133
|
+
const walk = (node, nodeId) => {
|
|
32134
|
+
if (nodeId === id) return [node];
|
|
32135
|
+
if (!isHyperbolicDescendantId(id, nodeId)) return void 0;
|
|
32136
|
+
const children = node.children ?? [];
|
|
32137
|
+
const ids = hyperbolicChildIds(nodeId, children);
|
|
32138
|
+
for (let i = 0; i < children.length; i++) {
|
|
32139
|
+
const hit = walk(children[i], ids[i]);
|
|
32140
|
+
if (hit) return [node, ...hit];
|
|
32141
|
+
}
|
|
32142
|
+
};
|
|
32143
|
+
return walk(root, root.item.key);
|
|
32144
|
+
}
|
|
32145
|
+
/** Layout id of `target` (matched by object identity) within `root`, or `undefined` if absent. */
|
|
32146
|
+
function findHyperbolicIdOf(root, target) {
|
|
32147
|
+
if (!root) return void 0;
|
|
32148
|
+
const walk = (node, nodeId) => {
|
|
32149
|
+
if (node === target) return nodeId;
|
|
32150
|
+
const children = node.children ?? [];
|
|
32151
|
+
const ids = hyperbolicChildIds(nodeId, children);
|
|
32152
|
+
for (let i = 0; i < children.length; i++) {
|
|
32153
|
+
const hit = walk(children[i], ids[i]);
|
|
32154
|
+
if (hit !== void 0) return hit;
|
|
32155
|
+
}
|
|
32156
|
+
};
|
|
32157
|
+
return walk(root, root.item.key);
|
|
32158
|
+
}
|
|
32159
|
+
/** Key path (from the root's children) to the first node, depth-first, satisfying `predicate`. */
|
|
32160
|
+
function findHyperbolicKeyPath(nodes, predicate, prefix = []) {
|
|
32161
|
+
for (const node of nodes) {
|
|
32162
|
+
const path = [...prefix, node.item.key];
|
|
32163
|
+
if (predicate(node)) return path;
|
|
32164
|
+
const hit = node.children?.length ? findHyperbolicKeyPath(node.children, predicate, path) : void 0;
|
|
32165
|
+
if (hit) return hit;
|
|
32166
|
+
}
|
|
32167
|
+
}
|
|
31014
32168
|
/** Depth-first search for the node with `key`. Returns `undefined` when absent. */
|
|
31015
32169
|
function findHyperbolicNode(root, key) {
|
|
31016
32170
|
if (!root) return void 0;
|
|
@@ -31085,21 +32239,58 @@ function stableUnitJitter(key) {
|
|
|
31085
32239
|
* radially).
|
|
31086
32240
|
*/
|
|
31087
32241
|
const CHAIN_JITTER_RADIANS = .22;
|
|
32242
|
+
/** Default minimum hyperbolic distance between adjacent siblings. See {@link childDistance}. */
|
|
32243
|
+
const DEFAULT_SIBLING_SPACING = .5;
|
|
32244
|
+
/**
|
|
32245
|
+
* Hyperbolic distance from parent to a child whose angular slice (in the
|
|
32246
|
+
* parent's frame) is `share`, such that the child sits at least `spacing`
|
|
32247
|
+
* from a neighbour one slice-width away: from the hyperbolic law of cosines
|
|
32248
|
+
* for an isosceles triangle, `sinh(d/2) = sinh(L)·sin(Δθ/2)`, solved for L.
|
|
32249
|
+
* Never less than `minimum` (the `curvature`), and capped so a sliver-thin
|
|
32250
|
+
* slice can't fling a child off to infinity.
|
|
32251
|
+
*
|
|
32252
|
+
* This is what makes panning *useful* in a crowded fan: panning is an
|
|
32253
|
+
* isometry — it can't change the hyperbolic distance between siblings, only
|
|
32254
|
+
* move them to where the disk magnifies more. If siblings are laid out
|
|
32255
|
+
* nearly on top of each other in hyperbolic terms, no amount of dragging
|
|
32256
|
+
* separates them; guaranteeing a minimum spacing means dragging a crowd
|
|
32257
|
+
* towards the center always fans it out.
|
|
32258
|
+
*/
|
|
32259
|
+
function childDistance(share, spacing, minimum) {
|
|
32260
|
+
if (spacing <= 0) return minimum;
|
|
32261
|
+
const halfAngle = Math.min(share, Math.PI) / 2;
|
|
32262
|
+
const needed = Math.asinh(Math.sinh(spacing / 2) / Math.max(Math.sin(halfAngle), 1e-6));
|
|
32263
|
+
return Math.min(Math.max(minimum, needed), minimum + 6);
|
|
32264
|
+
}
|
|
31088
32265
|
/**
|
|
31089
32266
|
* Canonical (true-root-anchored) hyperbolic position of every currently-loaded
|
|
31090
|
-
* node, keyed by
|
|
32267
|
+
* node, keyed by layout id (see {@link hyperbolicChildIds}). Positions never depend on the current focus —
|
|
31091
32268
|
* refocusing is purely a Möbius re-projection of these fixed coordinates, so
|
|
31092
32269
|
* the tree never needs to be re-laid-out when the focus changes.
|
|
31093
32270
|
*
|
|
31094
32271
|
* Each node holds an angular wedge — inherited from its parent, expressed in
|
|
31095
32272
|
* its *own* local frame (root starts with the full `[-π, π]`) — across which
|
|
31096
|
-
* its children are placed at hyperbolic distance
|
|
31097
|
-
*
|
|
32273
|
+
* its children are placed at hyperbolic distance of at least `curvature`
|
|
32274
|
+
* (default `1`) — further for a thin slice, per `siblingSpacing` (see
|
|
32275
|
+
* {@link childDistance}).
|
|
32276
|
+
* That wedge is partitioned among the children, weighted by √ of each child's own
|
|
31098
32277
|
* subtree size rather than split evenly, and each child's own share is
|
|
31099
32278
|
* carried into its local frame via {@link boundaryAngleInLocalFrame} before
|
|
31100
|
-
* recursing. This is a strict partition
|
|
31101
|
-
*
|
|
31102
|
-
*
|
|
32279
|
+
* recursing. This is a strict partition, and each child's wedge region
|
|
32280
|
+
* (bounded by geodesic rays to the same two ideal boundary points as its
|
|
32281
|
+
* slice) nests inside its parent's, which is what guarantees sibling
|
|
32282
|
+
* subtrees never spatially cross regardless of how deep or bushy they are.
|
|
32283
|
+
*
|
|
32284
|
+
* Every node's local frame is tracked as a full {@link DiskIsometry}
|
|
32285
|
+
* (local → canonical), built by composing its parent's frame with the
|
|
32286
|
+
* translation to the child. Composing Möbius translations introduces a
|
|
32287
|
+
* rotation (gyration); dropping it — i.e. assuming a node's frame is simply
|
|
32288
|
+
* `mobiusToOrigin(·, position)` — makes every wedge from depth 3 onward
|
|
32289
|
+
* point in a subtly wrong direction, so subtrees cross each other.
|
|
32290
|
+
*
|
|
32291
|
+
* A wedge wider than π is narrowed to π (around the same bisector) before
|
|
32292
|
+
* the child recurses into it: a near-full-circle wedge would otherwise fan
|
|
32293
|
+
* that child's own children back around towards — and across — its parent.
|
|
31103
32294
|
*
|
|
31104
32295
|
* `curvature` scales how far apart (in hyperbolic distance) each generation
|
|
31105
32296
|
* sits. Raising it pushes each generation further out, so the Poincaré
|
|
@@ -31108,7 +32299,7 @@ const CHAIN_JITTER_RADIANS = .22;
|
|
|
31108
32299
|
* in" on the focus. Lowering it spreads generations out more evenly,
|
|
31109
32300
|
* flattening the view toward a Euclidean-feeling layout.
|
|
31110
32301
|
*/
|
|
31111
|
-
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
32302
|
+
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING) {
|
|
31112
32303
|
const map = /* @__PURE__ */ new Map();
|
|
31113
32304
|
if (!root) return map;
|
|
31114
32305
|
const origin = {
|
|
@@ -31117,70 +32308,122 @@ function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
|
31117
32308
|
};
|
|
31118
32309
|
map.set(root.item.key, {
|
|
31119
32310
|
node: root,
|
|
32311
|
+
id: root.item.key,
|
|
31120
32312
|
depth: 0,
|
|
31121
32313
|
parentKey: void 0,
|
|
32314
|
+
parentId: void 0,
|
|
31122
32315
|
position: origin,
|
|
32316
|
+
toParent: identityIsometry,
|
|
31123
32317
|
angularShare: 2 * Math.PI
|
|
31124
32318
|
});
|
|
31125
|
-
const place = (node,
|
|
32319
|
+
const place = (node, id, frame, wedgeStart, wedgeEnd, depth) => {
|
|
31126
32320
|
if (depth >= maxDepth) return;
|
|
31127
32321
|
const children = node.children ?? [];
|
|
31128
32322
|
const n = children.length;
|
|
31129
32323
|
if (n === 0) return;
|
|
31130
|
-
const r0 = euclideanRadiusForHyperbolicDistance(curvature);
|
|
31131
32324
|
const angularWidth = wedgeEnd - wedgeStart;
|
|
31132
|
-
const weights = children.map((child) => subtreeSize(child, maxDepth - depth - 1));
|
|
32325
|
+
const weights = children.map((child) => Math.sqrt(subtreeSize(child, maxDepth - depth - 1)));
|
|
31133
32326
|
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
32327
|
+
const ids = hyperbolicChildIds(id, children);
|
|
31134
32328
|
let cursor = wedgeStart;
|
|
31135
32329
|
children.forEach((child, index) => {
|
|
31136
32330
|
const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
|
|
31137
|
-
const
|
|
31138
|
-
|
|
31139
|
-
|
|
31140
|
-
const
|
|
32331
|
+
const bisector = cursor + share / 2;
|
|
32332
|
+
cursor += share;
|
|
32333
|
+
const theta = bisector + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
|
|
32334
|
+
const r = euclideanRadiusForHyperbolicDistance(n === 1 ? curvature : childDistance(share, siblingSpacing, curvature));
|
|
31141
32335
|
const local = {
|
|
31142
|
-
x:
|
|
31143
|
-
y:
|
|
32336
|
+
x: r * Math.cos(theta),
|
|
32337
|
+
y: r * Math.sin(theta)
|
|
31144
32338
|
};
|
|
31145
|
-
const
|
|
31146
|
-
|
|
32339
|
+
const toParent = translationFromOrigin(local);
|
|
32340
|
+
const childFrame = composeIsometry(frame, toParent);
|
|
32341
|
+
map.set(ids[index], {
|
|
31147
32342
|
node: child,
|
|
32343
|
+
id: ids[index],
|
|
31148
32344
|
depth: depth + 1,
|
|
31149
32345
|
parentKey: node.item.key,
|
|
31150
|
-
|
|
32346
|
+
parentId: id,
|
|
32347
|
+
position: applyIsometry(childFrame, origin),
|
|
32348
|
+
toParent,
|
|
31151
32349
|
angularShare: share
|
|
31152
32350
|
});
|
|
31153
|
-
const
|
|
31154
|
-
|
|
32351
|
+
const halfWedge = Math.min(share, Math.PI) / 2;
|
|
32352
|
+
const localStart = boundaryAngleInLocalFrame(bisector - halfWedge, local);
|
|
32353
|
+
let localWidth = boundaryAngleInLocalFrame(bisector + halfWedge, local) - localStart;
|
|
31155
32354
|
if (localWidth <= 0) localWidth += 2 * Math.PI;
|
|
31156
|
-
place(child,
|
|
32355
|
+
place(child, ids[index], childFrame, localStart, localStart + localWidth, depth + 1);
|
|
31157
32356
|
});
|
|
31158
32357
|
};
|
|
31159
|
-
place(root,
|
|
32358
|
+
place(root, root.item.key, identityIsometry, -Math.PI, Math.PI, 0);
|
|
31160
32359
|
return map;
|
|
31161
32360
|
}
|
|
31162
|
-
/**
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
|
|
31167
|
-
|
|
31168
|
-
|
|
32361
|
+
/**
|
|
32362
|
+
* Every node within `maxRadius` tree-graph steps of `focusedId`, with its
|
|
32363
|
+
* frame relative to the focus. Built by walking outward from the focus and
|
|
32364
|
+
* composing one short {@link CanonicalEntry.toParent} hop per edge — so
|
|
32365
|
+
* precision depends only on distance *from the focus*, never on how far the
|
|
32366
|
+
* focus is from the root.
|
|
32367
|
+
*/
|
|
32368
|
+
function focusFrames(entries, focusedId, maxRadius) {
|
|
32369
|
+
const frames = /* @__PURE__ */ new Map();
|
|
32370
|
+
if (!entries.has(focusedId)) return frames;
|
|
32371
|
+
frames.set(focusedId, {
|
|
32372
|
+
ring: 0,
|
|
32373
|
+
frame: identityIsometry
|
|
32374
|
+
});
|
|
32375
|
+
let frontier = [focusedId];
|
|
32376
|
+
for (let ring = 1; ring <= maxRadius && frontier.length > 0; ring++) {
|
|
31169
32377
|
const next = [];
|
|
31170
|
-
for (const
|
|
31171
|
-
const entry =
|
|
31172
|
-
|
|
31173
|
-
const
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31178
|
-
|
|
31179
|
-
|
|
32378
|
+
for (const id of frontier) {
|
|
32379
|
+
const entry = entries.get(id);
|
|
32380
|
+
const { frame } = frames.get(id);
|
|
32381
|
+
const visit = (neighbourId, neighbourFrame) => {
|
|
32382
|
+
if (frames.has(neighbourId) || !entries.has(neighbourId)) return;
|
|
32383
|
+
frames.set(neighbourId, {
|
|
32384
|
+
ring,
|
|
32385
|
+
frame: neighbourFrame()
|
|
32386
|
+
});
|
|
32387
|
+
next.push(neighbourId);
|
|
32388
|
+
};
|
|
32389
|
+
for (const childId of hyperbolicChildIds(id, entry.node.children ?? [])) visit(childId, () => composeIsometry(frame, entries.get(childId).toParent));
|
|
32390
|
+
if (entry.parentId !== void 0) visit(entry.parentId, () => composeIsometry(frame, invertIsometry(entry.toParent)));
|
|
31180
32391
|
}
|
|
31181
32392
|
frontier = next;
|
|
31182
32393
|
}
|
|
31183
|
-
return
|
|
32394
|
+
return frames;
|
|
32395
|
+
}
|
|
32396
|
+
/** `toParent` hops composed from `id` up to (not including) ancestor `ancestorId`: `id`-local → `ancestorId`-local. */
|
|
32397
|
+
function frameUpTo(entries, id, ancestorId) {
|
|
32398
|
+
let frame = identityIsometry;
|
|
32399
|
+
for (let cursor = id; cursor !== void 0 && cursor !== ancestorId; cursor = entries.get(cursor)?.parentId) frame = composeIsometry(entries.get(cursor).toParent, frame);
|
|
32400
|
+
return frame;
|
|
32401
|
+
}
|
|
32402
|
+
/**
|
|
32403
|
+
* The frame of node `toId` expressed in node `fromId`'s local frame (maps
|
|
32404
|
+
* `toId`-local coordinates to `fromId`-local ones), composed along the tree
|
|
32405
|
+
* path between them via their lowest common ancestor. Used to hand a view
|
|
32406
|
+
* over from one focus to another without passing through root-anchored
|
|
32407
|
+
* coordinates. `undefined` if either id is absent.
|
|
32408
|
+
*/
|
|
32409
|
+
function hyperbolicRelativeFrame(entries, fromId, toId) {
|
|
32410
|
+
if (!entries.has(fromId) || !entries.has(toId)) return void 0;
|
|
32411
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
32412
|
+
for (let cursor = fromId; cursor !== void 0; cursor = entries.get(cursor)?.parentId) ancestors.add(cursor);
|
|
32413
|
+
let lca = toId;
|
|
32414
|
+
while (lca !== void 0 && !ancestors.has(lca)) lca = entries.get(lca)?.parentId;
|
|
32415
|
+
if (lca === void 0) return void 0;
|
|
32416
|
+
return composeIsometry(invertIsometry(frameUpTo(entries, fromId, lca)), frameUpTo(entries, toId, lca));
|
|
32417
|
+
}
|
|
32418
|
+
/**
|
|
32419
|
+
* Resolve `focusedKey` — either a layout id, or (as a convenience, and for
|
|
32420
|
+
* data with globally-unique keys) a plain `item.key`, matching the first
|
|
32421
|
+
* node in depth-first order carrying it.
|
|
32422
|
+
*/
|
|
32423
|
+
function resolveFocusId(canonical, focusedKey) {
|
|
32424
|
+
if (focusedKey === void 0) return void 0;
|
|
32425
|
+
if (canonical.has(focusedKey)) return focusedKey;
|
|
32426
|
+
for (const entry of canonical.values()) if (entry.node.item.key === focusedKey) return entry.id;
|
|
31184
32427
|
}
|
|
31185
32428
|
/**
|
|
31186
32429
|
* Compute the visible nodes/edges of a hyperbolic tree view: every
|
|
@@ -31188,58 +32431,69 @@ function ringDistances(canonical, focusedKey, maxRadius) {
|
|
|
31188
32431
|
* node, projected onto the Poincaré disk with the focus at its center.
|
|
31189
32432
|
*
|
|
31190
32433
|
* @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
|
-
*
|
|
32434
|
+
* @param options `focusedKey` (a layout id or plain `item.key`; default the
|
|
32435
|
+
* true root; unknown keys fall back to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
32436
|
+
* cap on layout recursion depth, default 24), `curvature`,
|
|
32437
|
+
* `siblingSpacing`, `centerOverride` (put this point of the focus's local
|
|
32438
|
+
* frame at the center instead of the focus itself), `view` (a full
|
|
32439
|
+
* focus-local → screen isometry, taking precedence over `centerOverride` —
|
|
32440
|
+
* used for panning and animation, which also rotate).
|
|
31196
32441
|
*/
|
|
31197
32442
|
function computeHyperbolicLayout(root, options = {}) {
|
|
31198
32443
|
if (!root) return {
|
|
31199
32444
|
nodes: [],
|
|
31200
32445
|
edges: []
|
|
31201
32446
|
};
|
|
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 {
|
|
32447
|
+
const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING } = options;
|
|
32448
|
+
const canonical = computeCanonicalPositions(root, maxDepth, curvature, siblingSpacing);
|
|
32449
|
+
const focusedId = resolveFocusId(canonical, options.focusedKey) ?? root.item.key;
|
|
32450
|
+
if (!canonical.has(focusedId)) return {
|
|
31207
32451
|
nodes: [],
|
|
31208
32452
|
edges: []
|
|
31209
32453
|
};
|
|
31210
|
-
const
|
|
31211
|
-
|
|
31212
|
-
|
|
32454
|
+
const view = options.view ?? viewIsometry(options.centerOverride ?? {
|
|
32455
|
+
x: 0,
|
|
32456
|
+
y: 0
|
|
32457
|
+
});
|
|
32458
|
+
const frames = focusFrames(canonical, focusedId, maxRadius);
|
|
32459
|
+
const rawWeights = [...frames.keys()].map((key) => canonical.get(key).node.item.weight).filter((w) => typeof w === `number`);
|
|
31213
32460
|
const minWeight = rawWeights.length > 0 ? Math.min(...rawWeights) : void 0;
|
|
31214
32461
|
const maxWeight = rawWeights.length > 0 ? Math.max(...rawWeights) : void 0;
|
|
31215
32462
|
const normalizeWeight = (weight) => {
|
|
31216
32463
|
if (weight === void 0 || minWeight === void 0 || maxWeight === void 0 || maxWeight === minWeight) return .5;
|
|
31217
32464
|
return (weight - minWeight) / (maxWeight - minWeight);
|
|
31218
32465
|
};
|
|
31219
|
-
const nodes = [...
|
|
31220
|
-
const entry = canonical.get(
|
|
32466
|
+
const nodes = [...frames.entries()].map(([id, { ring, frame }]) => {
|
|
32467
|
+
const entry = canonical.get(id);
|
|
31221
32468
|
const weight = entry.node.item.weight;
|
|
31222
32469
|
return {
|
|
31223
32470
|
node: entry.node,
|
|
31224
|
-
key,
|
|
32471
|
+
key: entry.node.item.key,
|
|
32472
|
+
id,
|
|
31225
32473
|
depth: entry.depth,
|
|
31226
|
-
ring
|
|
32474
|
+
ring,
|
|
31227
32475
|
parentKey: entry.parentKey,
|
|
31228
|
-
|
|
32476
|
+
parentId: entry.parentId,
|
|
32477
|
+
position: applyIsometry(composeIsometry(view, frame), {
|
|
32478
|
+
x: 0,
|
|
32479
|
+
y: 0
|
|
32480
|
+
}),
|
|
31229
32481
|
angularShare: entry.angularShare,
|
|
31230
32482
|
weight,
|
|
31231
32483
|
weightNormalized: normalizeWeight(weight)
|
|
31232
32484
|
};
|
|
31233
32485
|
}).sort((a, b) => a.ring - b.ring);
|
|
31234
|
-
const
|
|
32486
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
31235
32487
|
const edges = [];
|
|
31236
32488
|
for (const n of nodes) {
|
|
31237
|
-
if (n.
|
|
31238
|
-
const parent =
|
|
32489
|
+
if (n.parentId === void 0) continue;
|
|
32490
|
+
const parent = byId.get(n.parentId);
|
|
31239
32491
|
if (!parent) continue;
|
|
31240
32492
|
edges.push({
|
|
31241
32493
|
fromKey: parent.key,
|
|
31242
32494
|
toKey: n.key,
|
|
32495
|
+
fromId: parent.id,
|
|
32496
|
+
toId: n.id,
|
|
31243
32497
|
from: parent.position,
|
|
31244
32498
|
to: n.position,
|
|
31245
32499
|
weightNormalized: (parent.weightNormalized + n.weightNormalized) / 2
|
|
@@ -31251,17 +32505,476 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
31251
32505
|
};
|
|
31252
32506
|
}
|
|
31253
32507
|
//#endregion
|
|
31254
|
-
//#region src/hyperbolic-tree/
|
|
32508
|
+
//#region src/hyperbolic-tree/keyboard.ts
|
|
32509
|
+
const unhandled = {
|
|
32510
|
+
action: { kind: `none` },
|
|
32511
|
+
handled: false
|
|
32512
|
+
};
|
|
32513
|
+
/** Angular distance (radians, 0–π) of a layout node from screen-"up" (−π/2). */
|
|
32514
|
+
function angleDistanceFromUp(n) {
|
|
32515
|
+
const angle = Math.atan2(n.position.y, n.position.x);
|
|
32516
|
+
let diff = Math.abs(angle - -Math.PI / 2);
|
|
32517
|
+
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
|
32518
|
+
return diff;
|
|
32519
|
+
}
|
|
32520
|
+
/**
|
|
32521
|
+
* Resolve `key` against the focused node (layout id `focusId`):
|
|
32522
|
+
* - `ArrowUp` — the visible child closest to screen-"up"
|
|
32523
|
+
* - `ArrowDown` — the parent
|
|
32524
|
+
* - `ArrowLeft` / `ArrowRight` — previous / next sibling, wrapping
|
|
32525
|
+
* - `Enter` — activate the focus
|
|
32526
|
+
* - `Escape` — back to the root (unhandled when already there)
|
|
32527
|
+
*/
|
|
32528
|
+
function resolveKeyboardAction(key, root, focusId, visibleNodes) {
|
|
32529
|
+
const path = findHyperbolicPathById(root, focusId);
|
|
32530
|
+
const focusNode = path?.at(-1);
|
|
32531
|
+
if (!path || !focusNode) return unhandled;
|
|
32532
|
+
const parent = path.length >= 2 ? path[path.length - 2] : void 0;
|
|
32533
|
+
const none = {
|
|
32534
|
+
action: { kind: `none` },
|
|
32535
|
+
handled: true
|
|
32536
|
+
};
|
|
32537
|
+
const focus = (node) => node ? {
|
|
32538
|
+
action: {
|
|
32539
|
+
kind: `focus`,
|
|
32540
|
+
node
|
|
32541
|
+
},
|
|
32542
|
+
handled: true
|
|
32543
|
+
} : none;
|
|
32544
|
+
switch (key) {
|
|
32545
|
+
case `ArrowUp`: {
|
|
32546
|
+
const children = visibleNodes.filter((n) => n.parentId === focusId);
|
|
32547
|
+
if (children.length === 0) return none;
|
|
32548
|
+
return focus(children.reduce((a, b) => angleDistanceFromUp(a) < angleDistanceFromUp(b) ? a : b).node);
|
|
32549
|
+
}
|
|
32550
|
+
case `ArrowDown`: return focus(parent);
|
|
32551
|
+
case `ArrowLeft`:
|
|
32552
|
+
case `ArrowRight`: {
|
|
32553
|
+
const siblings = parent?.children ?? [];
|
|
32554
|
+
const index = siblings.indexOf(focusNode);
|
|
32555
|
+
if (index === -1 || siblings.length <= 1) return none;
|
|
32556
|
+
return focus(siblings[(index + (key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length]);
|
|
32557
|
+
}
|
|
32558
|
+
case `Enter`: return {
|
|
32559
|
+
action: {
|
|
32560
|
+
kind: `activate`,
|
|
32561
|
+
node: focusNode,
|
|
32562
|
+
depth: path.length - 1
|
|
32563
|
+
},
|
|
32564
|
+
handled: true
|
|
32565
|
+
};
|
|
32566
|
+
case `Escape`: return focusNode === root ? unhandled : focus(root);
|
|
32567
|
+
default: return unhandled;
|
|
32568
|
+
}
|
|
32569
|
+
}
|
|
32570
|
+
//#endregion
|
|
32571
|
+
//#region src/hyperbolic-tree/lazy-children.ts
|
|
32572
|
+
function createLazyChildLoader(source, onChange) {
|
|
32573
|
+
const pending = /* @__PURE__ */ new Set();
|
|
32574
|
+
const aborters = /* @__PURE__ */ new Map();
|
|
32575
|
+
const fetchChildren = async (node, depth, signal) => {
|
|
32576
|
+
const { model, loadChildren } = source();
|
|
32577
|
+
if (model) return model.getChildren(node, depth, signal);
|
|
32578
|
+
return [...await collectDataResult(loadChildren({
|
|
32579
|
+
node,
|
|
32580
|
+
depth
|
|
32581
|
+
}, signal), signal)];
|
|
32582
|
+
};
|
|
32583
|
+
const load = async (node, depth) => {
|
|
32584
|
+
pending.add(node);
|
|
32585
|
+
const controller = new AbortController();
|
|
32586
|
+
aborters.set(node, controller);
|
|
32587
|
+
onChange();
|
|
32588
|
+
try {
|
|
32589
|
+
const children = await fetchChildren(node, depth, controller.signal);
|
|
32590
|
+
if (!controller.signal.aborted) node.children = children;
|
|
32591
|
+
} catch {
|
|
32592
|
+
if (!controller.signal.aborted) node.children = [];
|
|
32593
|
+
} finally {
|
|
32594
|
+
pending.delete(node);
|
|
32595
|
+
aborters.delete(node);
|
|
32596
|
+
if (!controller.signal.aborted) onChange();
|
|
32597
|
+
}
|
|
32598
|
+
};
|
|
32599
|
+
return {
|
|
32600
|
+
/** Start loading children for every node in `visible` that has none yet. */
|
|
32601
|
+
ensure(visible) {
|
|
32602
|
+
const { model, loadChildren } = source();
|
|
32603
|
+
if (!model && !loadChildren) return;
|
|
32604
|
+
for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
32605
|
+
},
|
|
32606
|
+
isPending: (node) => pending.has(node),
|
|
32607
|
+
abortAll() {
|
|
32608
|
+
for (const aborter of aborters.values()) aborter.abort();
|
|
32609
|
+
aborters.clear();
|
|
32610
|
+
pending.clear();
|
|
32611
|
+
}
|
|
32612
|
+
};
|
|
32613
|
+
}
|
|
32614
|
+
//#endregion
|
|
32615
|
+
//#region src/hyperbolic-tree/render-utils.ts
|
|
32616
|
+
/**
|
|
32617
|
+
* The Poincaré disk's conformal factor relative to the origin, `1 − |p|²`:
|
|
32618
|
+
* how much a shape of fixed hyperbolic size shrinks (in Euclidean terms) when
|
|
32619
|
+
* moved from the center to `p`. Sizing nodes by this makes them shrink
|
|
32620
|
+
* exactly as the space does, rather than by an ad-hoc falloff.
|
|
32621
|
+
*/
|
|
32622
|
+
function conformalScale(p) {
|
|
32623
|
+
return Math.max(0, 1 - (p.x * p.x + p.y * p.y));
|
|
32624
|
+
}
|
|
32625
|
+
/**
|
|
32626
|
+
* Euclidean distance from each node to its nearest visible neighbour, keyed
|
|
32627
|
+
* by layout id (`Infinity` for a lone node). Used to cap on-screen node size
|
|
32628
|
+
* so crowded nodes never overlap — measured in the *projected* view, so it
|
|
32629
|
+
* stays right whichever node is focused. O(n²), fine for the few hundred
|
|
32630
|
+
* nodes a `maxRadius` window typically holds.
|
|
32631
|
+
*/
|
|
32632
|
+
function nearestNeighbourDistances(nodes) {
|
|
32633
|
+
const out = /* @__PURE__ */ new Map();
|
|
32634
|
+
for (const n of nodes) out.set(n.id, Number.POSITIVE_INFINITY);
|
|
32635
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
32636
|
+
const a = nodes[i].position;
|
|
32637
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
32638
|
+
const b = nodes[j].position;
|
|
32639
|
+
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
32640
|
+
if (d < out.get(nodes[i].id)) out.set(nodes[i].id, d);
|
|
32641
|
+
if (d < out.get(nodes[j].id)) out.set(nodes[j].id, d);
|
|
32642
|
+
}
|
|
32643
|
+
}
|
|
32644
|
+
return out;
|
|
32645
|
+
}
|
|
32646
|
+
/** Weight multiplier for `n`, interpolating between the min/max weight scales. */
|
|
32647
|
+
function weightScale(n, options) {
|
|
32648
|
+
return options.minWeightScale + (options.maxWeightScale - options.minWeightScale) * n.weightNormalized;
|
|
32649
|
+
}
|
|
32650
|
+
/**
|
|
32651
|
+
* A dot's rendered radius: base size × weight × conformal shrink, capped to
|
|
32652
|
+
* under half the distance to its nearest neighbour so crowded siblings keep
|
|
32653
|
+
* a visible gap instead of merging into a blob.
|
|
32654
|
+
*/
|
|
32655
|
+
function dotRadius(n, nearest, options) {
|
|
32656
|
+
const radius = options.nodeRadius * conformalScale(n.position) * weightScale(n, options);
|
|
32657
|
+
return Math.max(Math.min(radius, nearest * .45), .003);
|
|
32658
|
+
}
|
|
32659
|
+
/**
|
|
32660
|
+
* Footprint of a `2·halfWidth × 2·halfHeight` rectangle centered on
|
|
32661
|
+
* `center` and rotated by `angle` radians.
|
|
32662
|
+
*/
|
|
32663
|
+
function orientedRect(id, center, halfWidth, halfHeight, angle = 0) {
|
|
32664
|
+
const cos = Math.cos(angle);
|
|
32665
|
+
const sin = Math.sin(angle);
|
|
32666
|
+
const corner = (u, v) => ({
|
|
32667
|
+
x: center.x + u * cos - v * sin,
|
|
32668
|
+
y: center.y + u * sin + v * cos
|
|
32669
|
+
});
|
|
32670
|
+
return {
|
|
32671
|
+
id,
|
|
32672
|
+
corners: [
|
|
32673
|
+
corner(-halfWidth, -halfHeight),
|
|
32674
|
+
corner(halfWidth, -halfHeight),
|
|
32675
|
+
corner(halfWidth, halfHeight),
|
|
32676
|
+
corner(-halfWidth, halfHeight)
|
|
32677
|
+
]
|
|
32678
|
+
};
|
|
32679
|
+
}
|
|
32680
|
+
function polygonArea(points) {
|
|
32681
|
+
let area = 0;
|
|
32682
|
+
for (let i = 0; i < points.length; i++) {
|
|
32683
|
+
const a = points[i];
|
|
32684
|
+
const b = points[(i + 1) % points.length];
|
|
32685
|
+
area += a.x * b.y - b.x * a.y;
|
|
32686
|
+
}
|
|
32687
|
+
return Math.abs(area) / 2;
|
|
32688
|
+
}
|
|
32689
|
+
/** Sutherland–Hodgman: `subject` clipped to convex `clip` (either winding). */
|
|
32690
|
+
function clipPolygon(subject, clip) {
|
|
32691
|
+
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;
|
|
32692
|
+
let output = [...subject];
|
|
32693
|
+
for (let i = 0; i < clip.length && output.length > 0; i++) {
|
|
32694
|
+
const a = clip[i];
|
|
32695
|
+
const b = clip[(i + 1) % clip.length];
|
|
32696
|
+
const side = (p) => orientation * ((b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x));
|
|
32697
|
+
const input = output;
|
|
32698
|
+
output = [];
|
|
32699
|
+
for (let j = 0; j < input.length; j++) {
|
|
32700
|
+
const current = input[j];
|
|
32701
|
+
const previous = input[(j + input.length - 1) % input.length];
|
|
32702
|
+
const sc = side(current);
|
|
32703
|
+
const sp = side(previous);
|
|
32704
|
+
if (sc >= 0 !== sp >= 0) {
|
|
32705
|
+
const t = sp / (sp - sc);
|
|
32706
|
+
output.push({
|
|
32707
|
+
x: previous.x + (current.x - previous.x) * t,
|
|
32708
|
+
y: previous.y + (current.y - previous.y) * t
|
|
32709
|
+
});
|
|
32710
|
+
}
|
|
32711
|
+
if (sc >= 0) output.push(current);
|
|
32712
|
+
}
|
|
32713
|
+
}
|
|
32714
|
+
return output;
|
|
32715
|
+
}
|
|
32716
|
+
function bounds(points) {
|
|
32717
|
+
const xs = points.map((p) => p.x);
|
|
32718
|
+
const ys = points.map((p) => p.y);
|
|
32719
|
+
return {
|
|
32720
|
+
x0: Math.min(...xs),
|
|
32721
|
+
y0: Math.min(...ys),
|
|
32722
|
+
x1: Math.max(...xs),
|
|
32723
|
+
y1: Math.max(...ys)
|
|
32724
|
+
};
|
|
32725
|
+
}
|
|
32726
|
+
/** Fraction (0–1) of `candidate`'s area covered by `other`. */
|
|
32727
|
+
function overlapFraction(candidate, other) {
|
|
32728
|
+
const a = bounds(candidate.corners);
|
|
32729
|
+
const b = bounds(other.corners);
|
|
32730
|
+
if (a.x1 <= b.x0 || b.x1 <= a.x0 || a.y1 <= b.y0 || b.y1 <= a.y0) return 0;
|
|
32731
|
+
const area = polygonArea(candidate.corners);
|
|
32732
|
+
if (area <= 0) return 1;
|
|
32733
|
+
return polygonArea(clipPolygon(candidate.corners, other.corners)) / area;
|
|
32734
|
+
}
|
|
31255
32735
|
/**
|
|
31256
|
-
*
|
|
31257
|
-
*
|
|
31258
|
-
*
|
|
31259
|
-
*
|
|
31260
|
-
*
|
|
31261
|
-
* but not what's on screen. Rendering in a "normal"-magnitude SVG coordinate
|
|
31262
|
-
* system (viewBox radius {@link RENDER_SCALE} instead of `1`) avoids it.
|
|
32736
|
+
* Greedy label placement: walk `shapes` in priority order and keep each one
|
|
32737
|
+
* whose overlap with every already-kept shape is at most `maxOverlap` of its
|
|
32738
|
+
* own area. Returns the ids of the kept labels. Checking actual (possibly
|
|
32739
|
+
* rotated) label footprints, not just node positions, is what stops a fan of
|
|
32740
|
+
* same-radius siblings from stacking their text into an unreadable pile.
|
|
31263
32741
|
*/
|
|
31264
|
-
|
|
32742
|
+
function cullOverlappingLabels(shapes, maxOverlap = .03) {
|
|
32743
|
+
const kept = [];
|
|
32744
|
+
for (const shape of shapes) if (kept.every((k) => overlapFraction(shape, k) <= maxOverlap)) kept.push(shape);
|
|
32745
|
+
return new Set(kept.map((r) => r.id));
|
|
32746
|
+
}
|
|
32747
|
+
/** `label` cut to at most `maxChars` characters, ending in `…` when cut. `0` or less disables truncation. */
|
|
32748
|
+
function truncateLabel(label, maxChars) {
|
|
32749
|
+
if (maxChars <= 0 || label.length <= maxChars) return label;
|
|
32750
|
+
return `${label.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
|
|
32751
|
+
}
|
|
32752
|
+
let measureContext;
|
|
32753
|
+
const widthCache = /* @__PURE__ */ new Map();
|
|
32754
|
+
/**
|
|
32755
|
+
* Rendered width of `text` at `fontSize` px in `fontFamily`, via an offscreen
|
|
32756
|
+
* canvas (cached). Falls back to a per-character estimate where canvas text
|
|
32757
|
+
* measurement is unavailable (e.g. non-browser test environments).
|
|
32758
|
+
*/
|
|
32759
|
+
function measureTextWidth(text, fontSize, fontFamily) {
|
|
32760
|
+
const cacheKey = `${fontFamily}\u0000${text}`;
|
|
32761
|
+
let atUnitSize = widthCache.get(cacheKey);
|
|
32762
|
+
if (atUnitSize === void 0) {
|
|
32763
|
+
if (measureContext === void 0) try {
|
|
32764
|
+
measureContext = typeof document === `undefined` ? null : document.createElement(`canvas`).getContext(`2d`);
|
|
32765
|
+
} catch {
|
|
32766
|
+
measureContext = null;
|
|
32767
|
+
}
|
|
32768
|
+
const reference = 100;
|
|
32769
|
+
let measured;
|
|
32770
|
+
if (measureContext) {
|
|
32771
|
+
measureContext.font = `${reference}px ${fontFamily || `sans-serif`}`;
|
|
32772
|
+
const width = measureContext.measureText(text).width;
|
|
32773
|
+
if (Number.isFinite(width) && width > 0) measured = width / reference;
|
|
32774
|
+
}
|
|
32775
|
+
atUnitSize = measured ?? text.length * .6;
|
|
32776
|
+
widthCache.set(cacheKey, atUnitSize);
|
|
32777
|
+
}
|
|
32778
|
+
return atUnitSize * fontSize;
|
|
32779
|
+
}
|
|
32780
|
+
/**
|
|
32781
|
+
* Smallest scale a box shrinks to, however far from the focus. Pure conformal
|
|
32782
|
+
* scaling (`1 − |z|²`) is geometrically exact but shrinks the second ring to
|
|
32783
|
+
* well under half size, taking labels below legibility long before the
|
|
32784
|
+
* boundary — flooring it keeps nearby rings readable while still reading as
|
|
32785
|
+
* "further away".
|
|
32786
|
+
*/
|
|
32787
|
+
const BOX_SCALE_FLOOR = .45;
|
|
32788
|
+
/**
|
|
32789
|
+
* Geometry for a `box`-shaped node. The box is laid out at its focus size and
|
|
32790
|
+
* then uniformly scaled — by the conformal factor, floored at
|
|
32791
|
+
* {@link BOX_SCALE_FLOOR} — so the label is measured just once. Weight
|
|
32792
|
+
* nudges the scale gently (square-rooted), so heavy nodes read as larger
|
|
32793
|
+
* without dwarfing their neighbours' labels.
|
|
32794
|
+
*/
|
|
32795
|
+
function boxGeometry(n, options) {
|
|
32796
|
+
const conformal = BOX_SCALE_FLOOR + .55 * conformalScale(n.position);
|
|
32797
|
+
const scale = Math.min(1, conformal * (options.zoom ?? 1)) * Math.sqrt(weightScale(n, options));
|
|
32798
|
+
const text = truncateLabel(n.node.item.label, options.maxChars);
|
|
32799
|
+
const padX = options.fontSize * .5;
|
|
32800
|
+
const height = options.fontSize * 1.55;
|
|
32801
|
+
return {
|
|
32802
|
+
scale,
|
|
32803
|
+
width: measureTextWidth(text, options.fontSize, options.fontFamily) + padX * 2,
|
|
32804
|
+
height,
|
|
32805
|
+
markerSize: options.fontSize * .55,
|
|
32806
|
+
text
|
|
32807
|
+
};
|
|
32808
|
+
}
|
|
32809
|
+
/** Label footprint of a box of `geometry` centered at `center` and rotated by `angle` (render units). */
|
|
32810
|
+
function boxLabelShape(id, center, geometry, angle = 0) {
|
|
32811
|
+
return orientedRect(id, center, geometry.width * geometry.scale / 2, geometry.height * geometry.scale / 2, angle);
|
|
32812
|
+
}
|
|
32813
|
+
//#endregion
|
|
32814
|
+
//#region src/hyperbolic-tree/render-context.ts
|
|
32815
|
+
const HORIZONTAL = {
|
|
32816
|
+
direction: 0,
|
|
32817
|
+
textAngle: 0,
|
|
32818
|
+
flipped: false
|
|
32819
|
+
};
|
|
32820
|
+
/**
|
|
32821
|
+
* Label font metrics. `--hyperbolictree-font-size` is in real screen px;
|
|
32822
|
+
* the SVG's viewBox is fixed, so it's converted to render units using the
|
|
32823
|
+
* on-screen disk size (`extentPx`) and zoom — keeping text legible whatever
|
|
32824
|
+
* size the element is laid out at, and constant-size while zooming
|
|
32825
|
+
* (zooming in spreads crowded labels apart rather than just magnifying the
|
|
32826
|
+
* pile).
|
|
32827
|
+
*/
|
|
32828
|
+
function fontMetrics(host, extentPx, zoom) {
|
|
32829
|
+
const style = typeof getComputedStyle === `function` ? getComputedStyle(host) : void 0;
|
|
32830
|
+
const parsed = Number.parseFloat(style?.getPropertyValue(`--hyperbolictree-font-size`) ?? ``);
|
|
32831
|
+
const px = Number.isFinite(parsed) && parsed > 0 ? parsed : 13;
|
|
32832
|
+
return {
|
|
32833
|
+
px,
|
|
32834
|
+
units: px * (1050 / (extentPx > 0 ? extentPx : 420) / (zoom || 1)),
|
|
32835
|
+
family: style?.fontFamily ?? ``
|
|
32836
|
+
};
|
|
32837
|
+
}
|
|
32838
|
+
/** Map of top-level-branch layout id → index among the root's children. */
|
|
32839
|
+
function branchIndices(root) {
|
|
32840
|
+
if (!root) return /* @__PURE__ */ new Map();
|
|
32841
|
+
return new Map(hyperbolicChildIds(root.item.key, root.children ?? []).map((id, i) => [id, i]));
|
|
32842
|
+
}
|
|
32843
|
+
/** Inline `--branch-index`/`--branch-count` declarations for the node/edge with layout id `id` (empty for the root). */
|
|
32844
|
+
function branchStyle(id, branches) {
|
|
32845
|
+
const branch = hyperbolicBranchId(id);
|
|
32846
|
+
const index = branch === void 0 ? void 0 : branches.get(branch);
|
|
32847
|
+
return index === void 0 ? `` : ` --branch-index: ${index}; --branch-count: ${branches.size};`;
|
|
32848
|
+
}
|
|
32849
|
+
function groupSiblings(nodes) {
|
|
32850
|
+
const groups = /* @__PURE__ */ new Map();
|
|
32851
|
+
for (const n of nodes) {
|
|
32852
|
+
const group = groups.get(n.parentId) ?? [];
|
|
32853
|
+
group.push(n);
|
|
32854
|
+
groups.set(n.parentId, group);
|
|
32855
|
+
}
|
|
32856
|
+
return groups;
|
|
32857
|
+
}
|
|
32858
|
+
/**
|
|
32859
|
+
* Label priority: the focus, then selected nodes, then larger (nearer the
|
|
32860
|
+
* center) first.
|
|
32861
|
+
*/
|
|
32862
|
+
function byLabelPriority(nodes, sizeOf, isSelected) {
|
|
32863
|
+
const rank = (n) => n.ring === 0 ? 0 : isSelected(n.node) ? 1 : 2;
|
|
32864
|
+
return [...nodes].sort((a, b) => rank(a) - rank(b) || sizeOf(b) - sizeOf(a) || a.ring - b.ring);
|
|
32865
|
+
}
|
|
32866
|
+
/**
|
|
32867
|
+
* Each node's label orientation. The reference neighbour is the one a step
|
|
32868
|
+
* nearer the focus: usually the parent, but for the focus's own ancestors
|
|
32869
|
+
* (which sit *behind* it) the child on the path back to the focus. The focus
|
|
32870
|
+
* itself stays horizontal.
|
|
32871
|
+
*/
|
|
32872
|
+
function labelOrientations(nodes, radial) {
|
|
32873
|
+
const out = /* @__PURE__ */ new Map();
|
|
32874
|
+
if (!radial) {
|
|
32875
|
+
for (const n of nodes) out.set(n.id, HORIZONTAL);
|
|
32876
|
+
return out;
|
|
32877
|
+
}
|
|
32878
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
32879
|
+
const nearer = /* @__PURE__ */ new Map();
|
|
32880
|
+
for (const n of nodes) {
|
|
32881
|
+
const parent = n.parentId === void 0 ? void 0 : byId.get(n.parentId);
|
|
32882
|
+
if (!parent) continue;
|
|
32883
|
+
if (parent.ring < n.ring) nearer.set(n.id, parent);
|
|
32884
|
+
else if (n.ring < parent.ring) nearer.set(parent.id, n);
|
|
32885
|
+
}
|
|
32886
|
+
for (const n of nodes) {
|
|
32887
|
+
const from = nearer.get(n.id);
|
|
32888
|
+
if (!from || n.ring === 0) {
|
|
32889
|
+
out.set(n.id, HORIZONTAL);
|
|
32890
|
+
continue;
|
|
32891
|
+
}
|
|
32892
|
+
const direction = Math.atan2(n.position.y - from.position.y, n.position.x - from.position.x);
|
|
32893
|
+
const flipped = Math.cos(direction) < 0;
|
|
32894
|
+
out.set(n.id, {
|
|
32895
|
+
direction,
|
|
32896
|
+
textAngle: flipped ? direction + Math.PI : direction,
|
|
32897
|
+
flipped
|
|
32898
|
+
});
|
|
32899
|
+
}
|
|
32900
|
+
return out;
|
|
32901
|
+
}
|
|
32902
|
+
const renderCenter = (n) => ({
|
|
32903
|
+
x: n.position.x * 500,
|
|
32904
|
+
y: n.position.y * 500
|
|
32905
|
+
});
|
|
32906
|
+
function boxLabels(nodes, options, orientations) {
|
|
32907
|
+
const { font } = options;
|
|
32908
|
+
const boxes = new Map(nodes.map((n) => [n.id, boxGeometry(n, {
|
|
32909
|
+
fontSize: font.units,
|
|
32910
|
+
fontFamily: font.family,
|
|
32911
|
+
maxChars: options.boxMaxChars,
|
|
32912
|
+
minWeightScale: options.minWeightScale,
|
|
32913
|
+
maxWeightScale: options.maxWeightScale,
|
|
32914
|
+
zoom: options.zoom
|
|
32915
|
+
})]));
|
|
32916
|
+
return {
|
|
32917
|
+
boxes,
|
|
32918
|
+
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)))
|
|
32919
|
+
};
|
|
32920
|
+
}
|
|
32921
|
+
function dotLabels(nodes, options, orientations) {
|
|
32922
|
+
const { font } = options;
|
|
32923
|
+
const nearest = nearestNeighbourDistances(nodes);
|
|
32924
|
+
const radii = new Map(nodes.map((n) => [n.id, dotRadius(n, nearest.get(n.id), options)]));
|
|
32925
|
+
return {
|
|
32926
|
+
radii,
|
|
32927
|
+
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) => {
|
|
32928
|
+
const { direction } = orientations.get(n.id);
|
|
32929
|
+
const halfWidth = measureTextWidth(n.node.item.label, font.units, font.family) / 2;
|
|
32930
|
+
const along = (radii.get(n.id) + .01) * 500 + halfWidth;
|
|
32931
|
+
const c = renderCenter(n);
|
|
32932
|
+
const center = {
|
|
32933
|
+
x: c.x + Math.cos(direction) * along,
|
|
32934
|
+
y: c.y + Math.sin(direction) * along
|
|
32935
|
+
};
|
|
32936
|
+
return orientedRect(n.id, center, halfWidth + font.units * .6, font.units * .6, direction);
|
|
32937
|
+
}))
|
|
32938
|
+
};
|
|
32939
|
+
}
|
|
32940
|
+
/**
|
|
32941
|
+
* Everything the node templates need for one render. Labels are placed
|
|
32942
|
+
* greedily by priority and dropped where they'd overlap; hovered/cursor
|
|
32943
|
+
* nodes are exempt from culling entirely — always labelled — so hovering a
|
|
32944
|
+
* marker reveals it without reshuffling everyone else's labels.
|
|
32945
|
+
*/
|
|
32946
|
+
function buildNodeRenderContext(nodes, options) {
|
|
32947
|
+
const orientations = labelOrientations(nodes, options.radialLabels);
|
|
32948
|
+
const shapeData = options.nodeShape === `box` ? {
|
|
32949
|
+
...boxLabels(nodes, options, orientations),
|
|
32950
|
+
radii: /* @__PURE__ */ new Map()
|
|
32951
|
+
} : {
|
|
32952
|
+
...dotLabels(nodes, options, orientations),
|
|
32953
|
+
boxes: /* @__PURE__ */ new Map()
|
|
32954
|
+
};
|
|
32955
|
+
const labelIds = new Set(shapeData.labelIds);
|
|
32956
|
+
for (const n of nodes) if (options.isTickled(n.id)) labelIds.add(n.id);
|
|
32957
|
+
return {
|
|
32958
|
+
siblingGroups: groupSiblings(nodes),
|
|
32959
|
+
font: options.font,
|
|
32960
|
+
branches: options.branches,
|
|
32961
|
+
boxes: shapeData.boxes,
|
|
32962
|
+
radii: shapeData.radii,
|
|
32963
|
+
labelIds,
|
|
32964
|
+
orientations
|
|
32965
|
+
};
|
|
32966
|
+
}
|
|
32967
|
+
/**
|
|
32968
|
+
* Paint order: far-from-focus first so nearer (larger) nodes sit on top;
|
|
32969
|
+
* then labelled over unlabelled; the hovered node above everything.
|
|
32970
|
+
*/
|
|
32971
|
+
function paintOrder(nodes, ctx, isTickled) {
|
|
32972
|
+
const layer = (n) => isTickled(n.id) ? 2 : ctx.labelIds.has(n.id) ? 1 : 0;
|
|
32973
|
+
const distance = (n) => n.position.x ** 2 + n.position.y ** 2;
|
|
32974
|
+
return [...nodes].sort((a, b) => layer(a) - layer(b) || distance(b) - distance(a));
|
|
32975
|
+
}
|
|
32976
|
+
//#endregion
|
|
32977
|
+
//#region src/hyperbolic-tree/render-templates.ts
|
|
31265
32978
|
/**
|
|
31266
32979
|
* Sanitizes a node key into a token safe to use inside a `part` attribute /
|
|
31267
32980
|
* `::part()` selector — arbitrary key characters (spaces, quotes, etc.)
|
|
@@ -31270,13 +32983,121 @@ const RENDER_SCALE = 500;
|
|
|
31270
32983
|
function partToken(key) {
|
|
31271
32984
|
return key.replace(/[^\w-]/g, `-`);
|
|
31272
32985
|
}
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
32986
|
+
const preventMouseDownFocus = (e) => e.preventDefault();
|
|
32987
|
+
/** The shared `<g part="node">` wrapper — ARIA, parts, weight/branch vars, tooltip, and pointer handling. */
|
|
32988
|
+
function renderNodeGroup(n, args, transform, content) {
|
|
32989
|
+
const { node, key, id } = n;
|
|
32990
|
+
const { ctx, state, handlers } = args;
|
|
32991
|
+
const selected = state.isSelected(node);
|
|
32992
|
+
const classes = [
|
|
32993
|
+
`node`,
|
|
32994
|
+
selected ? `selected` : ``,
|
|
32995
|
+
state.isTickled(id) ? `tickled` : ``,
|
|
32996
|
+
state.isFiltered(n) ? `filtered` : ``,
|
|
32997
|
+
state.isPending(node) ? `loading` : ``,
|
|
32998
|
+
n.ring === 0 ? `focused` : ``
|
|
32999
|
+
].filter(Boolean).join(` `);
|
|
33000
|
+
const siblings = ctx.siblingGroups.get(n.parentId) ?? [n];
|
|
33001
|
+
const weightStyle = `--node-weight: ${n.weightNormalized};${n.weight === void 0 ? `` : ` --node-weight-raw: ${n.weight};`}`;
|
|
33002
|
+
return svg`
|
|
33003
|
+
<g
|
|
33004
|
+
class=${classes}
|
|
33005
|
+
data-key=${key}
|
|
33006
|
+
role="treeitem"
|
|
33007
|
+
tabindex="-1"
|
|
33008
|
+
aria-level=${n.depth + 1}
|
|
33009
|
+
aria-selected=${selected}
|
|
33010
|
+
aria-setsize=${siblings.length}
|
|
33011
|
+
aria-posinset=${siblings.findIndex((sib) => sib.id === id) + 1}
|
|
33012
|
+
aria-label=${node.item.label}
|
|
33013
|
+
part="node node-${partToken(key)}"
|
|
33014
|
+
style=${weightStyle + branchStyle(id, ctx.branches)}
|
|
33015
|
+
transform=${transform}
|
|
33016
|
+
@pointerdown=${handlers.onPointerDown}
|
|
33017
|
+
@mousedown=${preventMouseDownFocus}
|
|
33018
|
+
@click=${(e) => handlers.onClick(n, e)}
|
|
33019
|
+
@dblclick=${(e) => handlers.onDblClick(n, e)}
|
|
33020
|
+
@pointerenter=${() => handlers.onPointerEnter(id)}
|
|
33021
|
+
>${state.showTitle(n) ? svg`<title>${node.item.label}</title>` : nothing}${content}</g>
|
|
33022
|
+
`;
|
|
33023
|
+
}
|
|
33024
|
+
const degrees = (radians) => radians * 180 / Math.PI;
|
|
33025
|
+
/** A dot's label: just past the dot, running along its orientation — flipped labels anchor at their end so they still read outward. */
|
|
33026
|
+
function renderDotLabel(n, ctx, scaledRadius) {
|
|
33027
|
+
const { textAngle, flipped } = ctx.orientations.get(n.id);
|
|
33028
|
+
const gap = scaledRadius + 5;
|
|
33029
|
+
return svg`<text
|
|
33030
|
+
class="hyperbolictree-label"
|
|
33031
|
+
part="label"
|
|
33032
|
+
x=${flipped ? -gap : gap}
|
|
33033
|
+
y="0"
|
|
33034
|
+
transform=${textAngle === 0 ? nothing : `rotate(${degrees(textAngle)})`}
|
|
33035
|
+
text-anchor=${flipped ? `end` : `start`}
|
|
33036
|
+
font-size=${ctx.font.units}
|
|
33037
|
+
dominant-baseline="middle"
|
|
33038
|
+
>${n.node.item.label}</text>`;
|
|
33039
|
+
}
|
|
33040
|
+
function renderDotNode(n, args) {
|
|
33041
|
+
const { ctx } = args;
|
|
33042
|
+
const scaledRadius = ctx.radii.get(n.id) * 500;
|
|
33043
|
+
return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})`, svg`
|
|
33044
|
+
<circle class="node-hit" r=${Math.max(scaledRadius, 17.5)} part="hit" />
|
|
33045
|
+
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
33046
|
+
${ctx.labelIds.has(n.id) ? renderDotLabel(n, ctx, scaledRadius) : nothing}
|
|
33047
|
+
`);
|
|
33048
|
+
}
|
|
33049
|
+
function renderBoxNode(n, args) {
|
|
33050
|
+
const { ctx } = args;
|
|
33051
|
+
const box = ctx.boxes.get(n.id);
|
|
33052
|
+
const showText = ctx.labelIds.has(n.id);
|
|
33053
|
+
const width = showText ? box.width : box.markerSize;
|
|
33054
|
+
const height = showText ? box.height : box.markerSize;
|
|
33055
|
+
const { textAngle } = ctx.orientations.get(n.id);
|
|
33056
|
+
const rotate = showText && textAngle !== 0 ? ` rotate(${degrees(textAngle)})` : ``;
|
|
33057
|
+
return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})${rotate} scale(${box.scale})`, svg`
|
|
33058
|
+
<rect
|
|
33059
|
+
class="node-box ${showText ? `` : `marker`}"
|
|
33060
|
+
part="box"
|
|
33061
|
+
x=${-width / 2}
|
|
33062
|
+
y=${-height / 2}
|
|
33063
|
+
width=${width}
|
|
33064
|
+
height=${height}
|
|
33065
|
+
rx=${ctx.font.units * .2}
|
|
33066
|
+
/>
|
|
33067
|
+
${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}
|
|
33068
|
+
`);
|
|
33069
|
+
}
|
|
33070
|
+
/** Every visible node, in paint order, in the shape `ctx` was built for. */
|
|
33071
|
+
function renderNodes(nodes, shape, args) {
|
|
33072
|
+
const ordered = paintOrder(nodes, args.ctx, args.state.isTickled);
|
|
33073
|
+
const renderOne = shape === `box` ? renderBoxNode : renderDotNode;
|
|
33074
|
+
return repeat(ordered, (n) => n.id, (n) => renderOne(n, args));
|
|
33075
|
+
}
|
|
33076
|
+
function renderDefaultEdge(edge, branches) {
|
|
33077
|
+
return svg`<path
|
|
33078
|
+
class="edge"
|
|
33079
|
+
d=${geodesicPath(edge.from, edge.to, 500)}
|
|
33080
|
+
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
33081
|
+
data-from=${edge.fromKey}
|
|
33082
|
+
data-to=${edge.toKey}
|
|
33083
|
+
style="--edge-weight: ${edge.weightNormalized};${branchStyle(edge.toId, branches)}"
|
|
33084
|
+
/>`;
|
|
31279
33085
|
}
|
|
33086
|
+
/** Every visible edge — via `edgeRenderer` when given, else the default geodesic arc. */
|
|
33087
|
+
function renderEdges(edges, byId, branches, edgeRenderer) {
|
|
33088
|
+
return repeat(edges, (e) => `${e.fromId}->${e.toId}`, (edge) => {
|
|
33089
|
+
const fromNode = byId.get(edge.fromId)?.node;
|
|
33090
|
+
const toNode = byId.get(edge.toId)?.node;
|
|
33091
|
+
if (edgeRenderer && fromNode && toNode) return svg`<g transform="scale(${500})">${edgeRenderer({
|
|
33092
|
+
edge,
|
|
33093
|
+
fromNode,
|
|
33094
|
+
toNode
|
|
33095
|
+
})}</g>`;
|
|
33096
|
+
return renderDefaultEdge(edge, branches);
|
|
33097
|
+
});
|
|
33098
|
+
}
|
|
33099
|
+
//#endregion
|
|
33100
|
+
//#region src/hyperbolic-tree/hyperbolictree-list.ts
|
|
31280
33101
|
let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitElement {
|
|
31281
33102
|
constructor(..._args) {
|
|
31282
33103
|
super(..._args);
|
|
@@ -31284,37 +33105,63 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31284
33105
|
this.selectionFilter = `all`;
|
|
31285
33106
|
this.interactionMode = `standard`;
|
|
31286
33107
|
this.maxRadius = 4;
|
|
33108
|
+
this.nodeShape = `dot`;
|
|
33109
|
+
this.boxMaxChars = 24;
|
|
33110
|
+
this.minLabelSize = 9;
|
|
33111
|
+
this.colorByBranch = false;
|
|
31287
33112
|
this.nodeRadius = .05;
|
|
31288
33113
|
this.minNodeRadius = .015;
|
|
31289
33114
|
this.minWeightScale = .6;
|
|
31290
33115
|
this.maxWeightScale = 1.6;
|
|
31291
33116
|
this.zoom = 1;
|
|
33117
|
+
this.disableZoomGestures = false;
|
|
33118
|
+
this.disableRadialLabels = false;
|
|
33119
|
+
this.disableChildPreview = false;
|
|
31292
33120
|
this.curvature = 1;
|
|
33121
|
+
this.siblingSpacing = DEFAULT_SIBLING_SPACING;
|
|
31293
33122
|
this.focusAnimationDuration = 400;
|
|
31294
33123
|
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
31295
33124
|
this.disableKeyboardNavigation = false;
|
|
31296
33125
|
this.registry = void 0;
|
|
31297
|
-
this.
|
|
31298
|
-
this.
|
|
31299
|
-
this.
|
|
33126
|
+
this._focusId = void 0;
|
|
33127
|
+
this._viewOverride = void 0;
|
|
33128
|
+
this._focusRotation = 0;
|
|
33129
|
+
this._zoomOffset = {
|
|
31300
33130
|
x: 0,
|
|
31301
33131
|
y: 0
|
|
31302
33132
|
};
|
|
33133
|
+
this._previewId = void 0;
|
|
33134
|
+
this.#preview = createChildPreviewTiming({
|
|
33135
|
+
candidate: (id) => {
|
|
33136
|
+
const nodes = this.#layout().nodes;
|
|
33137
|
+
const n = id === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === id);
|
|
33138
|
+
return n && needsChildPreview(n, nodes, this.#lastLabelIds) ? n.id : void 0;
|
|
33139
|
+
},
|
|
33140
|
+
setPreviewId: (id) => {
|
|
33141
|
+
this._previewId = id;
|
|
33142
|
+
}
|
|
33143
|
+
});
|
|
33144
|
+
this.#lazy = createLazyChildLoader(() => ({
|
|
33145
|
+
model: this.model,
|
|
33146
|
+
loadChildren: this.loadChildren
|
|
33147
|
+
}), () => this.requestUpdate());
|
|
33148
|
+
this.#lastLabelIds = /* @__PURE__ */ new Set();
|
|
31303
33149
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
31304
33150
|
labelSelector: `.hyperbolictree-label`,
|
|
31305
33151
|
highlightKey: `incr-search-highlight`
|
|
31306
33152
|
}) });
|
|
31307
33153
|
this.tickled = new TickledController(this, {
|
|
31308
33154
|
getItems: () => this.#layout().nodes.map((n) => ({
|
|
31309
|
-
id: n.
|
|
33155
|
+
id: n.id,
|
|
31310
33156
|
enabled: true
|
|
31311
33157
|
})),
|
|
31312
33158
|
onStateChange: () => {
|
|
31313
33159
|
this.setAttribute(`data-input-mode`, this.tickled.inputMode);
|
|
31314
33160
|
},
|
|
31315
33161
|
onTickle: (id) => {
|
|
33162
|
+
this.#preview.update(id);
|
|
31316
33163
|
if (id === void 0) return;
|
|
31317
|
-
const n = this.#layout().nodes.find((n) => n.
|
|
33164
|
+
const n = this.#layout().nodes.find((n) => n.id === id);
|
|
31318
33165
|
if (!n) return;
|
|
31319
33166
|
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
31320
33167
|
detail: {
|
|
@@ -31326,8 +33173,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31326
33173
|
}));
|
|
31327
33174
|
}
|
|
31328
33175
|
});
|
|
31329
|
-
this
|
|
31330
|
-
|
|
33176
|
+
this._hostSize = {
|
|
33177
|
+
width: 0,
|
|
33178
|
+
height: 0
|
|
33179
|
+
};
|
|
33180
|
+
this.#gestures = createGestureHandlers({
|
|
33181
|
+
svgRect: () => this.renderRoot.querySelector(`svg.hyperbolictree`)?.getBoundingClientRect(),
|
|
33182
|
+
takeView: () => {
|
|
33183
|
+
const view = this.#currentView();
|
|
33184
|
+
this.#cancelAnimation();
|
|
33185
|
+
this._viewOverride = view;
|
|
33186
|
+
return view;
|
|
33187
|
+
},
|
|
33188
|
+
setView: (view) => {
|
|
33189
|
+
this._viewOverride = view;
|
|
33190
|
+
},
|
|
33191
|
+
zoomState: () => this.#zoomState(),
|
|
33192
|
+
setZoomState: (state) => {
|
|
33193
|
+
this.zoom = state.zoom;
|
|
33194
|
+
this._zoomOffset = state.offset;
|
|
33195
|
+
},
|
|
33196
|
+
zoomGesturesEnabled: () => !this.disableZoomGestures,
|
|
33197
|
+
hasFocus: () => this.renderRoot.activeElement !== null,
|
|
33198
|
+
focusTree: () => this.#focusTree()
|
|
33199
|
+
});
|
|
31331
33200
|
}
|
|
31332
33201
|
static {
|
|
31333
33202
|
this.shadowRootOptions = {
|
|
@@ -31340,37 +33209,55 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31340
33209
|
}
|
|
31341
33210
|
/** The node currently centered on the disk. Pure view-state — `root` is never mutated by focus changes. */
|
|
31342
33211
|
get focusedNode() {
|
|
31343
|
-
if (this.
|
|
31344
|
-
return
|
|
33212
|
+
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
33213
|
+
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
31345
33214
|
}
|
|
33215
|
+
#preview;
|
|
33216
|
+
#lazy;
|
|
33217
|
+
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
33218
|
+
#lastLabelIds;
|
|
31346
33219
|
#modelUnsubscribe;
|
|
31347
|
-
#pendingKeys;
|
|
31348
|
-
#aborters;
|
|
31349
33220
|
#ownedRegistry;
|
|
31350
33221
|
#unregisterSearchCommands;
|
|
31351
33222
|
#animFrame;
|
|
31352
|
-
#
|
|
33223
|
+
#resizeObserver;
|
|
33224
|
+
#gestures;
|
|
31353
33225
|
get #effectiveRoot() {
|
|
31354
33226
|
return this.model?.root ?? this.root;
|
|
31355
33227
|
}
|
|
31356
33228
|
#layout() {
|
|
31357
33229
|
return computeHyperbolicLayout(this.#effectiveRoot, {
|
|
31358
|
-
focusedKey: this.
|
|
33230
|
+
focusedKey: this._focusId,
|
|
31359
33231
|
maxRadius: this.maxRadius,
|
|
31360
33232
|
curvature: this.curvature,
|
|
31361
|
-
|
|
33233
|
+
siblingSpacing: this.siblingSpacing,
|
|
33234
|
+
view: this.#currentView()
|
|
31362
33235
|
});
|
|
31363
33236
|
}
|
|
31364
|
-
#
|
|
31365
|
-
|
|
31366
|
-
|
|
31367
|
-
|
|
31368
|
-
|
|
31369
|
-
|
|
31370
|
-
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
33237
|
+
get #rootId() {
|
|
33238
|
+
return this.#effectiveRoot?.item.key;
|
|
33239
|
+
}
|
|
33240
|
+
/** The projection currently on screen, from the focus's local frame. */
|
|
33241
|
+
#currentView() {
|
|
33242
|
+
return this._viewOverride ?? rotationIsometry(this._focusRotation);
|
|
33243
|
+
}
|
|
33244
|
+
/**
|
|
33245
|
+
* The current view re-expressed in `targetId`'s local frame — i.e. the same
|
|
33246
|
+
* picture, but with `targetId` as the reference node. Where the view hands
|
|
33247
|
+
* over on a focus change.
|
|
33248
|
+
*/
|
|
33249
|
+
#viewFromFrameOf(targetId, currentId) {
|
|
33250
|
+
const entries = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature, this.siblingSpacing);
|
|
33251
|
+
const relative = currentId === void 0 ? void 0 : hyperbolicRelativeFrame(entries, currentId, targetId);
|
|
33252
|
+
return relative ? composeIsometry(this.#currentView(), relative) : rotationIsometry(this._focusRotation);
|
|
33253
|
+
}
|
|
33254
|
+
/** Layout id of `node` — by identity, falling back to the first node sharing its key. */
|
|
33255
|
+
#idOf(node) {
|
|
33256
|
+
const root = this.#effectiveRoot;
|
|
33257
|
+
const direct = findHyperbolicIdOf(root, node);
|
|
33258
|
+
if (direct !== void 0) return direct;
|
|
33259
|
+
const byKey = findHyperbolicNode(root, node.item.key);
|
|
33260
|
+
return byKey ? findHyperbolicIdOf(root, byKey) : void 0;
|
|
31374
33261
|
}
|
|
31375
33262
|
connectedCallback() {
|
|
31376
33263
|
super.connectedCallback();
|
|
@@ -31390,14 +33277,25 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31390
33277
|
});
|
|
31391
33278
|
this.registry = registry;
|
|
31392
33279
|
this.#syncSearchCommands();
|
|
33280
|
+
if (typeof ResizeObserver === `function`) {
|
|
33281
|
+
this.#resizeObserver = new ResizeObserver(([entry]) => {
|
|
33282
|
+
const { width, height } = entry.contentRect;
|
|
33283
|
+
this._hostSize = {
|
|
33284
|
+
width,
|
|
33285
|
+
height
|
|
33286
|
+
};
|
|
33287
|
+
});
|
|
33288
|
+
this.#resizeObserver.observe(this);
|
|
33289
|
+
}
|
|
31393
33290
|
}
|
|
31394
33291
|
disconnectedCallback() {
|
|
31395
33292
|
super.disconnectedCallback();
|
|
33293
|
+
this.#resizeObserver?.disconnect();
|
|
33294
|
+
this.#resizeObserver = void 0;
|
|
31396
33295
|
this.#modelUnsubscribe?.();
|
|
31397
33296
|
this.#modelUnsubscribe = void 0;
|
|
31398
|
-
|
|
31399
|
-
this.#
|
|
31400
|
-
this.#pendingKeys.clear();
|
|
33297
|
+
this.#preview.dispose();
|
|
33298
|
+
this.#lazy.abortAll();
|
|
31401
33299
|
if (this.#animFrame !== void 0) {
|
|
31402
33300
|
cancelAnimationFrame(this.#animFrame);
|
|
31403
33301
|
this.#animFrame = void 0;
|
|
@@ -31438,58 +33336,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31438
33336
|
}
|
|
31439
33337
|
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
31440
33338
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
31441
|
-
if (this.
|
|
31442
|
-
this.
|
|
31443
|
-
this.
|
|
33339
|
+
if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
|
|
33340
|
+
this._focusId = void 0;
|
|
33341
|
+
this._viewOverride = void 0;
|
|
33342
|
+
this._focusRotation = 0;
|
|
31444
33343
|
}
|
|
31445
|
-
this.#
|
|
33344
|
+
this.#lazy.ensure(this.#layout().nodes);
|
|
31446
33345
|
}
|
|
31447
33346
|
#resetViewState() {
|
|
31448
33347
|
this.#clearSelectionInternal();
|
|
31449
33348
|
this.tickled.clearTickled();
|
|
31450
|
-
this.
|
|
31451
|
-
this.
|
|
31452
|
-
this.
|
|
33349
|
+
this._focusId = void 0;
|
|
33350
|
+
this._viewOverride = void 0;
|
|
33351
|
+
this._focusRotation = 0;
|
|
33352
|
+
this._zoomOffset = {
|
|
31453
33353
|
x: 0,
|
|
31454
33354
|
y: 0
|
|
31455
33355
|
};
|
|
33356
|
+
this._previewId = void 0;
|
|
33357
|
+
this.#gestures.cancel();
|
|
31456
33358
|
if (this.#animFrame !== void 0) {
|
|
31457
33359
|
cancelAnimationFrame(this.#animFrame);
|
|
31458
33360
|
this.#animFrame = void 0;
|
|
31459
33361
|
}
|
|
31460
33362
|
}
|
|
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
33363
|
#canSelect(node) {
|
|
31494
33364
|
if (this.selectionFilter === `none`) return false;
|
|
31495
33365
|
if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
|
|
@@ -31565,83 +33435,83 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31565
33435
|
* the previous one, for interop with generic `TreeComponent` listeners.
|
|
31566
33436
|
*/
|
|
31567
33437
|
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);
|
|
33438
|
+
const root = this.#effectiveRoot;
|
|
33439
|
+
const targetId = this.#idOf(node);
|
|
33440
|
+
const currentId = this._focusId ?? this.#rootId;
|
|
33441
|
+
if (targetId === void 0 || currentId === targetId) return;
|
|
33442
|
+
const targetNode = findHyperbolicPathById(root, targetId)?.at(-1) ?? node;
|
|
33443
|
+
const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
|
|
33444
|
+
const fromView = this.#viewFromFrameOf(targetId, currentId);
|
|
33445
|
+
this._focusId = targetId;
|
|
33446
|
+
this.#gestures.cancel();
|
|
33447
|
+
this._previewId = void 0;
|
|
33448
|
+
this.#animateTo(fromView);
|
|
31587
33449
|
this.dispatchEvent(new CustomEvent(`focus`, {
|
|
31588
33450
|
detail: {
|
|
31589
|
-
node,
|
|
33451
|
+
node: targetNode,
|
|
31590
33452
|
previous: previousNode
|
|
31591
33453
|
},
|
|
31592
33454
|
bubbles: true,
|
|
31593
33455
|
composed: true
|
|
31594
33456
|
}));
|
|
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
|
-
}
|
|
33457
|
+
if (previousNode && currentId !== void 0) {
|
|
33458
|
+
if (isHyperbolicDescendantId(targetId, currentId)) this.dispatchEvent(new CustomEvent(`expand`, {
|
|
33459
|
+
detail: {
|
|
33460
|
+
node: targetNode,
|
|
33461
|
+
depth: hyperbolicIdDepth(targetId)
|
|
33462
|
+
},
|
|
33463
|
+
bubbles: true,
|
|
33464
|
+
composed: true
|
|
33465
|
+
}));
|
|
33466
|
+
else if (isHyperbolicDescendantId(currentId, targetId)) this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
33467
|
+
detail: {
|
|
33468
|
+
node: previousNode,
|
|
33469
|
+
depth: hyperbolicIdDepth(currentId)
|
|
33470
|
+
},
|
|
33471
|
+
bubbles: true,
|
|
33472
|
+
composed: true
|
|
33473
|
+
}));
|
|
31617
33474
|
}
|
|
31618
33475
|
this.requestUpdate();
|
|
31619
33476
|
}
|
|
31620
|
-
#
|
|
31621
|
-
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
31622
|
-
}
|
|
31623
|
-
#animateTo(from, to) {
|
|
33477
|
+
#cancelAnimation() {
|
|
31624
33478
|
if (this.#animFrame !== void 0) {
|
|
31625
33479
|
cancelAnimationFrame(this.#animFrame);
|
|
31626
33480
|
this.#animFrame = void 0;
|
|
31627
33481
|
}
|
|
31628
|
-
|
|
31629
|
-
|
|
33482
|
+
}
|
|
33483
|
+
/** Animate from view `from` (in the new focus's frame) to the settled, centered view — see {@link focusTransition}. */
|
|
33484
|
+
#animateTo(from) {
|
|
33485
|
+
this.#cancelAnimation();
|
|
33486
|
+
const { viewAt, settledRotation } = focusTransition(from);
|
|
33487
|
+
this._focusRotation = settledRotation;
|
|
33488
|
+
const fromOffset = this._zoomOffset;
|
|
33489
|
+
if (this.focusAnimationDuration <= 0 || prefersReducedMotion$1() || typeof requestAnimationFrame !== `function`) {
|
|
33490
|
+
this._viewOverride = void 0;
|
|
33491
|
+
this._zoomOffset = {
|
|
33492
|
+
x: 0,
|
|
33493
|
+
y: 0
|
|
33494
|
+
};
|
|
31630
33495
|
return;
|
|
31631
33496
|
}
|
|
31632
33497
|
const start = performance.now();
|
|
31633
33498
|
const duration = this.focusAnimationDuration;
|
|
31634
33499
|
const step = (now) => {
|
|
31635
33500
|
const t = Math.min(1, (now - start) / duration);
|
|
31636
|
-
|
|
31637
|
-
this.
|
|
33501
|
+
const remaining = 1 - easeInOutCubic(t);
|
|
33502
|
+
this._zoomOffset = {
|
|
33503
|
+
x: fromOffset.x * remaining,
|
|
33504
|
+
y: fromOffset.y * remaining
|
|
33505
|
+
};
|
|
33506
|
+
this._viewOverride = t >= 1 ? void 0 : viewAt(t);
|
|
31638
33507
|
this.#animFrame = t < 1 ? requestAnimationFrame(step) : void 0;
|
|
31639
33508
|
};
|
|
31640
33509
|
this.#animFrame = requestAnimationFrame(step);
|
|
31641
33510
|
}
|
|
31642
33511
|
#focusAndMaybeSelect(node) {
|
|
31643
33512
|
this.focusOn(node);
|
|
31644
|
-
this
|
|
33513
|
+
const id = this.#idOf(node);
|
|
33514
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
31645
33515
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
31646
33516
|
}
|
|
31647
33517
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -31656,7 +33526,8 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31656
33526
|
}
|
|
31657
33527
|
this.focusOn(current);
|
|
31658
33528
|
this.select(current);
|
|
31659
|
-
this
|
|
33529
|
+
const id = this.#idOf(current);
|
|
33530
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
31660
33531
|
this.requestUpdate();
|
|
31661
33532
|
return true;
|
|
31662
33533
|
}
|
|
@@ -31664,7 +33535,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31664
33535
|
navigateToNode(predicate) {
|
|
31665
33536
|
const root = this.#effectiveRoot;
|
|
31666
33537
|
if (!root?.children) return false;
|
|
31667
|
-
const keyPath =
|
|
33538
|
+
const keyPath = findHyperbolicKeyPath(root.children, predicate);
|
|
31668
33539
|
if (!keyPath) return false;
|
|
31669
33540
|
return this.navigateTo(keyPath);
|
|
31670
33541
|
}
|
|
@@ -31675,22 +33546,13 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31675
33546
|
*/
|
|
31676
33547
|
revealKey(key) {
|
|
31677
33548
|
const node = findHyperbolicNode(this.#effectiveRoot, key);
|
|
31678
|
-
|
|
31679
|
-
if (!
|
|
31680
|
-
this.
|
|
33549
|
+
const id = node ? this.#idOf(node) : void 0;
|
|
33550
|
+
if (!node || id === void 0) return false;
|
|
33551
|
+
if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
|
|
33552
|
+
this.tickled.setTickled(id);
|
|
31681
33553
|
this.requestUpdate();
|
|
31682
33554
|
return true;
|
|
31683
33555
|
}
|
|
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
33556
|
/**
|
|
31695
33557
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
31696
33558
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -31727,7 +33589,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31727
33589
|
this.requestUpdate();
|
|
31728
33590
|
}
|
|
31729
33591
|
} else if (this.interactionMode !== `manual`) this.select(n.node);
|
|
31730
|
-
this.tickled.setTickled(n.
|
|
33592
|
+
this.tickled.setTickled(n.id);
|
|
31731
33593
|
this.focusOn(n.node);
|
|
31732
33594
|
}
|
|
31733
33595
|
#handleItemDblClick(n) {
|
|
@@ -31749,239 +33611,147 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31749
33611
|
}));
|
|
31750
33612
|
}
|
|
31751
33613
|
#handleKeyDown(event) {
|
|
31752
|
-
if (this.disableKeyboardNavigation) return;
|
|
31753
33614
|
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
|
-
}
|
|
33615
|
+
if (this.disableKeyboardNavigation || !root) return;
|
|
33616
|
+
if (event.key === `Escape` && this._previewId !== void 0) {
|
|
33617
|
+
event.preventDefault();
|
|
33618
|
+
this._previewId = void 0;
|
|
33619
|
+
return;
|
|
33620
|
+
}
|
|
33621
|
+
const { action, handled } = resolveKeyboardAction(event.key, root, this._focusId ?? root.item.key, this.#layout().nodes);
|
|
33622
|
+
if (handled) event.preventDefault();
|
|
33623
|
+
if (action.kind === `focus`) this.#focusAndMaybeSelect(action.node);
|
|
33624
|
+
else if (action.kind === `activate`) {
|
|
33625
|
+
if (this.interactionMode !== `manual`) this.select(action.node);
|
|
33626
|
+
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
33627
|
+
detail: {
|
|
33628
|
+
node: action.node,
|
|
33629
|
+
depth: action.depth
|
|
33630
|
+
},
|
|
33631
|
+
bubbles: true,
|
|
33632
|
+
composed: true
|
|
33633
|
+
}));
|
|
31803
33634
|
}
|
|
31804
33635
|
}
|
|
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();
|
|
33636
|
+
#zoomState() {
|
|
33637
|
+
return clampZoomState({
|
|
33638
|
+
zoom: this.zoom,
|
|
33639
|
+
offset: this._zoomOffset
|
|
33640
|
+
});
|
|
31832
33641
|
}
|
|
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
|
-
/>`;
|
|
33642
|
+
/**
|
|
33643
|
+
* Any pointer interaction (a node press, or a pan) moves keyboard focus to
|
|
33644
|
+
* the tree, so the user can carry straight on with the arrow keys.
|
|
33645
|
+
* Focuses the stable `<svg role="tree">` — not the node, which paint-order
|
|
33646
|
+
* re-sorting may move in the DOM (dropping its focus) — without scrolling.
|
|
33647
|
+
*/
|
|
33648
|
+
#focusTree() {
|
|
33649
|
+
this.renderRoot.querySelector(`svg.hyperbolictree`)?.focus({ preventScroll: true });
|
|
31851
33650
|
}
|
|
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);
|
|
33651
|
+
#handleNodePointerDown(event) {
|
|
33652
|
+
event.stopPropagation();
|
|
33653
|
+
this.#focusTree();
|
|
33654
|
+
}
|
|
33655
|
+
#pickPreviewChild(child) {
|
|
33656
|
+
this.#preview.hide();
|
|
33657
|
+
if (this.interactionMode !== `manual`) this.select(child);
|
|
33658
|
+
const id = this.#idOf(child);
|
|
33659
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
33660
|
+
this.focusOn(child);
|
|
33661
|
+
this.#focusTree();
|
|
33662
|
+
}
|
|
33663
|
+
#previewFor(nodes) {
|
|
33664
|
+
const n = this._previewId === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === this._previewId);
|
|
33665
|
+
if (!n) return void 0;
|
|
33666
|
+
const { width, height } = this._hostSize;
|
|
33667
|
+
const anchor = diskToHostPx(n.position, this.#zoomState(), width || 420, height || 420);
|
|
33668
|
+
return {
|
|
33669
|
+
node: n.node,
|
|
33670
|
+
anchor,
|
|
33671
|
+
above: anchor.y > (height || 420) / 2
|
|
33672
|
+
};
|
|
33673
|
+
}
|
|
33674
|
+
#nodeContext(nodes, branches) {
|
|
33675
|
+
return buildNodeRenderContext(nodes, {
|
|
33676
|
+
nodeShape: this.nodeShape,
|
|
33677
|
+
radialLabels: !this.disableRadialLabels,
|
|
33678
|
+
zoom: this.#zoomState().zoom,
|
|
33679
|
+
font: fontMetrics(this, Math.min(this._hostSize.width, this._hostSize.height), this.zoom),
|
|
33680
|
+
branches,
|
|
33681
|
+
boxMaxChars: this.boxMaxChars,
|
|
33682
|
+
minLabelSize: this.minLabelSize,
|
|
33683
|
+
nodeRadius: this.nodeRadius,
|
|
33684
|
+
minNodeRadius: this.minNodeRadius,
|
|
33685
|
+
minWeightScale: this.minWeightScale,
|
|
33686
|
+
maxWeightScale: this.maxWeightScale,
|
|
33687
|
+
isSelected: (node) => this._selectedNodes.has(node),
|
|
33688
|
+
isTickled: (id) => this.tickled.isTickled(id)
|
|
31898
33689
|
});
|
|
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
33690
|
}
|
|
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
|
-
`;
|
|
33691
|
+
#renderNodes(nodes, ctx) {
|
|
33692
|
+
return renderNodes(nodes, this.nodeShape, {
|
|
33693
|
+
ctx,
|
|
33694
|
+
state: {
|
|
33695
|
+
isSelected: (node) => this._selectedNodes.has(node),
|
|
33696
|
+
isTickled: (id) => this.tickled.isTickled(id),
|
|
33697
|
+
showTitle: (n) => this.disableChildPreview || !n.node.children?.length,
|
|
33698
|
+
isFiltered: (n) => !this.#matchesFilter(n.node, n.depth),
|
|
33699
|
+
isPending: (node) => this.#lazy.isPending(node)
|
|
33700
|
+
},
|
|
33701
|
+
handlers: {
|
|
33702
|
+
onPointerDown: (e) => this.#handleNodePointerDown(e),
|
|
33703
|
+
onClick: (n, e) => {
|
|
33704
|
+
e.stopPropagation();
|
|
33705
|
+
this.#handleItemClick(n, e);
|
|
33706
|
+
},
|
|
33707
|
+
onDblClick: (n, e) => {
|
|
33708
|
+
e.stopPropagation();
|
|
33709
|
+
this.#handleItemDblClick(n);
|
|
33710
|
+
},
|
|
33711
|
+
onPointerEnter: (id) => this.tickled.handlePointerEnter(id)
|
|
33712
|
+
}
|
|
33713
|
+
});
|
|
31950
33714
|
}
|
|
31951
33715
|
render() {
|
|
31952
33716
|
const { nodes, edges } = this.#layout();
|
|
31953
|
-
const
|
|
31954
|
-
const
|
|
31955
|
-
|
|
31956
|
-
|
|
31957
|
-
|
|
31958
|
-
|
|
31959
|
-
}
|
|
31960
|
-
const labelKeys = this.#computeLabelKeys(nodes);
|
|
33717
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
33718
|
+
const branches = branchIndices(this.#effectiveRoot);
|
|
33719
|
+
const ctx = this.#nodeContext(nodes, branches);
|
|
33720
|
+
this.#lastLabelIds = ctx.labelIds;
|
|
33721
|
+
const { zoom, offset } = this.#zoomState();
|
|
33722
|
+
const pan = offset.x === 0 && offset.y === 0 ? `` : `translate(${offset.x * 500} ${offset.y * 500}) `;
|
|
31961
33723
|
return html`
|
|
31962
33724
|
<svg
|
|
31963
33725
|
class="hyperbolictree"
|
|
31964
|
-
viewBox="${-
|
|
33726
|
+
viewBox="${-525} ${-525} ${1050} ${1050}"
|
|
31965
33727
|
preserveAspectRatio="xMidYMid meet"
|
|
31966
33728
|
role="tree"
|
|
31967
33729
|
tabindex="0"
|
|
31968
33730
|
data-input-mode=${this.tickled.inputMode}
|
|
33731
|
+
data-node-shape=${this.nodeShape}
|
|
31969
33732
|
@keydown=${(e) => this.#handleKeyDown(e)}
|
|
31970
|
-
@pointerdown=${(e) => this.#
|
|
31971
|
-
@pointermove=${(e) => this.#
|
|
31972
|
-
@pointerup=${(e) => this.#
|
|
33733
|
+
@pointerdown=${(e) => this.#gestures.onPointerDown(e)}
|
|
33734
|
+
@pointermove=${(e) => this.#gestures.onPointerMove(e)}
|
|
33735
|
+
@pointerup=${(e) => this.#gestures.onPointerUp(e)}
|
|
33736
|
+
@pointercancel=${(e) => this.#gestures.onPointerUp(e)}
|
|
33737
|
+
@wheel=${(e) => this.#gestures.onWheel(e)}
|
|
31973
33738
|
@pointerleave=${() => this.tickled.clearTickled()}
|
|
31974
33739
|
>
|
|
31975
|
-
<g class="zoom" transform="scale(${
|
|
31976
|
-
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${
|
|
33740
|
+
<g class="zoom" transform="${pan}scale(${zoom})">
|
|
33741
|
+
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${500} />
|
|
31977
33742
|
<g class="edges" part="edges">
|
|
31978
|
-
${
|
|
33743
|
+
${renderEdges(edges, byId, branches, this.edgeRenderer)}
|
|
31979
33744
|
</g>
|
|
31980
33745
|
<g class="nodes" part="nodes">
|
|
31981
|
-
${
|
|
33746
|
+
${this.#renderNodes(nodes, ctx)}
|
|
31982
33747
|
</g>
|
|
31983
33748
|
</g>
|
|
31984
33749
|
</svg>
|
|
33750
|
+
${renderChildPreview(this.#previewFor(nodes), {
|
|
33751
|
+
onPointerEnter: () => this.#preview.pointerEnter(),
|
|
33752
|
+
onPointerLeave: () => this.#preview.pointerLeave(this.tickled.tickledId),
|
|
33753
|
+
onPick: (child) => this.#pickPreviewChild(child)
|
|
33754
|
+
})}
|
|
31985
33755
|
${this.search.renderField()}
|
|
31986
33756
|
`;
|
|
31987
33757
|
}
|
|
@@ -31989,102 +33759,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
31989
33759
|
this.styles = [
|
|
31990
33760
|
themeFallbacks,
|
|
31991
33761
|
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
|
-
`
|
|
33762
|
+
hyperbolicTreeStyles
|
|
32088
33763
|
];
|
|
32089
33764
|
}
|
|
32090
33765
|
};
|
|
@@ -32107,6 +33782,24 @@ __decorate([property({
|
|
|
32107
33782
|
type: Number,
|
|
32108
33783
|
attribute: `max-radius`
|
|
32109
33784
|
})], HyperbolicTreeListElement.prototype, "maxRadius", void 0);
|
|
33785
|
+
__decorate([property({
|
|
33786
|
+
type: String,
|
|
33787
|
+
attribute: `node-shape`,
|
|
33788
|
+
reflect: true
|
|
33789
|
+
})], HyperbolicTreeListElement.prototype, "nodeShape", void 0);
|
|
33790
|
+
__decorate([property({
|
|
33791
|
+
type: Number,
|
|
33792
|
+
attribute: `box-max-chars`
|
|
33793
|
+
})], HyperbolicTreeListElement.prototype, "boxMaxChars", void 0);
|
|
33794
|
+
__decorate([property({
|
|
33795
|
+
type: Number,
|
|
33796
|
+
attribute: `min-label-size`
|
|
33797
|
+
})], HyperbolicTreeListElement.prototype, "minLabelSize", void 0);
|
|
33798
|
+
__decorate([property({
|
|
33799
|
+
type: Boolean,
|
|
33800
|
+
attribute: `color-by-branch`,
|
|
33801
|
+
reflect: true
|
|
33802
|
+
})], HyperbolicTreeListElement.prototype, "colorByBranch", void 0);
|
|
32110
33803
|
__decorate([property({
|
|
32111
33804
|
type: Number,
|
|
32112
33805
|
attribute: `node-radius`
|
|
@@ -32124,7 +33817,23 @@ __decorate([property({
|
|
|
32124
33817
|
attribute: `max-weight-scale`
|
|
32125
33818
|
})], HyperbolicTreeListElement.prototype, "maxWeightScale", void 0);
|
|
32126
33819
|
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "zoom", void 0);
|
|
33820
|
+
__decorate([property({
|
|
33821
|
+
type: Boolean,
|
|
33822
|
+
attribute: `disable-zoom-gestures`
|
|
33823
|
+
})], HyperbolicTreeListElement.prototype, "disableZoomGestures", void 0);
|
|
33824
|
+
__decorate([property({
|
|
33825
|
+
type: Boolean,
|
|
33826
|
+
attribute: `disable-radial-labels`
|
|
33827
|
+
})], HyperbolicTreeListElement.prototype, "disableRadialLabels", void 0);
|
|
33828
|
+
__decorate([property({
|
|
33829
|
+
type: Boolean,
|
|
33830
|
+
attribute: `disable-child-preview`
|
|
33831
|
+
})], HyperbolicTreeListElement.prototype, "disableChildPreview", void 0);
|
|
32127
33832
|
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "curvature", void 0);
|
|
33833
|
+
__decorate([property({
|
|
33834
|
+
type: Number,
|
|
33835
|
+
attribute: `sibling-spacing`
|
|
33836
|
+
})], HyperbolicTreeListElement.prototype, "siblingSpacing", void 0);
|
|
32128
33837
|
__decorate([property({
|
|
32129
33838
|
type: Number,
|
|
32130
33839
|
attribute: `focus-animation-duration`
|
|
@@ -32143,9 +33852,12 @@ __decorate([property({
|
|
|
32143
33852
|
attribute: `search-modes`,
|
|
32144
33853
|
reflect: true
|
|
32145
33854
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
32146
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
32147
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
32148
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
33855
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
|
|
33856
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
|
|
33857
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
|
|
33858
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
|
|
33859
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_previewId", void 0);
|
|
33860
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_hostSize", void 0);
|
|
32149
33861
|
HyperbolicTreeListElement = __decorate([safeCustomElement(`ixfx-hyperbolictree-list`)], HyperbolicTreeListElement);
|
|
32150
33862
|
//#endregion
|
|
32151
33863
|
//#region src/icicle-tree/layout.ts
|
|
@@ -37913,6 +39625,9 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37913
39625
|
const open = this._isOpen(node, depth);
|
|
37914
39626
|
const isBranch = node.item.isLeaf !== true;
|
|
37915
39627
|
const icon = node.item.icon;
|
|
39628
|
+
const iconColor = appearanceColor(node.item.iconColor);
|
|
39629
|
+
const textColor = appearanceColor(node.item.textColor);
|
|
39630
|
+
const badge = appearanceAbsent(node.item.badge) ? void 0 : node.item.badge;
|
|
37916
39631
|
const tooltip = node.item.tooltip;
|
|
37917
39632
|
const isCheckedMode = this.interactionMode === `checked`;
|
|
37918
39633
|
let checkboxTpl = nothing;
|
|
@@ -37940,8 +39655,9 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37940
39655
|
@pointerenter=${() => this._handleItemPointerEnter(node, depth)}
|
|
37941
39656
|
>
|
|
37942
39657
|
${checkboxTpl}
|
|
37943
|
-
${icon ? html`<ixfx-icon class="item-icon" name="${icon}"></ixfx-icon>` : nothing}
|
|
37944
|
-
<span class="label">${node.item.label}</span>
|
|
39658
|
+
${icon ? html`<ixfx-icon class="item-icon" name="${icon}" style=${iconColor ? `color:${iconColor}` : nothing}></ixfx-icon>` : nothing}
|
|
39659
|
+
<span class="label" style=${textColor ? `color:${textColor}` : nothing}>${node.item.label}</span>
|
|
39660
|
+
${badge === void 0 ? nothing : html`<span class="item-badge" part="badge">${badge}</span>`}
|
|
37945
39661
|
${tooltip ? html`<ixfx-tooltip class="item-tooltip" anchor="--tooltip-miller-${depth}-${node.item.key}" .value=${tooltip}></ixfx-tooltip>` : nothing}
|
|
37946
39662
|
${isBranch ? html`<span class="caret" aria-hidden="true">›</span>` : nothing}
|
|
37947
39663
|
</li>
|
|
@@ -37995,6 +39711,17 @@ let MillerListElement = class MillerListElement extends MillerBaseElement {
|
|
|
37995
39711
|
text-overflow: ellipsis;
|
|
37996
39712
|
}
|
|
37997
39713
|
|
|
39714
|
+
/* Finder-style badge: sits between the label and the caret, flush to
|
|
39715
|
+
the right edge of the row. The label is flex:1, so the gap opens up
|
|
39716
|
+
ahead of the badge automatically. */
|
|
39717
|
+
.item-badge {
|
|
39718
|
+
flex-shrink: 0;
|
|
39719
|
+
display: inline-flex;
|
|
39720
|
+
align-items: center;
|
|
39721
|
+
margin-inline-start: auto;
|
|
39722
|
+
padding-inline-start: var(--miller-list-badge-gap, 0.5em);
|
|
39723
|
+
}
|
|
39724
|
+
|
|
37998
39725
|
.item-tooltip {
|
|
37999
39726
|
position: absolute;
|
|
38000
39727
|
}
|
|
@@ -51586,6 +53313,6 @@ function init() {
|
|
|
51586
53313
|
console.log(`Init`);
|
|
51587
53314
|
}
|
|
51588
53315
|
//#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 };
|
|
53316
|
+
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
53317
|
|
|
51591
53318
|
//# sourceMappingURL=index.js.map
|