@ixfx/components 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/bundle/index.d.ts +917 -44
  2. package/bundle/index.js +702 -696
  3. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  4. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  5. package/dist/crumbs.d.ts +2 -115
  6. package/dist/crumbs.js +1 -1179
  7. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
  9. package/dist/data-grid.d.ts +3 -3
  10. package/dist/data-grid.js +1 -1
  11. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  13. package/dist/detail-list.d.ts +4 -4
  14. package/dist/detail-list.js +1 -1
  15. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-JPgQSmLW.d.ts} +27 -4
  16. package/dist/flat-list.d.ts +2 -2
  17. package/dist/flat-list.js +123 -4
  18. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  19. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  20. package/dist/grid-list.d.ts +3 -3
  21. package/dist/grid-list.js +1 -1
  22. package/dist/incr-search.d.ts +1 -1
  23. package/dist/index-9ttcdwlL.d.ts +71 -0
  24. package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  25. package/dist/index-CYRc6Dah.d.ts +475 -0
  26. package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
  27. package/dist/index.d.ts +334 -23
  28. package/dist/index.js +2247 -867
  29. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  30. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  31. package/dist/miller-BFRfn0ve.js +2237 -0
  32. package/dist/miller.d.ts +4 -437
  33. package/dist/miller.js +1 -1815
  34. package/dist/pending-BGidHppL.js +91 -0
  35. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  36. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  37. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  38. package/dist/split-layout.d.ts +1 -1
  39. package/dist/split-layout.js +1 -1
  40. package/dist/sunburst-tree.d.ts +2 -2
  41. package/dist/sunburst-tree.js +1 -1
  42. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_VxOHlo.js} +298 -79
  43. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
  44. package/dist/tree-CftHwtEb.js +146 -0
  45. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  46. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  47. package/dist/tree.d.ts +3 -3
  48. package/dist/tree.js +1 -1
  49. package/dist/types-LU1u0CB8.d.ts +87 -0
  50. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  51. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  52. package/dist/vertical-list.d.ts +3 -3
  53. package/dist/vertical-list.js +1 -1789
  54. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  55. package/docs-user/README.md +2 -0
  56. package/docs-user/crumbs.md +27 -0
  57. package/docs-user/data-grid.md +66 -1
  58. package/docs-user/detail-list.md +54 -0
  59. package/docs-user/dock.md +54 -12
  60. package/docs-user/flat-list.md +27 -0
  61. package/docs-user/grid-list.md +64 -11
  62. package/docs-user/hyperbolic-tree.md +31 -4
  63. package/docs-user/icicle-tree.md +36 -0
  64. package/docs-user/index.json +12 -2
  65. package/docs-user/llms.txt +2 -0
  66. package/docs-user/miller.md +52 -21
  67. package/docs-user/multicol-list.md +50 -8
  68. package/docs-user/split-layout.md +45 -17
  69. package/docs-user/sunburst-tree.md +32 -2
  70. package/docs-user/tree.md +45 -6
  71. package/docs-user/treemap.md +32 -0
  72. package/docs-user/twosplit-layout.md +22 -0
  73. package/docs-user/user-catalog.md +1 -1
  74. package/docs-user/vertical-list.md +55 -7
  75. package/llms.txt +2 -0
  76. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18,7 +18,9 @@ import { t as prominenceStyles } from "./prominence-DlFShic3.js";
18
18
  import { n as buttonStyles, t as ButtonElement } from "./button-ikByJJPN.js";
19
19
  import { a as toGroupKey, i as resolveGroupKey, n as defaultGroupLabel, o as toggleInSet, r as partitionByGroup, t as EMPTY_GROUP_LABEL } from "./grouping-RYPnHkrS.js";
20
20
  import { a as listItemFadeKeyframes, c as captionForEntry, i as animateListItemOut, l as createDefaultListDragPreview, n as LIST_ITEM_MOTION_MS, o as prefersReducedMotion, r as animateListItemIn, s as startListItemOutAnimation, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
21
- import { n as isMultiMode, r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-BeE70lbB.js";
21
+ import { n as isMultiMode, r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-5L0Nj9tR.js";
22
+ import { a as capKeys, c as keysAsFlags, d as partitionKeys, f as withoutFlag, i as capFlags, l as keysToArray, n as EMPTY_KEYS, o as flagsFromRecord, r as MAX_PENDING_KEYS, s as flagsToRecord, t as EMPTY_FLAGS, u as partitionFlags } from "./pending-BGidHppL.js";
23
+ import { n as createScrollHoverGuard, t as SCROLL_HOVER_SUPPRESS_ATTR } from "./scroll-hover-guard-Cjw6179Q.js";
22
24
  import "./interaction-E-uMGHkJ.js";
23
25
  import "./menu-item-BUv8An2k.js";
24
26
  import { Checkbox } from "./checkbox.js";
@@ -26,35 +28,37 @@ import { t as CheckedSelect } from "./checked-select-CBUidG7n.js";
26
28
  import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-DwAa6RDh.js";
27
29
  import { LabelledRangeInput } from "./labelled-range-input.js";
28
30
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
29
- import { a as TreeDataModel, c as pruneDraggedDescendants, i as TreeDropController, n as TreeListElement, o as applyTreeDrop, r as TreeBaseElement, s as dropTargetFor, t as TreeController } from "./tree-D8VxvR9u.js";
31
+ import { a as TreeDataModel, c as pruneDraggedDescendants, i as TreeDropController, n as TreeListElement, o as applyTreeDrop, r as TreeBaseElement, s as dropTargetFor, t as TreeController } from "./tree-Crdpl4nc.js";
32
+ import { a as restoreElementScroll, c as VIEW_STATE_VERSION, i as onUserScrollIntent, n as anchorFromRow, o as scrollToOffset, r as captureElementScroll, s as VIEW_STATE_CHANGE, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
30
33
  import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
34
+ import { t as CrumbNavigationElement } from "./crumb-navigation-YnFVFqrx.js";
31
35
  import { NarrowedTextElement } from "./narrowed-text.js";
32
- import { CrumbNavigationElement } from "./crumbs.js";
33
36
  import { DataDisplayComponent } from "./data-display.js";
34
37
  import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
35
- import { t as DataGridElement } from "./data-grid-C1sZX6n9.js";
38
+ import { t as DataGridElement } from "./data-grid-rMkLX2qo.js";
36
39
  import { a as menuFromTreeToElements, t as menu_exports } from "./menu-CU9KAm8s.js";
37
40
  import { t as PanTextElement } from "./pan-text-CqtvjPyK.js";
38
- import { t as DetailListElement } from "./detail-list-q8bs67BM.js";
41
+ import { t as DetailListElement } from "./detail-list-C4pDSoI8.js";
39
42
  import { ListGroupHeaderElement } from "./list-group-header.js";
40
- import { a as distribute, c as preserveRestoreWeights, d as toSnapshot, f as walk, i as buildModel, l as resolveTracks, n as SplitPanelElement, o as findPanelNode, r as applySnapshot, s as normalise, t as SplitLayoutElement, u as setPanelRestoreWeight } from "./split-layout-Deoxy47d.js";
43
+ import { a as distribute, c as preserveRestoreWeights, d as toSnapshot, f as walk, i as buildModel, l as resolveTracks, n as SplitPanelElement, o as findPanelNode, r as applySnapshot, s as normalise, t as SplitLayoutElement, u as setPanelRestoreWeight } from "./split-layout-DTY4PJRp.js";
41
44
  import { t as TabListElement } from "./tab-list-R_bGbDvO.js";
42
45
  import { EditableLabel } from "./editable-label.js";
43
46
  import { t as resolveAxisStep } from "./keyboard-B5xD7Fz-.js";
44
47
  import { t as WheelNudgeMixin } from "./wheel-nudge-D5rmht3v.js";
45
48
  import { n as EditableNumber, t as LabelledSliderInput } from "./labelled-slider-input-FdCRi6st.js";
46
- import { VerticalListElement } from "./vertical-list.js";
49
+ import { t as VerticalListElement } from "./vertical-list-1V1Vg2O5.js";
47
50
  import { FlatListElement } from "./flat-list.js";
48
51
  import { n as formStyles, r as adoptLightDomStyles, t as FormElement } from "./form-CY0nibZ5.js";
49
- import { t as GridListElement } from "./grid-list-y6dWJYu3.js";
52
+ import { t as GridListElement } from "./grid-list-CdEkScn-.js";
50
53
  import { PanelElement } from "./panel.js";
51
54
  import { GroupedItemListerElement } from "./grouped-item-lister.js";
55
+ import { a as resolveKeyPath, i as reconcileSelection$1, n as reconcileCursor, t as pathFromJson } from "./tree-CftHwtEb.js";
52
56
  import { t as IxfxHexEditorElement } from "./hex-editor-DiYpgIKs.js";
53
57
  import { t as RadialInputElement } from "./radial-input-KMVltQfe.js";
54
58
  import { LabelledRadialInput } from "./labelled-radial-input.js";
55
59
  import { t as SliderInputElement } from "./slider-input-ByHdotzp.js";
56
60
  import { LedElement } from "./led.js";
57
- import { MillerBaseElement, MillerListElement, MillerTreeController } from "./miller.js";
61
+ import { n as MillerListElement, r as MillerBaseElement, t as MillerTreeController } from "./miller-BFRfn0ve.js";
58
62
  import { NotificationPillElement } from "./notifier.js";
59
63
  import { a as TooltipElement, c as computeColour, i as PlotMultiAxis, l as parseColourScaleAttr, n as PlotSingleAxis, o as PlotDataSeries, r as PlotHistogram, s as applyAgeModification, t as PlotXyAxis, u as readBaseColour } from "./xy-axis-CyMd_Bn1.js";
60
64
  import { PolarPad } from "./polar-pad.js";
@@ -64,7 +68,7 @@ import { RangeElement } from "./range.js";
64
68
  import { SelectHorizElement } from "./select-horiz.js";
65
69
  import { SnackbarElement } from "./snackbar.js";
66
70
  import { SnapContainer } from "./snap-container.js";
67
- import { a as findSunburstId, c as sunburstChildId, d as sunburstParentId, f as sunburstPathToId, i as defaultSunburstValueAccessor, l as sunburstHasChildren, n as computeSunburstLayout, o as findSunburstNodeById, p as SUNBURST_PATH_SEPARATOR, r as createValueResolver, s as isSunburstAncestorId, t as SunburstTreeListElement, u as sunburstIdToPath } from "./sunbursttree-list-yXwS-uEI.js";
71
+ import { a as findSunburstId, c as sunburstChildId, d as sunburstParentId, f as sunburstPathToId, i as defaultSunburstValueAccessor, l as sunburstHasChildren, n as computeSunburstLayout, o as findSunburstNodeById, p as SUNBURST_PATH_SEPARATOR, r as createValueResolver, s as isSunburstAncestorId, t as SunburstTreeListElement, u as sunburstIdToPath } from "./sunbursttree-list-B_VxOHlo.js";
68
72
  import { SwipeElement } from "./swipe.js";
69
73
  import { a as resolveCssVars, i as attachTimelineInteractions, n as TimelineTrackTabElement, o as resolveGrid, r as timelineStyles, s as TimelineController, t as IxfxTimelineElement } from "./timeline-BP1Ux2EV.js";
70
74
  import { TransitoryLabelElement } from "./transitory-label.js";
@@ -1408,6 +1412,46 @@ __decorate([state()], SplitButton.prototype, "_loadingItems", void 0);
1408
1412
  __decorate([state()], SplitButton.prototype, "_menuHasItems", void 0);
1409
1413
  SplitButton = __decorate([safeCustomElement(`ixfx-split-button`)], SplitButton);
1410
1414
  //#endregion
1415
+ //#region src/common/view-state/bind.ts
1416
+ /** Whether `el` implements the view-state API. */
1417
+ function isViewStateful(el) {
1418
+ return typeof el === `object` && el !== null && typeof el.getViewState === `function` && typeof el.setViewState === `function`;
1419
+ }
1420
+ /**
1421
+ * Wire a component's view state to storage the host controls. Loads and
1422
+ * applies once, then saves on every `view-state-change` and when the page is
1423
+ * hidden. Returns a function that stops saving.
1424
+ *
1425
+ * ```ts
1426
+ * const stop = bindViewState(tree, {
1427
+ * load: () => JSON.parse(localStorage.getItem(`files`) ?? `null`),
1428
+ * save: s => localStorage.setItem(`files`, JSON.stringify(s)),
1429
+ * });
1430
+ * ```
1431
+ */
1432
+ function bindViewState(el, binding) {
1433
+ let loaded;
1434
+ try {
1435
+ loaded = binding.load();
1436
+ } catch (error) {
1437
+ console.warn(`bindViewState: load failed`, error);
1438
+ }
1439
+ if (loaded) el.setViewState(loaded);
1440
+ const onChange = (event) => {
1441
+ const { detail } = event;
1442
+ if (event.target !== el) return;
1443
+ if (detail.reason === `restore` && !binding.saveOnRestore) return;
1444
+ binding.save(detail.state);
1445
+ };
1446
+ const onHide = () => binding.save(el.getViewState());
1447
+ el.addEventListener(VIEW_STATE_CHANGE, onChange);
1448
+ globalThis.addEventListener?.(`pagehide`, onHide);
1449
+ return () => {
1450
+ el.removeEventListener(VIEW_STATE_CHANGE, onChange);
1451
+ globalThis.removeEventListener?.(`pagehide`, onHide);
1452
+ };
1453
+ }
1454
+ //#endregion
1411
1455
  //#region src/crumbs/crumb-path-controller.ts
1412
1456
  var CrumbPathController = class {
1413
1457
  #element = null;
@@ -3515,6 +3559,46 @@ function findEmptiedStacks(before, after) {
3515
3559
  return emptied;
3516
3560
  }
3517
3561
  //#endregion
3562
+ //#region src/dock/dock-view-state.ts
3563
+ /**
3564
+ * Pure helpers for `ixfx-dock-host` view state. The dock's layout is close to
3565
+ * structure, so the saved state is the layout tree itself; these helpers
3566
+ * validate what comes back from storage and filter out panels the host
3567
+ * doesn't hold.
3568
+ */
3569
+ /** Loose structural check of a parsed {@link DockLayout} (from storage, so untrusted). */
3570
+ function isDockLayout(value) {
3571
+ if (value === null || typeof value !== `object`) return false;
3572
+ return isDockNode(value.root);
3573
+ }
3574
+ function isDockNode(value) {
3575
+ if (value === null || typeof value !== `object`) return false;
3576
+ const node = value;
3577
+ if (node.kind === `stack`) return Array.isArray(node.panels) && node.panels.every((p) => typeof p === `string`) && typeof node.active === `string`;
3578
+ if (node.kind === `split`) return (node.direction === `row` || node.direction === `column`) && Array.isArray(node.sizes) && node.sizes.every((n) => typeof n === `number`) && Array.isArray(node.children) && node.children.every(isDockNode);
3579
+ return false;
3580
+ }
3581
+ /**
3582
+ * `layout` with every panel `isPresent` rejects removed (stacks that empty
3583
+ * out are dropped unless named, splits collapse as usual). Used for
3584
+ * `getViewState({ includePending: false })`.
3585
+ */
3586
+ function withoutAbsentPanels(layout, isPresent) {
3587
+ const root = serializeLayout(layout.root).root;
3588
+ const visit = (node) => {
3589
+ if (isStack(node)) {
3590
+ node.panels = node.panels.filter(isPresent);
3591
+ return;
3592
+ }
3593
+ node.children.forEach(visit);
3594
+ };
3595
+ visit(root);
3596
+ return {
3597
+ version: 1,
3598
+ root: normalize(root)
3599
+ };
3600
+ }
3601
+ //#endregion
3518
3602
  //#region src/tabs/tab-list-item.ts
3519
3603
  const resizeHandlers = /* @__PURE__ */ new WeakMap();
3520
3604
  const sharedResizeObserver = new ResizeObserver((entries) => {
@@ -4159,6 +4243,11 @@ let DockHostElement = class DockHostElement extends LitElement {
4159
4243
  this._toolbarSlots = /* @__PURE__ */ new Set();
4160
4244
  this._toolbarObserver = null;
4161
4245
  this._panelObservers = /* @__PURE__ */ new Map();
4246
+ this.#viewState = new ViewStateController(this, {
4247
+ kind: `dock-host`,
4248
+ capture: () => this.getViewState()
4249
+ });
4250
+ this.#layoutRestored = false;
4162
4251
  this._onDragStart = (event) => {
4163
4252
  if (event.composedPath().find((n) => n instanceof HTMLElement && n.tagName === `IXFX-DOCK-HOST`) !== this) return;
4164
4253
  const { panelId, pointerId, clientX, clientY } = event.detail;
@@ -4191,6 +4280,10 @@ let DockHostElement = class DockHostElement extends LitElement {
4191
4280
  this._onSplitWeightsChanged = () => {
4192
4281
  this._foldLiveWeights();
4193
4282
  this._scheduleJunctions();
4283
+ this.#viewState.notify();
4284
+ };
4285
+ this._onInnerViewState = (event) => {
4286
+ if (event.target === event.currentTarget) event.stopPropagation();
4194
4287
  };
4195
4288
  this._onSplitCollapsed = (event, collapsed) => {
4196
4289
  const detail = event.detail;
@@ -4201,6 +4294,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4201
4294
  if (node === null || !isStack(node)) return;
4202
4295
  if (collapsed) node.collapsed = true;
4203
4296
  else delete node.collapsed;
4297
+ this.#viewState.notify();
4204
4298
  };
4205
4299
  this._onSplitClose = (event) => {
4206
4300
  event.preventDefault();
@@ -4223,6 +4317,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4223
4317
  if (stack === null || !isStack(stack)) return;
4224
4318
  if (!setActive(stack, panelId)) return;
4225
4319
  this.requestUpdate();
4320
+ this.#viewState.notify();
4226
4321
  this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
4227
4322
  detail: { panelId },
4228
4323
  bubbles: true,
@@ -4273,6 +4368,9 @@ let DockHostElement = class DockHostElement extends LitElement {
4273
4368
  this._teardownTabDragListeners();
4274
4369
  };
4275
4370
  }
4371
+ #viewState;
4372
+ /** A layout was restored via `setViewState()`; it wins over the initial `layout` attribute. */
4373
+ #layoutRestored;
4276
4374
  connectedCallback() {
4277
4375
  super.connectedCallback();
4278
4376
  this._buildInitialModel();
@@ -4310,7 +4408,8 @@ let DockHostElement = class DockHostElement extends LitElement {
4310
4408
  }
4311
4409
  willUpdate(changed) {
4312
4410
  super.willUpdate(changed);
4313
- if (changed.has(`layout`) && this.layout !== null) {
4411
+ const initial = !this.hasUpdated && this.#layoutRestored;
4412
+ if (changed.has(`layout`) && this.layout !== null && !initial) {
4314
4413
  const parsed = parseLayout(this.layout);
4315
4414
  if (parsed) {
4316
4415
  this._model = parsed.root;
@@ -4342,6 +4441,10 @@ let DockHostElement = class DockHostElement extends LitElement {
4342
4441
  this._scheduleJunctions();
4343
4442
  }
4344
4443
  _buildInitialModel() {
4444
+ if (this.#layoutRestored) {
4445
+ this._mergePoolIntoModel();
4446
+ return;
4447
+ }
4345
4448
  if (this.layout !== null) {
4346
4449
  const parsed = parseLayout(this.layout);
4347
4450
  if (parsed) {
@@ -4476,21 +4579,56 @@ let DockHostElement = class DockHostElement extends LitElement {
4476
4579
  }
4477
4580
  this._pool = current;
4478
4581
  this.requestUpdate();
4479
- if (structuralChange) this._emitChange();
4582
+ if (structuralChange) this._emitChange(`data`);
4583
+ }
4584
+ /**
4585
+ * Snapshot the layout (live split weights folded in from the DOM). With
4586
+ * `includePending` (default), panels the restored layout names but the
4587
+ * host doesn't hold yet are kept in place; without it they are left out.
4588
+ */
4589
+ getViewState(options) {
4590
+ const layout = this._captureLayout();
4591
+ const withPending = options?.includePending ?? true;
4592
+ return this.#viewState.shape({ layout: withPending ? layout : withoutAbsentPanels(layout, (id) => this._findPanelEl(id) !== void 0) }, options);
4593
+ }
4594
+ /**
4595
+ * Restore a layout. Panels it names that aren't in the host yet keep their
4596
+ * place (shown as a placeholder tab) and slot in when they arrive — as a
4597
+ * light-DOM child, or via `addPanel()` without a target. Pooled panels it
4598
+ * doesn't name are appended to the first stack admitting their kind.
4599
+ */
4600
+ setViewState(state) {
4601
+ if (!this.#viewState.accepts(state) || state.layout === void 0) return;
4602
+ if (!isDockLayout(state.layout)) {
4603
+ console.warn(`[dock-host] ignoring view state with a malformed layout`);
4604
+ return;
4605
+ }
4606
+ const layout = state.layout;
4607
+ this.#viewState.restoring(() => {
4608
+ this.#layoutRestored = true;
4609
+ this._returnAllToolbars();
4610
+ this._loadGeneration++;
4611
+ this._model = normalize(cloneNode(layout.root));
4612
+ this._mergePoolIntoModel();
4613
+ this._liveStacks = snapshotNonEmptyStacks(this._model);
4614
+ this.requestUpdate();
4615
+ this._emitChange();
4616
+ });
4480
4617
  }
4481
- /** Serialize the current layout, folding live split weights from the DOM. */
4482
- saveLayout() {
4618
+ /**
4619
+ * Placeholders are part of the layout tree rather than a separate pending
4620
+ * set, so there is nothing to clear here: remove a placeholder with
4621
+ * `removePanel(id)`.
4622
+ */
4623
+ clearPendingViewState() {}
4624
+ /** The current layout, folding live split weights from the DOM. */
4625
+ _captureLayout() {
4483
4626
  this._foldLiveWeights();
4484
4627
  return serializeLayout(this._model);
4485
4628
  }
4486
- loadLayout(layout) {
4487
- this._returnAllToolbars();
4488
- this._loadGeneration++;
4489
- this._model = normalize(cloneNode(layout.root));
4490
- this._mergePoolIntoModel();
4491
- this._liveStacks = snapshotNonEmptyStacks(this._model);
4492
- this.requestUpdate();
4493
- this._emitChange();
4629
+ /** Whether `panelId` is in the model only as a placeholder from a restored layout. */
4630
+ _isPlaceholder(panelId) {
4631
+ return !this._pool.some((p) => p.panelId === panelId) && collectPanelIds(this._model).includes(panelId);
4494
4632
  }
4495
4633
  /**
4496
4634
  * Dock a panel. Returns false (without touching the DOM) when the
@@ -4537,11 +4675,12 @@ let DockHostElement = class DockHostElement extends LitElement {
4537
4675
  if (panel.collapsible !== void 0) el.collapsible = panel.collapsible;
4538
4676
  if (panel.kind !== void 0) el.kind = panel.kind;
4539
4677
  }
4678
+ const takesSavedPlace = target === void 0 && this._isPlaceholder(el.panelId);
4540
4679
  if (el.parentElement !== this) this.appendChild(el);
4541
4680
  if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
4542
4681
  this._returnAllToolbars();
4543
- if (collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
4544
- if (splitTarget !== null) insertBeside(this._model, splitTarget, opToDirection(op), createStack([el.panelId]), `after`);
4682
+ if (!takesSavedPlace && collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
4683
+ if (takesSavedPlace) {} else if (splitTarget !== null) insertBeside(this._model, splitTarget, opToDirection(op), createStack([el.panelId]), `after`);
4545
4684
  else if (stackPath !== null) {
4546
4685
  const stack = findNode(this._model, stackPath);
4547
4686
  if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target?.index);
@@ -4582,6 +4721,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4582
4721
  if (stack === null || !isStack(stack)) return false;
4583
4722
  if (!setActive(stack, panelId)) return false;
4584
4723
  this.requestUpdate();
4724
+ this.#viewState.notify();
4585
4725
  this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
4586
4726
  detail: { panelId },
4587
4727
  bubbles: true,
@@ -4641,7 +4781,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4641
4781
  * {@link setLeafPlacementPolicy}'s `allowEdgeSplit` (default `true`).
4642
4782
  * Pass `[]` to seal the leaf (nothing admitted). Warns when no stack
4643
4783
  * currently carries `name`, but the rule is still stored and applies once
4644
- * one appears. Rules are host policy: kept across `loadLayout()`, never
4784
+ * one appears. Rules are host policy: kept across `setViewState()`, never
4645
4785
  * serialized.
4646
4786
  */
4647
4787
  setLeafExclusiveContents(name, kinds) {
@@ -4688,7 +4828,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4688
4828
  * unconstrained* leaf — the policy is not propagated to it.
4689
4829
  *
4690
4830
  * Warns when no stack currently carries `name`; the policy is stored and
4691
- * applies once one appears. Host policy: kept across `loadLayout()`, never
4831
+ * applies once one appears. Host policy: kept across `setViewState()`, never
4692
4832
  * serialized.
4693
4833
  */
4694
4834
  setLeafPlacementPolicy(name, policy) {
@@ -4837,7 +4977,8 @@ let DockHostElement = class DockHostElement extends LitElement {
4837
4977
  * already docked here or its kind is refused by `acceptsKinds`.
4838
4978
  */
4839
4979
  acceptExternalPanel(snapshot, at) {
4840
- if (collectPanelIds(this._model).includes(snapshot.panelId)) return null;
4980
+ const placeholder = this._isPlaceholder(snapshot.panelId);
4981
+ if (!placeholder && collectPanelIds(this._model).includes(snapshot.panelId)) return null;
4841
4982
  if (this.acceptsKinds !== null && !this.acceptsKinds.includes(normalizeKind(snapshot.kind))) return null;
4842
4983
  const el = document.createElement(`ixfx-dock-panel`);
4843
4984
  el.panelId = snapshot.panelId;
@@ -4850,7 +4991,7 @@ let DockHostElement = class DockHostElement extends LitElement {
4850
4991
  if (`contentHTML` in snapshot && snapshot.contentHTML) el.innerHTML = snapshot.contentHTML;
4851
4992
  this._returnAllToolbars();
4852
4993
  this._pool = [...this._pool, el];
4853
- this._appendPanelToFirstStack(el.panelId);
4994
+ if (!placeholder) this._appendPanelToFirstStack(el.panelId);
4854
4995
  this.appendChild(el);
4855
4996
  normalize(this._model);
4856
4997
  this.endExternalDrop();
@@ -5346,10 +5487,11 @@ let DockHostElement = class DockHostElement extends LitElement {
5346
5487
  hostEmpty: now.size === 0
5347
5488
  });
5348
5489
  }
5349
- _emitChange() {
5350
- const layout = this.saveLayout();
5490
+ _emitChange(reason = `user`) {
5491
+ const layout = this._captureLayout();
5351
5492
  this._announceEmptiedStacks();
5352
5493
  this._emit(`dock-change`, { layout });
5494
+ this.#viewState.notify(reason);
5353
5495
  }
5354
5496
  render() {
5355
5497
  return html`
@@ -5385,6 +5527,7 @@ let DockHostElement = class DockHostElement extends LitElement {
5385
5527
  return html`
5386
5528
  <ixfx-split-layout
5387
5529
  class="split"
5530
+ forget-absent
5388
5531
  direction=${node.direction}
5389
5532
  size=${size === void 0 ? nothing : live(String(size))}
5390
5533
  style=${`--split-gutter-size: var(--dock-split-bar-size, 2px); --split-collapsed-size: var(--dock-collapsed-size, 40px)`}
@@ -5395,6 +5538,7 @@ let DockHostElement = class DockHostElement extends LitElement {
5395
5538
  @ixfx-split-collapse=${(e) => this._onSplitCollapsed(e, true)}
5396
5539
  @ixfx-split-expand=${(e) => this._onSplitCollapsed(e, false)}
5397
5540
  @ixfx-split-close=${this._onSplitClose}
5541
+ @view-state-change=${this._onInnerViewState}
5398
5542
  >
5399
5543
  ${node.children.map((child, i) => this._renderNode(child, [...path, i], node.sizes[i]))}
5400
5544
  </ixfx-split-layout>
@@ -6768,269 +6912,6 @@ function createGestureHandlers(host) {
6768
6912
  };
6769
6913
  }
6770
6914
  //#endregion
6771
- //#region src/hyperbolic-tree/hyperbolictree-styles.ts
6772
- /** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
6773
- const hyperbolicTreeStyles = css`
6774
- :host {
6775
- --hyperbolictree-node-color: var(--surface-4);
6776
- --hyperbolictree-node-text: var(--surface-3-text);
6777
- --hyperbolictree-edge-color: var(--surface-6);
6778
- --hyperbolictree-boundary-color: var(--border-subtle);
6779
- /* Real screen px — converted to SVG units from the element's rendered size. */
6780
- --hyperbolictree-font-size: 13px;
6781
- --hyperbolictree-box-color: var(--hyperbolictree-node-color);
6782
- --hyperbolictree-box-text: var(--surface-4-text);
6783
- --hyperbolictree-box-border: var(--surface-2);
6784
- --hyperbolictree-branch-hue-start: 250;
6785
- --hyperbolictree-branch-chroma: 0.13;
6786
- --hyperbolictree-branch-lightness: 62%;
6787
- --hyperbolictree-branch-mix: 55%;
6788
-
6789
- display: block;
6790
- position: relative;
6791
- overflow: hidden;
6792
- outline: none;
6793
- overscroll-behavior: contain;
6794
- }
6795
-
6796
- /*
6797
- * delegatesFocus means the host itself reports as focused whenever
6798
- * the shadow DOM's focused descendant (the svg) is — without this,
6799
- * the host also picks up the browser's own default focus ring/fill
6800
- * sized to the whole box.
6801
- */
6802
- :host(:focus),
6803
- :host(:focus-visible) {
6804
- outline: none;
6805
- }
6806
-
6807
- .hyperbolictree {
6808
- display: block;
6809
- width: 100%;
6810
- height: 100%;
6811
- outline: none;
6812
- touch-action: none;
6813
- overscroll-behavior: contain;
6814
- }
6815
-
6816
- .disk-boundary {
6817
- fill: none;
6818
- stroke: var(--hyperbolictree-boundary-color);
6819
- stroke-width: 1;
6820
- }
6821
-
6822
- .edge {
6823
- fill: none;
6824
- stroke: var(--hyperbolictree-edge-color);
6825
- /* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
6826
- stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
6827
- /* Constant on-screen width, however far the disk is zoomed. */
6828
- vector-effect: non-scaling-stroke;
6829
- }
6830
-
6831
- .node {
6832
- cursor: pointer;
6833
- }
6834
-
6835
- /*
6836
- * color-by-branch: each top-level branch gets an evenly-spaced hue,
6837
- * mixed into the normal node/box/edge colour (rather than replacing it)
6838
- * so it stays in keeping with — and legible in — the current theme.
6839
- * Only sets the base colour vars, so selected/tickled states still win.
6840
- */
6841
- :host([color-by-branch]) .node,
6842
- :host([color-by-branch]) .edge {
6843
- --_branch-color: oklch(
6844
- var(--hyperbolictree-branch-lightness)
6845
- var(--hyperbolictree-branch-chroma)
6846
- calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
6847
- );
6848
- }
6849
-
6850
- :host([color-by-branch]) .node {
6851
- --_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
6852
- --_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
6853
- }
6854
-
6855
- :host([color-by-branch]) .edge {
6856
- stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
6857
- }
6858
-
6859
- .node-hit {
6860
- fill: transparent;
6861
- pointer-events: all;
6862
- }
6863
-
6864
- .node-dot {
6865
- /* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
6866
- fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
6867
- stroke: var(--surface-2);
6868
- stroke-width: 1;
6869
- pointer-events: none;
6870
- }
6871
-
6872
- .node-box {
6873
- fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
6874
- stroke: var(--hyperbolictree-box-border);
6875
- stroke-width: 1.5;
6876
- /* Keep the border crisp however far the box has been scaled down. */
6877
- vector-effect: non-scaling-stroke;
6878
- }
6879
-
6880
- /* Text-less stand-in for a box too small, or too crowded, to label. */
6881
- .node-box.marker {
6882
- stroke-width: 1;
6883
- }
6884
-
6885
- /* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
6886
- .hyperbolictree-label {
6887
- fill: var(--hyperbolictree-node-text);
6888
- cursor: pointer;
6889
- user-select: none;
6890
- }
6891
-
6892
- .box-label {
6893
- fill: var(--hyperbolictree-box-text);
6894
- pointer-events: none;
6895
- }
6896
-
6897
- /*
6898
- * State fills come paired with their theme text tokens: a box label sits
6899
- * on the state fill itself, so e.g. --item-bg-selected needs
6900
- * --item-text-selected on top, not the resting box text colour.
6901
- */
6902
- .node.tickled .node-dot,
6903
- .node.tickled .node-box {
6904
- fill: var(--item-bg-tickled);
6905
- }
6906
-
6907
- .node.tickled .box-label {
6908
- fill: var(--item-text-tickled);
6909
- }
6910
-
6911
- .node.selected .node-dot,
6912
- .node.selected .node-box {
6913
- fill: var(--item-bg-selected);
6914
- }
6915
-
6916
- .node.selected .box-label {
6917
- fill: var(--item-text-selected);
6918
- }
6919
-
6920
- .node.selected.tickled .node-dot,
6921
- .node.selected.tickled .node-box {
6922
- fill: var(--item-bg-selected-tickled);
6923
- }
6924
-
6925
- .node.focused .node-dot,
6926
- .node.focused .node-box {
6927
- stroke: var(--accent);
6928
- stroke-width: 3;
6929
- }
6930
-
6931
- /* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
6932
- .node.tickled .hyperbolictree-label:not(.box-label) {
6933
- paint-order: stroke;
6934
- stroke: var(--surface-2);
6935
- stroke-width: 4px;
6936
- stroke-linejoin: round;
6937
- }
6938
-
6939
- .node.filtered {
6940
- opacity: 0.3;
6941
- }
6942
-
6943
- .node.loading .node-dot,
6944
- .node.loading .node-box {
6945
- opacity: 0.6;
6946
- }
6947
-
6948
-
6949
- /* Child preview: a clickable list of a crowded node's children. */
6950
- .child-preview {
6951
- position: absolute;
6952
- z-index: 1;
6953
- min-width: 8rem;
6954
- max-width: min(18rem, 60%);
6955
- max-height: 45%;
6956
- overflow: auto;
6957
- padding: var(--space-xs, 4px);
6958
- background: var(--surface-3);
6959
- color: var(--surface-3-text);
6960
- border: var(--border-subtle);
6961
- border-radius: var(--radius-m, 6px);
6962
- box-shadow: var(--shadow-m);
6963
- font-size: var(--hyperbolictree-font-size);
6964
- /* Beside the node, never over it, with the dot level with the popover's middle. */
6965
- transform: translate(14px, -50%);
6966
- }
6967
-
6968
- .child-preview.left {
6969
- transform: translate(calc(-100% - 14px), -50%);
6970
- }
6971
-
6972
- .child-preview.below {
6973
- transform: translate(-50%, 14px);
6974
- }
6975
-
6976
- .child-preview.above {
6977
- transform: translate(-50%, calc(-100% - 14px));
6978
- }
6979
-
6980
- .child-preview-title {
6981
- display: flex;
6982
- align-items: center;
6983
- gap: 4px;
6984
- padding: 2px 6px 4px;
6985
- font-weight: 600;
6986
- }
6987
-
6988
- .child-preview-title span {
6989
- overflow: hidden;
6990
- text-overflow: ellipsis;
6991
- white-space: nowrap;
6992
- }
6993
-
6994
- .child-preview button.child-preview-up {
6995
- display: inline-flex;
6996
- flex: none;
6997
- width: auto;
6998
- padding: 2px;
6999
- }
7000
-
7001
- .child-preview ul {
7002
- margin: 0;
7003
- padding: 0;
7004
- list-style: none;
7005
- }
7006
-
7007
- .child-preview button {
7008
- display: block;
7009
- width: 100%;
7010
- padding: 2px 6px;
7011
- border: none;
7012
- border-radius: var(--radius-s, 4px);
7013
- background: none;
7014
- color: inherit;
7015
- font: inherit;
7016
- text-align: start;
7017
- overflow: hidden;
7018
- text-overflow: ellipsis;
7019
- white-space: nowrap;
7020
- cursor: pointer;
7021
- }
7022
-
7023
- .child-preview button:hover {
7024
- background: var(--item-bg-tickled);
7025
- color: var(--item-text-tickled);
7026
- }
7027
-
7028
- .child-preview-more {
7029
- padding: 2px 6px;
7030
- color: var(--surface-muted-text);
7031
- }
7032
- `;
7033
- //#endregion
7034
6915
  //#region src/hyperbolic-tree/layout.ts
7035
6916
  const ID_SEPARATOR = `\u001F`;
7036
6917
  /**
@@ -7445,6 +7326,564 @@ function computeHyperbolicLayout(root, options = {}) {
7445
7326
  };
7446
7327
  }
7447
7328
  //#endregion
7329
+ //#region src/hyperbolic-tree/hyperbolic-view-state.ts
7330
+ function indexHyperbolicKeys(root) {
7331
+ const byKey = /* @__PURE__ */ new Map();
7332
+ const nodes = /* @__PURE__ */ new Set();
7333
+ const walk = (node) => {
7334
+ nodes.add(node);
7335
+ if (!byKey.has(node.item.key)) byKey.set(node.item.key, node);
7336
+ for (const child of node.children ?? []) walk(child);
7337
+ };
7338
+ if (root) walk(root);
7339
+ return {
7340
+ byKey,
7341
+ nodes
7342
+ };
7343
+ }
7344
+ /** Key path from the root's children to the node with layout id `id`. */
7345
+ function centerPathOf(root, id) {
7346
+ if (id === void 0) return [];
7347
+ return findHyperbolicPathById(root, id)?.slice(1).map((n) => n.item.key) ?? [];
7348
+ }
7349
+ /** Walk `path` from the root, matching each step by key among the loaded children. */
7350
+ function resolveCenterPath(root, path) {
7351
+ if (!root) return { kind: `missing` };
7352
+ let node = root;
7353
+ let id = root.item.key;
7354
+ for (const [depth, key] of path.entries()) {
7355
+ const children = node.children;
7356
+ if (children === void 0) return node.item.isLeaf === true ? { kind: `missing` } : {
7357
+ kind: `load`,
7358
+ node,
7359
+ depth
7360
+ };
7361
+ const index = children.findIndex((c) => c.item.key === key);
7362
+ if (index < 0) return { kind: `missing` };
7363
+ id = hyperbolicChildIds(id, children)[index];
7364
+ node = children[index];
7365
+ }
7366
+ return {
7367
+ kind: `found`,
7368
+ id: path.length === 0 ? void 0 : id
7369
+ };
7370
+ }
7371
+ /**
7372
+ * As `reconcileSelection` in `tree/tree-view-state.ts`, but a selected node
7373
+ * still in the tree is kept by identity — keys may repeat here, so re-keying
7374
+ * it would jump the selection to the first node sharing its key on every
7375
+ * lazy load. Only nodes that left the tree are re-keyed (or go pending).
7376
+ */
7377
+ function reconcileHyperbolicSelection(selected, pending, index, canSelect, single) {
7378
+ if (selected.size === 0 && pending.size === 0) return {
7379
+ selected,
7380
+ pending,
7381
+ restored: false,
7382
+ changed: false
7383
+ };
7384
+ const next = /* @__PURE__ */ new Set();
7385
+ const nextPending = /* @__PURE__ */ new Set();
7386
+ let changed = false;
7387
+ for (const node of selected) {
7388
+ if (index.nodes.has(node)) {
7389
+ next.add(node);
7390
+ continue;
7391
+ }
7392
+ changed = true;
7393
+ const current = index.byKey.get(node.item.key);
7394
+ if (current) next.add(current);
7395
+ else nextPending.add(node.item.key);
7396
+ }
7397
+ let restored;
7398
+ for (const key of pending) {
7399
+ const node = index.byKey.get(key);
7400
+ if (node && canSelect(node)) {
7401
+ next.add(node);
7402
+ restored = node;
7403
+ changed = true;
7404
+ } else nextPending.add(key);
7405
+ }
7406
+ if (!changed) return {
7407
+ selected,
7408
+ pending,
7409
+ restored: false,
7410
+ changed: false
7411
+ };
7412
+ if (single && next.size > 0) return {
7413
+ selected: /* @__PURE__ */ new Set([restored ?? [...next].at(-1)]),
7414
+ pending: EMPTY_KEYS,
7415
+ restored: restored !== void 0,
7416
+ changed: true
7417
+ };
7418
+ return {
7419
+ selected: next,
7420
+ pending: capKeys(nextPending),
7421
+ restored: restored !== void 0,
7422
+ changed: true
7423
+ };
7424
+ }
7425
+ /**
7426
+ * Centre: a live centre whose path no longer resolves goes pending (the view
7427
+ * falls back to the root); one that resolves to a new id is re-seated; a
7428
+ * pending path that now resolves is applied.
7429
+ */
7430
+ function reconcileHyperbolicViewState(input) {
7431
+ const { root } = input;
7432
+ let center;
7433
+ let pendingCenter = input.pendingCenter;
7434
+ if (input.focusId !== void 0) {
7435
+ const live = resolveCenterPath(root, input.centerPath);
7436
+ if (live.kind !== `found` || live.id === void 0) {
7437
+ pendingCenter ??= input.centerPath;
7438
+ center = {
7439
+ id: void 0,
7440
+ path: [],
7441
+ restored: false
7442
+ };
7443
+ } else if (live.id !== input.focusId) center = {
7444
+ id: live.id,
7445
+ path: input.centerPath,
7446
+ restored: false
7447
+ };
7448
+ }
7449
+ if (pendingCenter) {
7450
+ const resolved = resolveCenterPath(root, pendingCenter);
7451
+ if (resolved.kind === `found`) {
7452
+ center = {
7453
+ id: resolved.id,
7454
+ path: pendingCenter,
7455
+ restored: true
7456
+ };
7457
+ pendingCenter = void 0;
7458
+ }
7459
+ }
7460
+ const selection = reconcileHyperbolicSelection(input.selected, input.pendingSelected, input.index, input.canSelect, input.singleSelect);
7461
+ return {
7462
+ center,
7463
+ pendingCenter,
7464
+ selection
7465
+ };
7466
+ }
7467
+ /** Sanitise a JSON zoom: a finite positive number, else `undefined`. */
7468
+ function zoomFromJson(value) {
7469
+ return typeof value === `number` && Number.isFinite(value) && value > 0 ? value : void 0;
7470
+ }
7471
+ //#endregion
7472
+ //#region src/hyperbolic-tree/hyperbolic-view-state-binding.ts
7473
+ function createHyperbolicViewState(host) {
7474
+ const { controller } = host;
7475
+ /** Key path of the centre as last seen — what goes pending if it leaves the tree. */
7476
+ let centerPath = [];
7477
+ let pendingCenter;
7478
+ let pendingSelected = EMPTY_KEYS;
7479
+ let pendingFocus;
7480
+ /** Where keyboard / search navigation last put the cursor. */
7481
+ let keyboardCursor;
7482
+ let structureVersion = 0;
7483
+ let reconciledVersion = -1;
7484
+ let cache;
7485
+ const index = () => {
7486
+ if (cache?.version !== structureVersion) cache = {
7487
+ version: structureVersion,
7488
+ index: indexHyperbolicKeys(host.root())
7489
+ };
7490
+ return cache.index;
7491
+ };
7492
+ /** Key of the keyboard cursor, while the cursor is still where the keyboard put it. */
7493
+ const cursorKey = () => keyboardCursor && keyboardCursor.id === host.tickled() ? keyboardCursor.key : void 0;
7494
+ const applyCenter = (id, path, restored) => {
7495
+ centerPath = path;
7496
+ if (host.focusId() === id) return;
7497
+ host.setCenter(id, restored);
7498
+ controller.notify();
7499
+ };
7500
+ const reconcileTickled = (idx) => {
7501
+ const tickled = host.tickled();
7502
+ const alive = tickled !== void 0 && findHyperbolicPathById(host.root(), tickled) !== void 0;
7503
+ const cursor = reconcileCursor(cursorKey(), pendingFocus, (key) => alive && idx.byKey.has(key), (key) => host.visibleIdOf(key) !== void 0);
7504
+ pendingFocus = cursor.pending;
7505
+ if (cursor.clear || tickled !== void 0 && !alive) host.setTickled(void 0);
7506
+ const id = cursor.apply === void 0 ? void 0 : host.visibleIdOf(cursor.apply);
7507
+ if (cursor.apply !== void 0 && id !== void 0) {
7508
+ host.setTickled(id);
7509
+ keyboardCursor = {
7510
+ id,
7511
+ key: cursor.apply
7512
+ };
7513
+ controller.notify();
7514
+ }
7515
+ };
7516
+ /** Move state for missing keys to pending; apply pending state for keys now loaded. */
7517
+ const reconcile = () => {
7518
+ reconciledVersion = structureVersion;
7519
+ const root = host.root();
7520
+ if (!root) return;
7521
+ const idx = index();
7522
+ controller.restoring(() => {
7523
+ const r = reconcileHyperbolicViewState({
7524
+ root,
7525
+ index: idx,
7526
+ focusId: host.focusId(),
7527
+ centerPath,
7528
+ pendingCenter,
7529
+ selected: host.selected(),
7530
+ pendingSelected,
7531
+ canSelect: host.canSelect,
7532
+ singleSelect: host.singleSelect()
7533
+ });
7534
+ pendingCenter = r.pendingCenter;
7535
+ if (r.center) applyCenter(r.center.id, r.center.path, r.center.restored);
7536
+ const sel = r.selection;
7537
+ if (sel.changed) {
7538
+ pendingSelected = sel.pending;
7539
+ const primary = host.selectedNode();
7540
+ host.setSelection(sel.selected, primary && sel.selected.has(primary) ? primary : [...sel.selected].at(-1), sel.restored);
7541
+ }
7542
+ reconcileTickled(idx);
7543
+ });
7544
+ };
7545
+ return {
7546
+ /** The structure changed (new root / model, invalidation, lazy load). */
7547
+ invalidate() {
7548
+ structureVersion++;
7549
+ },
7550
+ /** Run in `willUpdate`. `centerGone` — the centre vanished through in-place mutation. */
7551
+ reconcileIfNeeded(centerGone) {
7552
+ if (centerGone) structureVersion++;
7553
+ if (reconciledVersion !== structureVersion) reconcile();
7554
+ },
7555
+ /** An explicit centre change: supersedes a saved one still waiting. */
7556
+ centred(id) {
7557
+ pendingCenter = void 0;
7558
+ centerPath = centerPathOf(host.root(), id);
7559
+ controller.notify();
7560
+ },
7561
+ /** Selection changed — explicit changes supersede a saved selection still waiting. */
7562
+ selectionChanged() {
7563
+ if (!controller.isRestoring) pendingSelected = EMPTY_KEYS;
7564
+ controller.notify();
7565
+ },
7566
+ clearPendingSelection() {
7567
+ pendingSelected = EMPTY_KEYS;
7568
+ },
7569
+ /** The cursor moved by keyboard / search navigation. */
7570
+ keyboardCursorMoved(id, key) {
7571
+ keyboardCursor = {
7572
+ id,
7573
+ key
7574
+ };
7575
+ pendingFocus = void 0;
7576
+ controller.notify();
7577
+ },
7578
+ /** A user-visible change with no pending facet (e.g. a zoom gesture). */
7579
+ notify() {
7580
+ controller.notify();
7581
+ },
7582
+ get(options) {
7583
+ const withPending = options?.includePending ?? true;
7584
+ return controller.shape({
7585
+ selected: keysToArray([...host.selected()].map((n) => n.item.key), withPending ? pendingSelected : []),
7586
+ focus: cursorKey() ?? (withPending ? pendingFocus : void 0),
7587
+ center: [...(withPending ? pendingCenter : void 0) ?? centerPath],
7588
+ zoom: host.zoom()
7589
+ }, options);
7590
+ },
7591
+ set(state) {
7592
+ if (!controller.accepts(state)) return;
7593
+ controller.restoring(() => {
7594
+ if (state.selected) {
7595
+ pendingSelected = capKeys(new Set(state.selected));
7596
+ if (host.selected().size > 0) host.setSelection(/* @__PURE__ */ new Set(), void 0, true);
7597
+ }
7598
+ if (state.focus !== void 0) pendingFocus = state.focus;
7599
+ const zoom = zoomFromJson(state.zoom);
7600
+ if (zoom !== void 0 && zoom !== host.zoom()) {
7601
+ host.setZoom(zoom);
7602
+ controller.notify();
7603
+ }
7604
+ const center = pathFromJson(state.center);
7605
+ if (center) pendingCenter = center;
7606
+ reconcile();
7607
+ if (center && pendingCenter) applyCenter(void 0, [], true);
7608
+ });
7609
+ },
7610
+ clearPending() {
7611
+ pendingSelected = EMPTY_KEYS;
7612
+ pendingFocus = void 0;
7613
+ pendingCenter = void 0;
7614
+ },
7615
+ /** Load the next unloaded ancestor on a pending centre path (one load at a time). */
7616
+ continueRestoreLoads() {
7617
+ if (!pendingCenter || !host.restoreLoads()) return;
7618
+ const resolved = resolveCenterPath(host.root(), pendingCenter);
7619
+ if (resolved.kind === `load`) host.load(resolved.node, resolved.depth);
7620
+ }
7621
+ };
7622
+ }
7623
+ //#endregion
7624
+ //#region src/hyperbolic-tree/hyperbolictree-styles.ts
7625
+ /** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
7626
+ const hyperbolicTreeStyles = css`
7627
+ :host {
7628
+ --hyperbolictree-node-color: var(--surface-4);
7629
+ --hyperbolictree-node-text: var(--surface-3-text);
7630
+ --hyperbolictree-edge-color: var(--surface-6);
7631
+ --hyperbolictree-boundary-color: var(--border-subtle);
7632
+ /* Real screen px — converted to SVG units from the element's rendered size. */
7633
+ --hyperbolictree-font-size: 13px;
7634
+ --hyperbolictree-box-color: var(--hyperbolictree-node-color);
7635
+ --hyperbolictree-box-text: var(--surface-4-text);
7636
+ --hyperbolictree-box-border: var(--surface-2);
7637
+ --hyperbolictree-branch-hue-start: 250;
7638
+ --hyperbolictree-branch-chroma: 0.13;
7639
+ --hyperbolictree-branch-lightness: 62%;
7640
+ --hyperbolictree-branch-mix: 55%;
7641
+
7642
+ display: block;
7643
+ position: relative;
7644
+ overflow: hidden;
7645
+ outline: none;
7646
+ overscroll-behavior: contain;
7647
+ }
7648
+
7649
+ /*
7650
+ * delegatesFocus means the host itself reports as focused whenever
7651
+ * the shadow DOM's focused descendant (the svg) is — without this,
7652
+ * the host also picks up the browser's own default focus ring/fill
7653
+ * sized to the whole box.
7654
+ */
7655
+ :host(:focus),
7656
+ :host(:focus-visible) {
7657
+ outline: none;
7658
+ }
7659
+
7660
+ .hyperbolictree {
7661
+ display: block;
7662
+ width: 100%;
7663
+ height: 100%;
7664
+ outline: none;
7665
+ touch-action: none;
7666
+ overscroll-behavior: contain;
7667
+ }
7668
+
7669
+ .disk-boundary {
7670
+ fill: none;
7671
+ stroke: var(--hyperbolictree-boundary-color);
7672
+ stroke-width: 1;
7673
+ }
7674
+
7675
+ .edge {
7676
+ fill: none;
7677
+ stroke: var(--hyperbolictree-edge-color);
7678
+ /* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
7679
+ stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
7680
+ /* Constant on-screen width, however far the disk is zoomed. */
7681
+ vector-effect: non-scaling-stroke;
7682
+ }
7683
+
7684
+ .node {
7685
+ cursor: pointer;
7686
+ }
7687
+
7688
+ /*
7689
+ * color-by-branch: each top-level branch gets an evenly-spaced hue,
7690
+ * mixed into the normal node/box/edge colour (rather than replacing it)
7691
+ * so it stays in keeping with — and legible in — the current theme.
7692
+ * Only sets the base colour vars, so selected/tickled states still win.
7693
+ */
7694
+ :host([color-by-branch]) .node,
7695
+ :host([color-by-branch]) .edge {
7696
+ --_branch-color: oklch(
7697
+ var(--hyperbolictree-branch-lightness)
7698
+ var(--hyperbolictree-branch-chroma)
7699
+ calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
7700
+ );
7701
+ }
7702
+
7703
+ :host([color-by-branch]) .node {
7704
+ --_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
7705
+ --_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
7706
+ }
7707
+
7708
+ :host([color-by-branch]) .edge {
7709
+ stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
7710
+ }
7711
+
7712
+ .node-hit {
7713
+ fill: transparent;
7714
+ pointer-events: all;
7715
+ }
7716
+
7717
+ .node-dot {
7718
+ /* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
7719
+ fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
7720
+ stroke: var(--surface-2);
7721
+ stroke-width: 1;
7722
+ pointer-events: none;
7723
+ }
7724
+
7725
+ .node-box {
7726
+ fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
7727
+ stroke: var(--hyperbolictree-box-border);
7728
+ stroke-width: 1.5;
7729
+ /* Keep the border crisp however far the box has been scaled down. */
7730
+ vector-effect: non-scaling-stroke;
7731
+ }
7732
+
7733
+ /* Text-less stand-in for a box too small, or too crowded, to label. */
7734
+ .node-box.marker {
7735
+ stroke-width: 1;
7736
+ }
7737
+
7738
+ /* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
7739
+ .hyperbolictree-label {
7740
+ fill: var(--hyperbolictree-node-text);
7741
+ cursor: pointer;
7742
+ user-select: none;
7743
+ }
7744
+
7745
+ .box-label {
7746
+ fill: var(--hyperbolictree-box-text);
7747
+ pointer-events: none;
7748
+ }
7749
+
7750
+ /*
7751
+ * State fills come paired with their theme text tokens: a box label sits
7752
+ * on the state fill itself, so e.g. --item-bg-selected needs
7753
+ * --item-text-selected on top, not the resting box text colour.
7754
+ */
7755
+ .node.tickled .node-dot,
7756
+ .node.tickled .node-box {
7757
+ fill: var(--item-bg-tickled);
7758
+ }
7759
+
7760
+ .node.tickled .box-label {
7761
+ fill: var(--item-text-tickled);
7762
+ }
7763
+
7764
+ .node.selected .node-dot,
7765
+ .node.selected .node-box {
7766
+ fill: var(--item-bg-selected);
7767
+ }
7768
+
7769
+ .node.selected .box-label {
7770
+ fill: var(--item-text-selected);
7771
+ }
7772
+
7773
+ .node.selected.tickled .node-dot,
7774
+ .node.selected.tickled .node-box {
7775
+ fill: var(--item-bg-selected-tickled);
7776
+ }
7777
+
7778
+ .node.focused .node-dot,
7779
+ .node.focused .node-box {
7780
+ stroke: var(--accent);
7781
+ stroke-width: 3;
7782
+ }
7783
+
7784
+ /* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
7785
+ .node.tickled .hyperbolictree-label:not(.box-label) {
7786
+ paint-order: stroke;
7787
+ stroke: var(--surface-2);
7788
+ stroke-width: 4px;
7789
+ stroke-linejoin: round;
7790
+ }
7791
+
7792
+ .node.filtered {
7793
+ opacity: 0.3;
7794
+ }
7795
+
7796
+ .node.loading .node-dot,
7797
+ .node.loading .node-box {
7798
+ opacity: 0.6;
7799
+ }
7800
+
7801
+
7802
+ /* Child preview: a clickable list of a crowded node's children. */
7803
+ .child-preview {
7804
+ position: absolute;
7805
+ z-index: 1;
7806
+ min-width: 8rem;
7807
+ max-width: min(18rem, 60%);
7808
+ max-height: 45%;
7809
+ overflow: auto;
7810
+ padding: var(--space-xs, 4px);
7811
+ background: var(--surface-3);
7812
+ color: var(--surface-3-text);
7813
+ border: var(--border-subtle);
7814
+ border-radius: var(--radius-m, 6px);
7815
+ box-shadow: var(--shadow-m);
7816
+ font-size: var(--hyperbolictree-font-size);
7817
+ /* Beside the node, never over it, with the dot level with the popover's middle. */
7818
+ transform: translate(14px, -50%);
7819
+ }
7820
+
7821
+ .child-preview.left {
7822
+ transform: translate(calc(-100% - 14px), -50%);
7823
+ }
7824
+
7825
+ .child-preview.below {
7826
+ transform: translate(-50%, 14px);
7827
+ }
7828
+
7829
+ .child-preview.above {
7830
+ transform: translate(-50%, calc(-100% - 14px));
7831
+ }
7832
+
7833
+ .child-preview-title {
7834
+ display: flex;
7835
+ align-items: center;
7836
+ gap: 4px;
7837
+ padding: 2px 6px 4px;
7838
+ font-weight: 600;
7839
+ }
7840
+
7841
+ .child-preview-title span {
7842
+ overflow: hidden;
7843
+ text-overflow: ellipsis;
7844
+ white-space: nowrap;
7845
+ }
7846
+
7847
+ .child-preview button.child-preview-up {
7848
+ display: inline-flex;
7849
+ flex: none;
7850
+ width: auto;
7851
+ padding: 2px;
7852
+ }
7853
+
7854
+ .child-preview ul {
7855
+ margin: 0;
7856
+ padding: 0;
7857
+ list-style: none;
7858
+ }
7859
+
7860
+ .child-preview button {
7861
+ display: block;
7862
+ width: 100%;
7863
+ padding: 2px 6px;
7864
+ border: none;
7865
+ border-radius: var(--radius-s, 4px);
7866
+ background: none;
7867
+ color: inherit;
7868
+ font: inherit;
7869
+ text-align: start;
7870
+ overflow: hidden;
7871
+ text-overflow: ellipsis;
7872
+ white-space: nowrap;
7873
+ cursor: pointer;
7874
+ }
7875
+
7876
+ .child-preview button:hover {
7877
+ background: var(--item-bg-tickled);
7878
+ color: var(--item-text-tickled);
7879
+ }
7880
+
7881
+ .child-preview-more {
7882
+ padding: 2px 6px;
7883
+ color: var(--surface-muted-text);
7884
+ }
7885
+ `;
7886
+ //#endregion
7448
7887
  //#region src/hyperbolic-tree/keyboard.ts
7449
7888
  const unhandled = {
7450
7889
  action: { kind: `none` },
@@ -7543,6 +7982,11 @@ function createLazyChildLoader(source, onChange) {
7543
7982
  if (!model && !loadChildren) return;
7544
7983
  for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
7545
7984
  },
7985
+ /** Start loading `node`'s children unless loaded or already loading (view-state restore). */
7986
+ loadOne(node, depth) {
7987
+ const { model, loadChildren } = source();
7988
+ if ((model || loadChildren) && node.children === void 0 && !pending.has(node)) load(node, depth);
7989
+ },
7546
7990
  isPending: (node) => pending.has(node),
7547
7991
  abortAll() {
7548
7992
  for (const aborter of aborters.values()) aborter.abort();
@@ -8063,6 +8507,35 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8063
8507
  this.disableKeyboardNavigation = false;
8064
8508
  this.registry = void 0;
8065
8509
  this._focusId = void 0;
8510
+ this.restoreLoads = true;
8511
+ this.#viewState = createHyperbolicViewState({
8512
+ controller: new ViewStateController(this, {
8513
+ kind: `hyperbolictree-list`,
8514
+ capture: () => this.getViewState()
8515
+ }),
8516
+ root: () => this.#effectiveRoot,
8517
+ focusId: () => this._focusId,
8518
+ setCenter: (id, restored) => this.#restoreCenter(id, restored),
8519
+ selected: () => this._selectedNodes,
8520
+ selectedNode: () => this.selectedNode,
8521
+ canSelect: (node) => this.#canSelect(node),
8522
+ singleSelect: () => this.interactionMode === `implicit`,
8523
+ setSelection: (next, primary, fire) => {
8524
+ const prev = this._selectedNodes;
8525
+ this._selectedNodes = new Set(next);
8526
+ this.selectedNode = primary;
8527
+ if (fire) this.#dispatchSelect(prev);
8528
+ },
8529
+ tickled: () => this.tickled.tickledId,
8530
+ setTickled: (id) => id === void 0 ? this.tickled.clearTickled() : this.tickled.setTickled(id),
8531
+ visibleIdOf: (key) => this.#layout().nodes.find((n) => n.key === key)?.id,
8532
+ zoom: () => this.zoom,
8533
+ setZoom: (zoom) => {
8534
+ this.zoom = zoom;
8535
+ },
8536
+ restoreLoads: () => this.restoreLoads,
8537
+ load: (node, depth) => this.#lazy.loadOne(node, depth)
8538
+ });
8066
8539
  this._viewOverride = void 0;
8067
8540
  this._focusRotation = 0;
8068
8541
  this._zoomOffset = {
@@ -8084,7 +8557,10 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8084
8557
  this.#lazy = createLazyChildLoader(() => ({
8085
8558
  model: this.model,
8086
8559
  loadChildren: this.loadChildren
8087
- }), () => this.requestUpdate());
8560
+ }), () => {
8561
+ this.#viewState.invalidate();
8562
+ this.requestUpdate();
8563
+ });
8088
8564
  this.#lastLabelIds = /* @__PURE__ */ new Set();
8089
8565
  this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
8090
8566
  labelSelector: `.hyperbolictree-label`,
@@ -8131,6 +8607,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8131
8607
  },
8132
8608
  zoomState: () => this.#zoomState(),
8133
8609
  setZoomState: (state) => {
8610
+ if (state.zoom !== this.zoom) this.#viewState.notify();
8134
8611
  this.zoom = state.zoom;
8135
8612
  this._zoomOffset = state.offset;
8136
8613
  },
@@ -8153,6 +8630,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8153
8630
  if (this._focusId === void 0) return this.#effectiveRoot;
8154
8631
  return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
8155
8632
  }
8633
+ #viewState;
8156
8634
  #preview;
8157
8635
  #lazy;
8158
8636
  /** Labels shown in the last render — what the child preview judges legibility by. */
@@ -8260,46 +8738,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8260
8738
  this.#unregisterSearchCommands?.();
8261
8739
  this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
8262
8740
  }
8741
+ willUpdate(changedProps) {
8742
+ if (changedProps.has(`root`) && this.root || changedProps.has(`model`)) this.#onDataReset();
8743
+ this.#viewState.reconcileIfNeeded(this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId));
8744
+ }
8263
8745
  updated(changedProps) {
8264
- if (changedProps.has(`root`) && this.root) this.#resetViewState();
8265
8746
  if (changedProps.has(`model`)) {
8266
8747
  this.#modelUnsubscribe?.();
8267
8748
  this.#modelUnsubscribe = void 0;
8268
- if (this.model) {
8269
- this.#modelUnsubscribe = this.model._subscribe((event) => {
8270
- if (event.type === `root`) {
8271
- this.#resetViewState();
8272
- this.requestUpdate();
8273
- } else if (event.type === `invalidate`) this.requestUpdate();
8274
- });
8275
- this.#resetViewState();
8276
- }
8749
+ if (this.model) this.#modelUnsubscribe = this.model._subscribe((event) => {
8750
+ if (event.type === `root`) this.#onDataReset();
8751
+ else if (event.type === `invalidate`) this.#viewState.invalidate();
8752
+ this.requestUpdate();
8753
+ });
8277
8754
  }
8278
- if (changedProps.has(`interactionMode`)) this.clearSelection();
8755
+ if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) this.clearSelection();
8279
8756
  if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
8280
- if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
8281
- this._focusId = void 0;
8282
- this._viewOverride = void 0;
8283
- this._focusRotation = 0;
8284
- }
8285
8757
  this.#lazy.ensure(this.#layout().nodes);
8758
+ this.#viewState.continueRestoreLoads();
8286
8759
  }
8287
- #resetViewState() {
8288
- this.#clearSelectionInternal();
8289
- this.tickled.clearTickled();
8290
- this._focusId = void 0;
8291
- this._viewOverride = void 0;
8292
- this._focusRotation = 0;
8293
- this._zoomOffset = {
8294
- x: 0,
8295
- y: 0
8296
- };
8760
+ /** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
8761
+ #onDataReset() {
8297
8762
  this._previewId = void 0;
8298
8763
  this.#gestures.cancel();
8299
- if (this.#animFrame !== void 0) {
8300
- cancelAnimationFrame(this.#animFrame);
8301
- this.#animFrame = void 0;
8302
- }
8764
+ this.#viewState.invalidate();
8303
8765
  }
8304
8766
  #canSelect(node) {
8305
8767
  if (this.selectionFilter === `none`) return false;
@@ -8312,6 +8774,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8312
8774
  this.selectedNode = void 0;
8313
8775
  }
8314
8776
  #dispatchSelect(previous) {
8777
+ this.#viewState.selectionChanged();
8315
8778
  this.dispatchEvent(new CustomEvent(`select`, {
8316
8779
  detail: {
8317
8780
  selected: new Set(this._selectedNodes),
@@ -8339,6 +8802,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8339
8802
  this.requestUpdate();
8340
8803
  }
8341
8804
  clearSelection() {
8805
+ this.#viewState.clearPendingSelection();
8342
8806
  if (this._selectedNodes.size === 0) return;
8343
8807
  const prev = new Set(this._selectedNodes);
8344
8808
  this.#clearSelectionInternal();
@@ -8384,6 +8848,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8384
8848
  const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
8385
8849
  const fromView = this.#viewFromFrameOf(targetId, currentId);
8386
8850
  this._focusId = targetId;
8851
+ this.#viewState.centred(targetId);
8387
8852
  this.#gestures.cancel();
8388
8853
  this._previewId = void 0;
8389
8854
  this.#animateTo(fromView);
@@ -8452,7 +8917,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8452
8917
  #focusAndMaybeSelect(node) {
8453
8918
  this.focusOn(node);
8454
8919
  const id = this.#idOf(node);
8455
- if (id !== void 0) this.tickled.setTickled(id);
8920
+ if (id !== void 0) this.#setKeyboardCursor(id, node);
8456
8921
  if (this.interactionMode === `implicit`) this.select(node);
8457
8922
  }
8458
8923
  /** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
@@ -8490,10 +8955,15 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8490
8955
  const id = node ? this.#idOf(node) : void 0;
8491
8956
  if (!node || id === void 0) return false;
8492
8957
  if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
8493
- this.tickled.setTickled(id);
8958
+ this.#setKeyboardCursor(id, node);
8494
8959
  this.requestUpdate();
8495
8960
  return true;
8496
8961
  }
8962
+ /** Move the cursor as keyboard / search navigation does — recorded as `focus` in the view state. */
8963
+ #setKeyboardCursor(id, node) {
8964
+ this.tickled.setTickled(id);
8965
+ this.#viewState.keyboardCursorMoved(id, node.item.key);
8966
+ }
8497
8967
  /**
8498
8968
  * All nodes currently laid out on the disk (within `maxRadius` of the
8499
8969
  * focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
@@ -8506,6 +8976,49 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
8506
8976
  depth: n.depth
8507
8977
  }));
8508
8978
  }
8979
+ /**
8980
+ * Snapshot selection, centred node, keyboard cursor and zoom as plain JSON,
8981
+ * keyed by `item.key`. Includes pending state for keys not currently loaded.
8982
+ */
8983
+ getViewState(options) {
8984
+ return this.#viewState.get(options);
8985
+ }
8986
+ /**
8987
+ * Apply saved view state. Fields present replace the current ones; keys
8988
+ * that aren't loaded yet are applied as they appear — a deep `center` loads
8989
+ * its ancestors via `loadChildren` / `model` when `restoreLoads`.
8990
+ */
8991
+ setViewState(state) {
8992
+ this.#viewState.set(state);
8993
+ this.requestUpdate();
8994
+ }
8995
+ clearPendingViewState() {
8996
+ this.#viewState.clearPending();
8997
+ }
8998
+ /** Centre set by view-state reconcile: snaps (no animation); `restored` fires `focus`. */
8999
+ #restoreCenter(id, restored) {
9000
+ const previous = this.focusedNode;
9001
+ this.#cancelAnimation();
9002
+ this._focusId = id;
9003
+ this._viewOverride = void 0;
9004
+ this._zoomOffset = {
9005
+ x: 0,
9006
+ y: 0
9007
+ };
9008
+ if (!restored) {
9009
+ this._focusRotation = 0;
9010
+ return;
9011
+ }
9012
+ const node = this.focusedNode;
9013
+ if (node) this.dispatchEvent(new CustomEvent(`focus`, {
9014
+ detail: {
9015
+ node,
9016
+ previous
9017
+ },
9018
+ bubbles: true,
9019
+ composed: true
9020
+ }));
9021
+ }
8509
9022
  #matchesFilter(node, depth) {
8510
9023
  if (!this.filterPredicate) return true;
8511
9024
  if (this.filterPredicate(node, depth)) return true;
@@ -8791,6 +9304,7 @@ __decorate([property({
8791
9304
  reflect: true
8792
9305
  })], HyperbolicTreeListElement.prototype, "searchModes", void 0);
8793
9306
  __decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
9307
+ __decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "restoreLoads", void 0);
8794
9308
  __decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
8795
9309
  __decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
8796
9310
  __decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
@@ -8825,6 +9339,30 @@ function findIciclePath(root, key) {
8825
9339
  }
8826
9340
  }
8827
9341
  /**
9342
+ * Follow `keys` from `root`; a leading key equal to the root's own key
9343
+ * matches the root itself. Returns `undefined` when a key isn't found.
9344
+ */
9345
+ function resolveIcicleKeyPath(root, keys) {
9346
+ let current = root;
9347
+ for (const key of keys) {
9348
+ const found = key === current?.item.key ? current : current?.children?.find((n) => n.item.key === key);
9349
+ if (!found) return void 0;
9350
+ current = found;
9351
+ }
9352
+ return current;
9353
+ }
9354
+ /** Key path (depth-first) to the first node among `nodes` matching `predicate`. */
9355
+ function findIcicleKeyPath(nodes, predicate, prefix = []) {
9356
+ for (const node of nodes) {
9357
+ const path = [...prefix, node.item.key];
9358
+ if (predicate(node)) return path;
9359
+ if (node.children?.length) {
9360
+ const hit = findIcicleKeyPath(node.children, predicate, path);
9361
+ if (hit) return hit;
9362
+ }
9363
+ }
9364
+ }
9365
+ /**
8828
9366
  * Compute the visible rectangles for an icicle view.
8829
9367
  *
8830
9368
  * Bands start at the zoom node's **children** (the zoom node itself is shown in
@@ -8878,6 +9416,259 @@ function computeIcicleLayout(root, options = {}) {
8878
9416
  return rects;
8879
9417
  }
8880
9418
  //#endregion
9419
+ //#region src/icicle-tree/icicle-view-state.ts
9420
+ /**
9421
+ * - `found` — every key matched: `nodes[i]` is the node for `keys[i]`.
9422
+ * - `load` — `node` (at `depth`; root's children are `0`) must load its
9423
+ * children before the path can continue.
9424
+ * - `missing` — a key isn't there (yet), or the next step can't load.
9425
+ */
9426
+ /** First node (depth-first) for each key in the loaded tree. */
9427
+ function indexNodesByKey(root) {
9428
+ const out = /* @__PURE__ */ new Map();
9429
+ const visit = (node) => {
9430
+ if (!out.has(node.item.key)) out.set(node.item.key, node);
9431
+ for (const child of node.children ?? []) visit(child);
9432
+ };
9433
+ if (root) visit(root);
9434
+ return out;
9435
+ }
9436
+ /**
9437
+ * Nodes along a pending zoom path (`[]` = full tree), or `undefined` while it
9438
+ * can't resolve. Keys are unique tree-wide, so a zoom node that has moved is
9439
+ * still found by its key.
9440
+ */
9441
+ function resolveZoomPath$1(root, path, nodeByKey) {
9442
+ const found = resolveKeyPath(root, path, () => false);
9443
+ if (found.status === `found`) return found.nodes;
9444
+ const key = path.at(-1);
9445
+ return key !== void 0 && nodeByKey(key) ? findIciclePath(root, key)?.slice(1) : void 0;
9446
+ }
9447
+ /** Pixel scroll of a scrolling host. Rects aren't a linear list, so there's no anchor. */
9448
+ function captureHostScroll(el) {
9449
+ return el.scrollLeft === 0 ? { top: el.scrollTop } : {
9450
+ top: el.scrollTop,
9451
+ left: el.scrollLeft
9452
+ };
9453
+ }
9454
+ /** Returns `true` once the saved position is fully reached (else retry after the next render). */
9455
+ function applyHostScroll(el, scroll) {
9456
+ const top = scrollToOffset(el, scroll.top);
9457
+ const left = scroll.left === void 0 || scrollToOffset(el, scroll.left, `horizontal`);
9458
+ return top && left;
9459
+ }
9460
+ //#endregion
9461
+ //#region src/icicle-tree/keyboard.ts
9462
+ /**
9463
+ * Rect the cursor moves to for `key`, or `undefined` when the key isn't a
9464
+ * navigation key or there's nowhere to go. With no current rect, any
9465
+ * navigation key lands on the first rect.
9466
+ */
9467
+ function icicleKeyTarget(rects, current, key) {
9468
+ if (rects.length === 0) return void 0;
9469
+ if (key === `Home`) return rects[0];
9470
+ if (key === `End`) return rects.at(-1);
9471
+ if (![
9472
+ `ArrowLeft`,
9473
+ `ArrowRight`,
9474
+ `ArrowUp`,
9475
+ `ArrowDown`
9476
+ ].includes(key)) return void 0;
9477
+ if (!current) return rects[0];
9478
+ switch (key) {
9479
+ case `ArrowLeft`:
9480
+ case `ArrowRight`: {
9481
+ const dir = key === `ArrowRight` ? 1 : -1;
9482
+ const siblings = rects.filter((r) => r.relativeDepth === current.relativeDepth && r.parentKey === current.parentKey);
9483
+ return siblings[siblings.findIndex((r) => r.node.item.key === current.node.item.key) + dir] ?? (dir > 0 ? siblings[0] : siblings.at(-1));
9484
+ }
9485
+ case `ArrowUp`: return (current.parentKey === void 0 ? void 0 : rects.find((r) => r.node.item.key === current.parentKey)) ?? rects[0];
9486
+ default: return rects.find((r) => r.parentKey === current.node.item.key);
9487
+ }
9488
+ }
9489
+ //#endregion
9490
+ //#region src/icicle-tree/styles.ts
9491
+ /** Shadow-DOM styles for `ixfx-icicletree-list`. */
9492
+ const icicleTreeStyles = css`
9493
+ :host {
9494
+ --icicletree-band-height: 44px;
9495
+ --icicletree-band-gap: 2px;
9496
+ --icicletree-rect-gap: 2px;
9497
+ --icicletree-rect-bg: var(--surface-4);
9498
+ --icicletree-rect-text: var(--surface-4-text);
9499
+ --icicletree-font-size: var(--text-s);
9500
+
9501
+ display: block;
9502
+ overflow: auto;
9503
+ }
9504
+
9505
+ .icicle {
9506
+ display: flex;
9507
+ gap: var(--icicletree-band-gap);
9508
+ outline: none;
9509
+ }
9510
+
9511
+ .icicle.horizontal {
9512
+ flex-direction: column;
9513
+ }
9514
+
9515
+ .icicle.vertical {
9516
+ flex-direction: row;
9517
+ min-height: 200px;
9518
+ }
9519
+
9520
+ .breadcrumb {
9521
+ display: flex;
9522
+ align-items: center;
9523
+ padding: var(--space-xs) var(--space-s);
9524
+ background: var(--surface-3);
9525
+ color: var(--surface-3-text);
9526
+ border: var(--border-subtle);
9527
+ border-radius: var(--radius-m);
9528
+ font-size: var(--text-s);
9529
+ flex-shrink: 0;
9530
+ }
9531
+
9532
+ .breadcrumb ixfx-crumb-navigation {
9533
+ flex: 1;
9534
+ min-width: 0;
9535
+ }
9536
+
9537
+ .band {
9538
+ position: relative;
9539
+ }
9540
+
9541
+ .icicle.horizontal .band {
9542
+ height: var(--icicletree-band-height);
9543
+ }
9544
+
9545
+ .icicle.vertical .band {
9546
+ flex: 1 1 0;
9547
+ min-width: 0;
9548
+ height: 100%;
9549
+ }
9550
+
9551
+ .rect {
9552
+ box-sizing: border-box;
9553
+ position: absolute;
9554
+ overflow: hidden;
9555
+ display: flex;
9556
+ align-items: center;
9557
+ padding: 0 var(--space-s);
9558
+ background: var(--icicletree-rect-bg);
9559
+ color: var(--icicletree-rect-text);
9560
+ font-size: var(--icicletree-font-size);
9561
+ cursor: pointer;
9562
+ transition:
9563
+ width var(--transition),
9564
+ height var(--transition),
9565
+ left var(--transition),
9566
+ top var(--transition),
9567
+ opacity var(--transition),
9568
+ background-color var(--transition);
9569
+ }
9570
+
9571
+ .icicle.horizontal .rect {
9572
+ top: 0;
9573
+ height: 100%;
9574
+ }
9575
+
9576
+ /*
9577
+ * A rotated label needs its full allotted width to be usable — the
9578
+ * default left/right padding (--space-s, ~4px a side) can consume
9579
+ * most or all of a rect sized down to minVerticalLabelWidth, leaving
9580
+ * the rotated text clipped to invisible. #labelMode's width estimate
9581
+ * assumes the whole rect width is usable, so drop the padding here to
9582
+ * match that assumption instead of also budgeting for it in the
9583
+ * threshold (which would make already-narrow rects need even more).
9584
+ */
9585
+ .icicle.horizontal .rect.rotated {
9586
+ padding: 0;
9587
+ }
9588
+
9589
+ .icicle.vertical .rect {
9590
+ left: 0;
9591
+ width: 100%;
9592
+ padding: var(--space-s) 0;
9593
+ justify-content: center;
9594
+ writing-mode: horizontal-tb;
9595
+ }
9596
+
9597
+ .rect.filtered {
9598
+ opacity: 0.3;
9599
+ filter: saturate(0.4);
9600
+ }
9601
+
9602
+ .rect.empty {
9603
+ padding: 0;
9604
+ border: none;
9605
+ }
9606
+
9607
+ .rect.loading {
9608
+ cursor: progress;
9609
+ opacity: 0.6;
9610
+ }
9611
+
9612
+ .icicle-label {
9613
+ overflow: hidden;
9614
+ text-overflow: ellipsis;
9615
+ white-space: nowrap;
9616
+ flex: 1;
9617
+ min-width: 0;
9618
+ }
9619
+
9620
+ .icicle-label.hide-label {
9621
+ visibility: hidden;
9622
+ }
9623
+
9624
+ .icicle-label.label-vertical {
9625
+ writing-mode: vertical-rl;
9626
+ text-orientation: sideways;
9627
+ flex: none;
9628
+ width: 100%;
9629
+ height: 100%;
9630
+ padding: var(--space-2xs, 2px) 0;
9631
+ box-sizing: border-box;
9632
+ display: flex;
9633
+ align-items: center;
9634
+ justify-content: center;
9635
+ }
9636
+
9637
+ /*
9638
+ * Instant tooltip for rects whose label is rotated or hidden — anchored
9639
+ * via CSS anchor positioning (anchor-name set on the tickled rect in
9640
+ * #renderRect) rather than a JS-computed position, and shown/hidden via
9641
+ * the Popover API rather than the browser's own (slow, un-stylable)
9642
+ * title tooltip.
9643
+ */
9644
+ .icicle-tooltip {
9645
+ position: fixed;
9646
+ position-anchor: --icicle-rect-anchor;
9647
+ position-area: bottom center;
9648
+ position-try-fallbacks: top center;
9649
+ margin: var(--space-xs, 4px) 0 0;
9650
+ padding: var(--space-2xs, 2px) var(--space-xs, 6px);
9651
+ background: var(--surface-5);
9652
+ color: var(--surface-5-text);
9653
+ border: var(--border-subtle);
9654
+ border-radius: var(--radius-s, 4px);
9655
+ font-size: var(--text-s);
9656
+ white-space: nowrap;
9657
+ pointer-events: none;
9658
+ inset: auto;
9659
+ }
9660
+
9661
+ .icicle-tooltip:popover-open {
9662
+ display: block;
9663
+ }
9664
+
9665
+ .empty-state {
9666
+ padding: var(--space-l);
9667
+ color: var(--surface-muted-text);
9668
+ font-size: var(--text-s);
9669
+ }
9670
+ `;
9671
+ //#endregion
8881
9672
  //#region src/icicle-tree/icicletree-list.ts
8882
9673
  let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
8883
9674
  constructor(..._args) {
@@ -8895,6 +9686,14 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
8895
9686
  this.disableKeyboardNavigation = false;
8896
9687
  this.registry = void 0;
8897
9688
  this._zoomKey = void 0;
9689
+ this.restoreLoads = true;
9690
+ this.#viewState = new ViewStateController(this, {
9691
+ kind: `icicletree-list`,
9692
+ capture: () => this.getViewState()
9693
+ });
9694
+ this.#pendingSelected = EMPTY_KEYS;
9695
+ this.#keyboardCursor = false;
9696
+ this.#zoomPath = [];
8898
9697
  this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
8899
9698
  labelSelector: `.icicle-label`,
8900
9699
  highlightKey: `incr-search-highlight`
@@ -8923,6 +9722,9 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
8923
9722
  });
8924
9723
  this.#pendingKeys = /* @__PURE__ */ new Set();
8925
9724
  this.#aborters = /* @__PURE__ */ new Map();
9725
+ this.#onScroll = () => {
9726
+ if (!this.#viewState.pendingScroll) this.#viewState.notify();
9727
+ };
8926
9728
  }
8927
9729
  static {
8928
9730
  this.shadowRootOptions = {
@@ -8938,6 +9740,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
8938
9740
  if (this._zoomKey === void 0) return void 0;
8939
9741
  return findIcicleNode(this.#effectiveRoot, this._zoomKey);
8940
9742
  }
9743
+ #viewState;
9744
+ /** Saved selection for keys not (yet) loaded. */
9745
+ #pendingSelected;
9746
+ /** Saved zoom path not (yet) resolvable. */
9747
+ #pendingZoom;
9748
+ #pendingFocus;
9749
+ /** Whether the cursor was last placed by the keyboard (only then is it `focus`). */
9750
+ #keyboardCursor;
9751
+ /** Last known key path of the zoom node, so it can go pending when the node leaves. */
9752
+ #zoomPath;
8941
9753
  #modelUnsubscribe;
8942
9754
  #pendingKeys;
8943
9755
  #aborters;
@@ -8974,6 +9786,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
8974
9786
  });
8975
9787
  this.registry = registry;
8976
9788
  this.#syncSearchCommands();
9789
+ this.#viewState.observeScrollTarget(this);
9790
+ this.addEventListener(`scroll`, this.#onScroll, { passive: true });
8977
9791
  if (typeof ResizeObserver !== `undefined`) {
8978
9792
  this.#resizeObserver = new ResizeObserver(() => this.requestUpdate());
8979
9793
  this.#resizeObserver.observe(this);
@@ -8986,6 +9800,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
8986
9800
  for (const aborter of this.#aborters.values()) aborter.abort();
8987
9801
  this.#aborters.clear();
8988
9802
  this.#pendingKeys.clear();
9803
+ this.removeEventListener(`scroll`, this.#onScroll);
8989
9804
  this.#resizeObserver?.disconnect();
8990
9805
  this.#resizeObserver = void 0;
8991
9806
  if (this.#ownedRegistry) {
@@ -9007,28 +9822,23 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9007
9822
  this.#unregisterSearchCommands?.();
9008
9823
  this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
9009
9824
  }
9010
- updated(changedProps) {
9011
- if (changedProps.has(`root`) && this.root) this.#resetViewState();
9012
- if (changedProps.has(`model`)) {
9013
- this.#modelUnsubscribe?.();
9014
- this.#modelUnsubscribe = void 0;
9015
- if (this.model) {
9016
- this.#modelUnsubscribe = this.model._subscribe((event) => {
9017
- if (event.type === `root`) {
9018
- this.#resetViewState();
9019
- this.requestUpdate();
9020
- } else if (event.type === `invalidate`) this.requestUpdate();
9021
- });
9022
- this.#resetViewState();
9023
- }
9024
- }
9025
- if (changedProps.has(`interactionMode`)) {
9825
+ willUpdate(changedProps) {
9826
+ if (changedProps.has(`root`) || changedProps.has(`model`)) this.#anchorIndex = void 0;
9827
+ if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
9026
9828
  this.#anchorIndex = void 0;
9027
9829
  this.clearSelection();
9028
9830
  }
9831
+ this.#reconcileViewState();
9832
+ }
9833
+ updated(changedProps) {
9834
+ if (changedProps.has(`model`)) {
9835
+ this.#modelUnsubscribe?.();
9836
+ this.#modelUnsubscribe = this.model?._subscribe(() => this.requestUpdate());
9837
+ }
9029
9838
  if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
9030
- if (this._zoomKey !== void 0 && !findIcicleNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
9031
9839
  this.#ensureVisibleChildren();
9840
+ this.#continueRestoreLoads();
9841
+ this.#viewState.applyPendingScroll((scroll) => applyHostScroll(this, scroll));
9032
9842
  this.#syncTooltip();
9033
9843
  }
9034
9844
  /** The host's own (viewport) extent — width in horizontal orientation, height in vertical. */
@@ -9078,12 +9888,103 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9078
9888
  popover.hidePopover();
9079
9889
  } catch {}
9080
9890
  }
9081
- #resetViewState() {
9082
- this.#clearSelectionInternal();
9083
- this.tickled.clearTickled();
9084
- this._zoomKey = void 0;
9085
- this.#anchorIndex = void 0;
9891
+ /**
9892
+ * Snapshot selection, zoom path, keyboard cursor and scroll as plain JSON,
9893
+ * keyed by `item.key`. Includes pending state for keys not currently loaded.
9894
+ */
9895
+ getViewState(options) {
9896
+ const withPending = options?.includePending ?? true;
9897
+ const focus = this.#keyboardCursor ? this.tickled.tickledId : void 0;
9898
+ return this.#viewState.shape({
9899
+ selected: keysToArray([...this._selectedNodes].map((n) => n.item.key), withPending ? this.#pendingSelected : []),
9900
+ zoom: (withPending ? this.#pendingZoom : void 0) ?? (this._zoomKey === void 0 ? [] : [...this.#zoomPath]),
9901
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0),
9902
+ scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? (this.isConnected ? captureHostScroll(this) : void 0)
9903
+ }, options);
9904
+ }
9905
+ /**
9906
+ * Apply saved view state. Fields present replace the current ones; keys
9907
+ * that aren't loaded yet are applied as they appear — including via
9908
+ * `loadChildren` / `model` down a saved zoom path when `restoreLoads`.
9909
+ */
9910
+ setViewState(state) {
9911
+ if (!this.#viewState.accepts(state)) return;
9912
+ this.#viewState.restoring(() => {
9913
+ if (state.selected) {
9914
+ this.#pendingSelected = capKeys(new Set(state.selected));
9915
+ if (this._selectedNodes.size > 0) {
9916
+ const prev = new Set(this._selectedNodes);
9917
+ this.#clearSelectionInternal();
9918
+ this.#dispatchSelect(prev);
9919
+ }
9920
+ }
9921
+ if (state.zoom) this.#pendingZoom = [...state.zoom];
9922
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
9923
+ if (state.scroll) this.#viewState.pendingScroll = state.scroll;
9924
+ this.#reconcileViewState();
9925
+ if (this.#pendingZoom !== void 0) this.zoomOut();
9926
+ });
9927
+ this.requestUpdate();
9928
+ }
9929
+ clearPendingViewState() {
9930
+ this.#pendingSelected = EMPTY_KEYS;
9931
+ this.#pendingZoom = void 0;
9932
+ this.#pendingFocus = void 0;
9933
+ this.#viewState.pendingScroll = void 0;
9934
+ }
9935
+ /** Move state for missing keys to pending; apply pending state for keys now loaded. */
9936
+ #reconcileViewState() {
9937
+ if (this._selectedNodes.size === 0 && this.#pendingSelected.size === 0 && this._zoomKey === void 0 && this.#pendingZoom === void 0 && this.tickled.tickledId === void 0 && this.#pendingFocus === void 0) return;
9938
+ const root = this.#effectiveRoot;
9939
+ let index;
9940
+ const nodeByKey = (key) => (index ??= indexNodesByKey(root)).get(key);
9941
+ this.#viewState.restoring(() => {
9942
+ this.#reconcileZoom(root, nodeByKey);
9943
+ const sel = reconcileSelection$1(this._selectedNodes, this.#pendingSelected, nodeByKey, (node) => this.#canSelect(node), this.interactionMode === `implicit`);
9944
+ if (sel.changed) {
9945
+ const prev = this._selectedNodes;
9946
+ const primary = this.selectedNode?.item.key;
9947
+ this._selectedNodes = new Set(sel.selected);
9948
+ this.#pendingSelected = sel.pending;
9949
+ this.selectedNode = [...sel.selected].find((n) => n.item.key === primary) ?? [...sel.selected].at(-1);
9950
+ if (sel.restored) this.#dispatchSelect(prev);
9951
+ else this.#viewState.notify(`data`);
9952
+ }
9953
+ this.#reconcileFocus(nodeByKey);
9954
+ });
9955
+ }
9956
+ #reconcileZoom(root, nodeByKey) {
9957
+ if (this._zoomKey !== void 0 && !nodeByKey(this._zoomKey)) {
9958
+ this.#pendingZoom ??= this.#zoomPath;
9959
+ this._zoomKey = void 0;
9960
+ this.#viewState.notify(`data`);
9961
+ }
9962
+ const nodes = this.#pendingZoom && resolveZoomPath$1(root, this.#pendingZoom, nodeByKey);
9963
+ if (!nodes) return;
9964
+ this.#pendingZoom = void 0;
9965
+ if (nodes.length === 0) this.zoomOut();
9966
+ else this.zoomTo(nodes.at(-1));
9967
+ }
9968
+ #reconcileFocus(nodeByKey) {
9969
+ const tickled = this.tickled.tickledId;
9970
+ if (tickled !== void 0 && !nodeByKey(tickled)) {
9971
+ if (this.#keyboardCursor) this.#pendingFocus ??= tickled;
9972
+ this.tickled.clearTickled();
9973
+ }
9974
+ const focus = this.#pendingFocus;
9975
+ if (focus !== void 0 && this.#rects().some((r) => r.node.item.key === focus)) {
9976
+ this.#pendingFocus = void 0;
9977
+ this.#keyboardCursor = true;
9978
+ this.tickled.setTickled(focus);
9979
+ }
9086
9980
  }
9981
+ /** Load the next unloaded node along a pending zoom path (one load at a time). */
9982
+ #continueRestoreLoads() {
9983
+ if (!this.restoreLoads || this.#pendingZoom === void 0 || !this.model && !this.loadChildren) return;
9984
+ const next = resolveKeyPath(this.#effectiveRoot, this.#pendingZoom, (node) => node.item.isLeaf !== true);
9985
+ if (next.status === `load` && !this.#pendingKeys.has(next.node.item.key)) this.#loadChildren(next.node, next.depth);
9986
+ }
9987
+ #onScroll;
9087
9988
  #ensureVisibleChildren() {
9088
9989
  if (!this.model && !this.loadChildren) return;
9089
9990
  for (const rect of this.#rects()) {
@@ -9127,6 +10028,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9127
10028
  this.selectedNode = void 0;
9128
10029
  }
9129
10030
  #dispatchSelect(previous) {
10031
+ if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
10032
+ this.#viewState.notify();
9130
10033
  this.dispatchEvent(new CustomEvent(`select`, {
9131
10034
  detail: {
9132
10035
  selected: new Set(this._selectedNodes),
@@ -9154,6 +10057,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9154
10057
  this.requestUpdate();
9155
10058
  }
9156
10059
  clearSelection() {
10060
+ this.#pendingSelected = EMPTY_KEYS;
9157
10061
  if (this._selectedNodes.size === 0) return;
9158
10062
  const prev = new Set(this._selectedNodes);
9159
10063
  this.#clearSelectionInternal();
@@ -9190,11 +10094,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9190
10094
  * Fires `zoom` and the semantic `expand` event.
9191
10095
  */
9192
10096
  zoomTo(node) {
10097
+ this.#zoomChanged();
9193
10098
  if (this._zoomKey === node.item.key) return;
9194
10099
  this._zoomKey = node.item.key;
9195
10100
  this.tickled.clearTickled();
9196
- const depth = findIciclePath(this.#effectiveRoot, node.item.key)?.length;
9197
- const absoluteDepth = depth === void 0 ? -1 : depth - 2;
10101
+ const path = findIciclePath(this.#effectiveRoot, node.item.key);
10102
+ this.#zoomPath = path?.slice(1).map((n) => n.item.key) ?? [node.item.key];
10103
+ const absoluteDepth = path === void 0 ? -1 : path.length - 2;
9198
10104
  this.dispatchEvent(new CustomEvent(`zoom`, {
9199
10105
  detail: {
9200
10106
  node,
@@ -9218,11 +10124,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9218
10124
  * the semantic `collapse` event for the previous zoom root.
9219
10125
  */
9220
10126
  zoomOut() {
10127
+ this.#zoomChanged();
9221
10128
  if (this._zoomKey === void 0) return;
9222
10129
  const previous = this.focusedNode;
9223
10130
  const path = this._zoomKey === void 0 ? void 0 : findIciclePath(this.#effectiveRoot, this._zoomKey);
9224
10131
  const absoluteDepth = path === void 0 ? -1 : path.length - 2;
9225
10132
  this._zoomKey = void 0;
10133
+ this.#zoomPath = [];
9226
10134
  this.tickled.clearTickled();
9227
10135
  this.dispatchEvent(new CustomEvent(`zoom`, {
9228
10136
  detail: {
@@ -9242,6 +10150,11 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9242
10150
  }));
9243
10151
  this.requestUpdate();
9244
10152
  }
10153
+ /** An explicit zoom supersedes a saved one still waiting. */
10154
+ #zoomChanged() {
10155
+ if (!this.#viewState.isRestoring) this.#pendingZoom = void 0;
10156
+ this.#viewState.notify();
10157
+ }
9245
10158
  #canZoom(node) {
9246
10159
  if (node.item.isLeaf === true) return false;
9247
10160
  if (node.children !== void 0) return node.children.length > 0;
@@ -9277,7 +10190,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9277
10190
  navigateToNode(predicate) {
9278
10191
  const root = this.#effectiveRoot;
9279
10192
  if (!root?.children) return false;
9280
- const keyPath = this.#findKeyPath(root.children, predicate);
10193
+ const keyPath = findIcicleKeyPath(root.children, predicate);
9281
10194
  if (!keyPath) return false;
9282
10195
  return this.navigateTo(keyPath);
9283
10196
  }
@@ -9288,35 +10201,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9288
10201
  revealKey(key) {
9289
10202
  const root = this.#effectiveRoot;
9290
10203
  if (!root?.children) return false;
9291
- const path = this.#findKeyPath(root.children, (n) => n.item.key === key);
10204
+ const path = findIcicleKeyPath(root.children, (n) => n.item.key === key);
9292
10205
  if (!path) return false;
9293
10206
  if (path.length > this.maxDepth) {
9294
- const ancestor = this.#resolveKeyPath(path.slice(0, path.length - this.maxDepth));
10207
+ const ancestor = resolveIcicleKeyPath(this.#effectiveRoot, path.slice(0, path.length - this.maxDepth));
9295
10208
  if (ancestor) this.zoomTo(ancestor);
9296
10209
  }
9297
10210
  this.tickled.setTickled(key);
9298
10211
  this.requestUpdate();
9299
10212
  return true;
9300
10213
  }
9301
- #resolveKeyPath(keys) {
9302
- let current = this.#effectiveRoot;
9303
- for (const key of keys) {
9304
- const found = key === current?.item.key ? current : current?.children?.find((n) => n.item.key === key);
9305
- if (!found) return void 0;
9306
- current = found;
9307
- }
9308
- return current;
9309
- }
9310
- #findKeyPath(nodes, predicate, prefix = []) {
9311
- for (const node of nodes) {
9312
- const path = [...prefix, node.item.key];
9313
- if (predicate(node)) return path;
9314
- if (node.children?.length) {
9315
- const hit = this.#findKeyPath(node.children, predicate, path);
9316
- if (hit) return hit;
9317
- }
9318
- }
9319
- }
9320
10214
  /**
9321
10215
  * All nodes currently visible as rectangles, in render (band-major) order,
9322
10216
  * with absolute depth (root's children are depth 0).
@@ -9393,44 +10287,20 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9393
10287
  const currentIndex = tickledKey === void 0 ? -1 : rects.findIndex((r) => r.node.item.key === tickledKey);
9394
10288
  const current = currentIndex >= 0 ? rects[currentIndex] : void 0;
9395
10289
  const moveTo = (rect) => {
9396
- if (!rect) return;
9397
10290
  event.preventDefault();
9398
10291
  this.tickled.setTickled(rect.node.item.key);
10292
+ this.#keyboardCursor = true;
10293
+ this.#pendingFocus = void 0;
10294
+ this.#viewState.notify();
9399
10295
  if (this.interactionMode === `implicit`) this.select(rect.node);
9400
10296
  };
10297
+ if (event.key === `ArrowLeft` || event.key === `ArrowRight`) event.preventDefault();
10298
+ const target = icicleKeyTarget(rects, current, event.key);
10299
+ if (target) {
10300
+ moveTo(target);
10301
+ return;
10302
+ }
9401
10303
  switch (event.key) {
9402
- case `ArrowLeft`:
9403
- case `ArrowRight`: {
9404
- event.preventDefault();
9405
- const dir = event.key === `ArrowRight` ? 1 : -1;
9406
- if (!current) {
9407
- moveTo(rects[0]);
9408
- return;
9409
- }
9410
- const siblings = rects.filter((r) => r.relativeDepth === current.relativeDepth && r.parentKey === current.parentKey);
9411
- moveTo(siblings[siblings.findIndex((r) => r.node.item.key === current.node.item.key) + dir] ?? (dir > 0 ? siblings[0] : siblings.at(-1)));
9412
- return;
9413
- }
9414
- case `ArrowUp`:
9415
- if (!current) {
9416
- moveTo(rects[0]);
9417
- return;
9418
- }
9419
- moveTo((current.parentKey === void 0 ? void 0 : rects.find((r) => r.node.item.key === current.parentKey)) ?? rects[0]);
9420
- return;
9421
- case `ArrowDown`:
9422
- if (!current) {
9423
- moveTo(rects[0]);
9424
- return;
9425
- }
9426
- moveTo(rects.find((r) => r.parentKey === current.node.item.key));
9427
- return;
9428
- case `Home`:
9429
- moveTo(rects[0]);
9430
- return;
9431
- case `End`:
9432
- moveTo(rects.at(-1));
9433
- return;
9434
10304
  case `Enter`:
9435
10305
  if (!current) return;
9436
10306
  event.preventDefault();
@@ -9480,6 +10350,10 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9480
10350
  if (node.item.key === this.#effectiveRoot?.item.key) this.zoomOut();
9481
10351
  else this.zoomTo(node);
9482
10352
  }
10353
+ #onRectPointerEnter(key) {
10354
+ this.tickled.handlePointerEnter(key);
10355
+ if (this.tickled.tickledId === key) this.#keyboardCursor = false;
10356
+ }
9483
10357
  #renderRect(rect) {
9484
10358
  const { node } = rect;
9485
10359
  const selected = this._selectedNodes.has(node);
@@ -9508,7 +10382,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9508
10382
  style=${sizeStyle}
9509
10383
  @click=${(e) => this.#handleItemClick(rect, this.#rects().findIndex((r) => r.node.item.key === node.item.key), e)}
9510
10384
  @dblclick=${() => this.#handleItemDblClick(rect)}
9511
- @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
10385
+ @pointerenter=${() => this.#onRectPointerEnter(node.item.key)}
9512
10386
  >
9513
10387
  <span class="icicle-label ${hideLabel ? `hide-label` : ``} ${rotated ? `label-vertical` : ``}" part="label">${node.item.label}</span>
9514
10388
  </div>
@@ -9553,185 +10427,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
9553
10427
  tickledStyles,
9554
10428
  tickledItemStyles,
9555
10429
  listSearchStyles,
9556
- css`
9557
- :host {
9558
- --icicletree-band-height: 44px;
9559
- --icicletree-band-gap: 2px;
9560
- --icicletree-rect-gap: 2px;
9561
- --icicletree-rect-bg: var(--surface-4);
9562
- --icicletree-rect-text: var(--surface-4-text);
9563
- --icicletree-font-size: var(--text-s);
9564
-
9565
- display: block;
9566
- overflow: auto;
9567
- }
9568
-
9569
- .icicle {
9570
- display: flex;
9571
- gap: var(--icicletree-band-gap);
9572
- outline: none;
9573
- }
9574
-
9575
- .icicle.horizontal {
9576
- flex-direction: column;
9577
- }
9578
-
9579
- .icicle.vertical {
9580
- flex-direction: row;
9581
- min-height: 200px;
9582
- }
9583
-
9584
- .breadcrumb {
9585
- display: flex;
9586
- align-items: center;
9587
- padding: var(--space-xs) var(--space-s);
9588
- background: var(--surface-3);
9589
- color: var(--surface-3-text);
9590
- border: var(--border-subtle);
9591
- border-radius: var(--radius-m);
9592
- font-size: var(--text-s);
9593
- flex-shrink: 0;
9594
- }
9595
-
9596
- .breadcrumb ixfx-crumb-navigation {
9597
- flex: 1;
9598
- min-width: 0;
9599
- }
9600
-
9601
- .band {
9602
- position: relative;
9603
- }
9604
-
9605
- .icicle.horizontal .band {
9606
- height: var(--icicletree-band-height);
9607
- }
9608
-
9609
- .icicle.vertical .band {
9610
- flex: 1 1 0;
9611
- min-width: 0;
9612
- height: 100%;
9613
- }
9614
-
9615
- .rect {
9616
- box-sizing: border-box;
9617
- position: absolute;
9618
- overflow: hidden;
9619
- display: flex;
9620
- align-items: center;
9621
- padding: 0 var(--space-s);
9622
- background: var(--icicletree-rect-bg);
9623
- color: var(--icicletree-rect-text);
9624
- font-size: var(--icicletree-font-size);
9625
- cursor: pointer;
9626
- transition:
9627
- width var(--transition),
9628
- height var(--transition),
9629
- left var(--transition),
9630
- top var(--transition),
9631
- opacity var(--transition),
9632
- background-color var(--transition);
9633
- }
9634
-
9635
- .icicle.horizontal .rect {
9636
- top: 0;
9637
- height: 100%;
9638
- }
9639
-
9640
- /*
9641
- * A rotated label needs its full allotted width to be usable — the
9642
- * default left/right padding (--space-s, ~4px a side) can consume
9643
- * most or all of a rect sized down to minVerticalLabelWidth, leaving
9644
- * the rotated text clipped to invisible. #labelMode's width estimate
9645
- * assumes the whole rect width is usable, so drop the padding here to
9646
- * match that assumption instead of also budgeting for it in the
9647
- * threshold (which would make already-narrow rects need even more).
9648
- */
9649
- .icicle.horizontal .rect.rotated {
9650
- padding: 0;
9651
- }
9652
-
9653
- .icicle.vertical .rect {
9654
- left: 0;
9655
- width: 100%;
9656
- padding: var(--space-s) 0;
9657
- justify-content: center;
9658
- writing-mode: horizontal-tb;
9659
- }
9660
-
9661
- .rect.filtered {
9662
- opacity: 0.3;
9663
- filter: saturate(0.4);
9664
- }
9665
-
9666
- .rect.empty {
9667
- padding: 0;
9668
- border: none;
9669
- }
9670
-
9671
- .rect.loading {
9672
- cursor: progress;
9673
- opacity: 0.6;
9674
- }
9675
-
9676
- .icicle-label {
9677
- overflow: hidden;
9678
- text-overflow: ellipsis;
9679
- white-space: nowrap;
9680
- flex: 1;
9681
- min-width: 0;
9682
- }
9683
-
9684
- .icicle-label.hide-label {
9685
- visibility: hidden;
9686
- }
9687
-
9688
- .icicle-label.label-vertical {
9689
- writing-mode: vertical-rl;
9690
- text-orientation: sideways;
9691
- flex: none;
9692
- width: 100%;
9693
- height: 100%;
9694
- padding: var(--space-2xs, 2px) 0;
9695
- box-sizing: border-box;
9696
- display: flex;
9697
- align-items: center;
9698
- justify-content: center;
9699
- }
9700
-
9701
- /*
9702
- * Instant tooltip for rects whose label is rotated or hidden — anchored
9703
- * via CSS anchor positioning (anchor-name set on the tickled rect in
9704
- * #renderRect) rather than a JS-computed position, and shown/hidden via
9705
- * the Popover API rather than the browser's own (slow, un-stylable)
9706
- * title tooltip.
9707
- */
9708
- .icicle-tooltip {
9709
- position: fixed;
9710
- position-anchor: --icicle-rect-anchor;
9711
- position-area: bottom center;
9712
- position-try-fallbacks: top center;
9713
- margin: var(--space-xs, 4px) 0 0;
9714
- padding: var(--space-2xs, 2px) var(--space-xs, 6px);
9715
- background: var(--surface-5);
9716
- color: var(--surface-5-text);
9717
- border: var(--border-subtle);
9718
- border-radius: var(--radius-s, 4px);
9719
- font-size: var(--text-s);
9720
- white-space: nowrap;
9721
- pointer-events: none;
9722
- inset: auto;
9723
- }
9724
-
9725
- .icicle-tooltip:popover-open {
9726
- display: block;
9727
- }
9728
-
9729
- .empty-state {
9730
- padding: var(--space-l);
9731
- color: var(--surface-muted-text);
9732
- font-size: var(--text-s);
9733
- }
9734
- `
10430
+ icicleTreeStyles
9735
10431
  ];
9736
10432
  }
9737
10433
  };
@@ -9786,6 +10482,7 @@ __decorate([property({
9786
10482
  reflect: true
9787
10483
  })], IcicleTreeListElement.prototype, "searchModes", void 0);
9788
10484
  __decorate([state()], IcicleTreeListElement.prototype, "_zoomKey", void 0);
10485
+ __decorate([property({ attribute: false })], IcicleTreeListElement.prototype, "restoreLoads", void 0);
9789
10486
  __decorate([query(`#icicle-tooltip`)], IcicleTreeListElement.prototype, "_tooltipEl", void 0);
9790
10487
  IcicleTreeListElement = __decorate([safeCustomElement(`ixfx-icicletree-list`)], IcicleTreeListElement);
9791
10488
  //#endregion
@@ -9811,6 +10508,136 @@ function findLeafCategory(categories, key) {
9811
10508
  return flattenCategories(categories).find((c) => c.key === key);
9812
10509
  }
9813
10510
  //#endregion
10511
+ //#region src/multicol-list/multicol-view-state.ts
10512
+ /**
10513
+ * Pure helpers for `ixfx-multicol-list` view state. The element runs these
10514
+ * when a state is restored, when `categories` change (so saved columns whose
10515
+ * category wasn't known yet can appear) and when a column's items arrive (so
10516
+ * saved selections apply once their keys exist).
10517
+ */
10518
+ const MULTICOL_VIEW_STATE_KIND = `multicol-list`;
10519
+ const isSort = (v) => v === `asc` || v === `desc`;
10520
+ /**
10521
+ * Validate saved columns (they come from storage, so may be malformed).
10522
+ * Entries without a string `id` / `categoryKey` are dropped, as are repeat
10523
+ * ids; bad optional fields fall back to defaults.
10524
+ */
10525
+ function sanitiseColumns(raw) {
10526
+ if (!Array.isArray(raw)) return [];
10527
+ const seen = /* @__PURE__ */ new Set();
10528
+ const out = [];
10529
+ for (const entry of raw) {
10530
+ if (entry === null || typeof entry !== `object`) continue;
10531
+ const e = entry;
10532
+ if (typeof e.id !== `string` || typeof e.categoryKey !== `string` || seen.has(e.id)) continue;
10533
+ seen.add(e.id);
10534
+ out.push({
10535
+ id: e.id,
10536
+ categoryKey: e.categoryKey,
10537
+ sort: isSort(e.sort) ? e.sort : `asc`,
10538
+ grouped: e.grouped === true,
10539
+ ...typeof e.menu === `boolean` ? { menu: e.menu } : {},
10540
+ ...typeof e.width === `number` && Number.isFinite(e.width) ? { width: e.width } : {}
10541
+ });
10542
+ }
10543
+ return out;
10544
+ }
10545
+ /** Split saved columns into those whose category resolves now and those to keep pending. */
10546
+ function partitionColumns(columns, hasCategory) {
10547
+ const ready = [];
10548
+ const pending = [];
10549
+ columns.forEach((state, index) => {
10550
+ if (hasCategory(state.categoryKey)) ready.push(state);
10551
+ else pending.push({
10552
+ state,
10553
+ index
10554
+ });
10555
+ });
10556
+ return {
10557
+ ready,
10558
+ pending
10559
+ };
10560
+ }
10561
+ /** Split pending columns into those whose category now resolves and those still waiting. */
10562
+ function resolvePendingColumns(pending, hasCategory) {
10563
+ if (pending.length === 0) return {
10564
+ ready: [],
10565
+ pending
10566
+ };
10567
+ const ready = pending.filter((p) => hasCategory(p.state.categoryKey));
10568
+ return ready.length === 0 ? {
10569
+ ready,
10570
+ pending
10571
+ } : {
10572
+ ready,
10573
+ pending: pending.filter((p) => !hasCategory(p.state.categoryKey))
10574
+ };
10575
+ }
10576
+ /** Insert pending columns back at their saved positions (clamped), for a snapshot. */
10577
+ function mergeColumns(live, pending) {
10578
+ const out = [...live];
10579
+ for (const p of [...pending].sort((a, b) => a.index - b.index)) out.splice(Math.min(p.index, out.length), 0, p.state);
10580
+ return out;
10581
+ }
10582
+ /**
10583
+ * - selected keys no longer in the data → pending
10584
+ * - pending keys now in the data → selected (appended, in pending order)
10585
+ */
10586
+ function reconcileSelection(selected, pending, has) {
10587
+ const kept = selected.filter(has);
10588
+ const dropped = kept.length === selected.length ? [] : selected.filter((k) => !has(k));
10589
+ const { present, missing } = partitionKeys(pending, has);
10590
+ if (dropped.length === 0 && present.length === 0) return {
10591
+ selected,
10592
+ pending,
10593
+ restored: false,
10594
+ changed: false
10595
+ };
10596
+ const nextPending = dropped.length === 0 ? missing : capKeys(/* @__PURE__ */ new Set([...missing, ...dropped]));
10597
+ const added = present.filter((k) => !kept.includes(k));
10598
+ return {
10599
+ selected: [...kept, ...added],
10600
+ pending: nextPending,
10601
+ restored: added.length > 0,
10602
+ changed: dropped.length > 0 || added.length > 0
10603
+ };
10604
+ }
10605
+ /** JSON array of string keys → capped pending set; non-strings are ignored. */
10606
+ function keysFromJson(raw) {
10607
+ if (!Array.isArray(raw)) return EMPTY_KEYS;
10608
+ return capKeys(new Set(raw.filter((k) => typeof k === `string`)));
10609
+ }
10610
+ /** Whether `raw` looks like a {@link ScrollState}. */
10611
+ function isScrollState(raw) {
10612
+ return raw !== null && typeof raw === `object` && typeof raw.top === `number`;
10613
+ }
10614
+ /** `record` entries as a map, keeping only values `accept` allows. */
10615
+ function mapFromRecord(record, accept) {
10616
+ const out = /* @__PURE__ */ new Map();
10617
+ if (record === null || typeof record !== `object`) return out;
10618
+ for (const [key, value] of Object.entries(record)) {
10619
+ const v = accept(value);
10620
+ if (v !== void 0) out.set(key, v);
10621
+ }
10622
+ return out;
10623
+ }
10624
+ const keyOf = (el) => el.dataset.key;
10625
+ /** Rendered, keyed item rows inside a column's scroll host, in visual order. */
10626
+ function itemRows(host) {
10627
+ return [...host.querySelectorAll(`li[data-key]`)];
10628
+ }
10629
+ function captureColumnScroll(host) {
10630
+ return captureElementScroll(host, itemRows(host), keyOf);
10631
+ }
10632
+ /** Apply `scroll` to a column's host; `true` once the position was fully reached. */
10633
+ function restoreColumnScroll(host, scroll) {
10634
+ const anchor = scroll.anchor;
10635
+ if (!anchor) return scrollToOffset(host, scroll.top);
10636
+ const row = itemRows(host).find((el) => keyOf(el) === anchor.key);
10637
+ if (!row) return false;
10638
+ return restoreElementScroll(host, row, anchor.offset);
10639
+ }
10640
+ //#endregion
9814
10641
  //#region src/multicol-list/types.ts
9815
10642
  const ALL_KEY = `__all__`;
9816
10643
  //#endregion
@@ -9862,6 +10689,7 @@ var MulticolColumnController = class {
9862
10689
  #dataProvider;
9863
10690
  #onSelectionChange;
9864
10691
  #onRequestData;
10692
+ #onScroll;
9865
10693
  #abortController = void 0;
9866
10694
  #host;
9867
10695
  #allRow;
@@ -9869,7 +10697,19 @@ var MulticolColumnController = class {
9869
10697
  #selectedValues = [];
9870
10698
  #allSelected = true;
9871
10699
  #lastItems = [];
10700
+ /** Keys of `#lastItems`; empty until data for the current category arrived. */
10701
+ #itemKeys = EMPTY_KEYS;
10702
+ /** Saved selection keys not in the data (yet). */
10703
+ #pendingSelection = EMPTY_KEYS;
10704
+ /** Saved scroll waiting for its anchor row / enough content. */
10705
+ #pendingScroll;
10706
+ #disposeScrollIntent;
9872
10707
  #syncingSelection = false;
10708
+ /**
10709
+ * Pending `#restoreAfterSync` passes. The lister re-renders new items and
10710
+ * may fire `select` before the sync runs; that isn't the user's doing.
10711
+ */
10712
+ #syncsInFlight = 0;
9873
10713
  #state;
9874
10714
  constructor(options) {
9875
10715
  this.id = options.id;
@@ -9879,6 +10719,7 @@ var MulticolColumnController = class {
9879
10719
  this.#dataProvider = options.dataProvider;
9880
10720
  this.#onSelectionChange = options.onSelectionChange;
9881
10721
  this.#onRequestData = options.onRequestData;
10722
+ this.#onScroll = options.onScroll;
9882
10723
  this.#state = {
9883
10724
  id: options.id,
9884
10725
  categoryKey: options.category.key,
@@ -9954,7 +10795,10 @@ var MulticolColumnController = class {
9954
10795
  grouped: category.defaults?.grouped ?? false,
9955
10796
  menu: category.defaults?.menu ?? true
9956
10797
  };
9957
- this.clearSelection();
10798
+ this.#pendingSelection = EMPTY_KEYS;
10799
+ this.#pendingScroll = void 0;
10800
+ this.#itemKeys = EMPTY_KEYS;
10801
+ this.#setAllSelection(`data`);
9958
10802
  this.#syncGrouping();
9959
10803
  }
9960
10804
  get listerHost() {
@@ -9979,7 +10823,7 @@ var MulticolColumnController = class {
9979
10823
  allRow.appendChild(allLabel);
9980
10824
  const applyAll = () => {
9981
10825
  if (this.#allSelected) return;
9982
- this.#setAllSelection();
10826
+ this.#setAllSelection(`user`);
9983
10827
  };
9984
10828
  allRow.addEventListener(`click`, applyAll);
9985
10829
  allRow.addEventListener(`keydown`, (e) => {
@@ -10000,6 +10844,10 @@ var MulticolColumnController = class {
10000
10844
  this.#syncGrouping();
10001
10845
  this.#syncAllRow();
10002
10846
  host.append(allRow, lister);
10847
+ host.addEventListener(`scroll`, this.#handleScroll, { passive: true });
10848
+ this.#disposeScrollIntent = onUserScrollIntent(host, () => {
10849
+ this.#pendingScroll = void 0;
10850
+ });
10003
10851
  if (this.#lastItems.length > 0) {
10004
10852
  this.#pushItems();
10005
10853
  this.#restoreAfterSync();
@@ -10007,6 +10855,9 @@ var MulticolColumnController = class {
10007
10855
  }
10008
10856
  unmount() {
10009
10857
  this.#abortPending();
10858
+ this.#host?.removeEventListener(`scroll`, this.#handleScroll);
10859
+ this.#disposeScrollIntent?.();
10860
+ this.#disposeScrollIntent = void 0;
10010
10861
  this.#allRow?.remove();
10011
10862
  this.#listerEl?.remove();
10012
10863
  this.#allRow = void 0;
@@ -10029,17 +10880,17 @@ var MulticolColumnController = class {
10029
10880
  }
10030
10881
  #applyItems(items) {
10031
10882
  this.#lastItems = items;
10032
- const valid = this.#selectedValues.filter((v) => items.some((i) => i.key === v));
10033
- const changed = valid.length !== this.#selectedValues.length;
10034
- this.#selectedValues = valid;
10035
- if (valid.length === 0) this.#allSelected = true;
10036
- if (!this.#listerEl) {
10037
- if (changed) this.#onSelectionChange(this.id, valid);
10038
- return;
10883
+ const keys = new Set(items.map((i) => i.key));
10884
+ this.#itemKeys = keys;
10885
+ const r = reconcileSelection(this.#selectedValues, this.#pendingSelection, (k) => keys.has(k));
10886
+ this.#pendingSelection = r.pending;
10887
+ this.#selectedValues = r.selected;
10888
+ this.#allSelected = r.selected.length === 0;
10889
+ if (this.#listerEl) {
10890
+ this.#pushItems();
10891
+ this.#restoreAfterSync();
10039
10892
  }
10040
- this.#pushItems();
10041
- this.#restoreAfterSync();
10042
- if (changed) this.#onSelectionChange(this.id, valid);
10893
+ if (r.changed) this.#onSelectionChange(this.id, r.selected, r.restored ? `restore` : `data`);
10043
10894
  }
10044
10895
  #pushItems() {
10045
10896
  if (!this.#listerEl) return;
@@ -10064,7 +10915,7 @@ var MulticolColumnController = class {
10064
10915
  return this.#listerEl.querySelectorAll(`:scope > .ungrouped-host > *, :scope > ixfx-panel > *`);
10065
10916
  }
10066
10917
  #handleSelect = () => {
10067
- if (this.#syncingSelection) return;
10918
+ if (this.#syncingSelection || this.#syncsInFlight > 0) return;
10068
10919
  const values = [...this.#listerEl?.selectedItems ?? []].map((i) => i.key).filter((k) => k !== ALL_KEY);
10069
10920
  if (values.length === this.#selectedValues.length && values.every((v) => this.#selectedValues.includes(v))) {
10070
10921
  this.#syncAllRow();
@@ -10073,9 +10924,9 @@ var MulticolColumnController = class {
10073
10924
  this.#selectedValues = values;
10074
10925
  this.#allSelected = values.length === 0;
10075
10926
  this.#syncAllRow();
10076
- this.#onSelectionChange(this.id, values);
10927
+ this.#onSelectionChange(this.id, values, `user`);
10077
10928
  };
10078
- #setAllSelection() {
10929
+ #setAllSelection(origin) {
10079
10930
  const previous = this.#selectedValues;
10080
10931
  this.#syncingSelection = true;
10081
10932
  try {
@@ -10086,17 +10937,64 @@ var MulticolColumnController = class {
10086
10937
  this.#syncingSelection = false;
10087
10938
  }
10088
10939
  this.#syncAllRow();
10089
- if (previous.length > 0) this.#onSelectionChange(this.id, []);
10940
+ if (previous.length > 0) this.#onSelectionChange(this.id, [], origin);
10090
10941
  }
10942
+ /** Back to 'All' — an explicit selection change. */
10091
10943
  clearSelection() {
10092
- this.#setAllSelection();
10944
+ this.#setAllSelection(`user`);
10093
10945
  }
10094
10946
  get selectedValues() {
10095
10947
  return this.#selectedValues;
10096
10948
  }
10949
+ get pendingSelection() {
10950
+ return this.#pendingSelection;
10951
+ }
10952
+ /**
10953
+ * Replace the selection with `keys`. Keys in the current data are selected
10954
+ * now; the rest stay pending until items carrying them arrive.
10955
+ */
10956
+ restoreSelection(keys) {
10957
+ const previous = this.#selectedValues;
10958
+ const r = reconcileSelection([], capKeys(keys), (k) => this.#itemKeys.has(k));
10959
+ this.#pendingSelection = r.pending;
10960
+ this.#selectedValues = r.selected;
10961
+ this.#allSelected = r.selected.length === 0;
10962
+ this.#restoreAfterSync();
10963
+ this.#syncAllRow();
10964
+ if (!(previous.length === r.selected.length && previous.every((k) => r.selected.includes(k)))) this.#onSelectionChange(this.id, r.selected, `restore`);
10965
+ }
10966
+ clearPendingSelection() {
10967
+ this.#pendingSelection = EMPTY_KEYS;
10968
+ }
10969
+ get pendingScroll() {
10970
+ return this.#pendingScroll;
10971
+ }
10972
+ /** Current scroll of the column's list; `undefined` while unmounted. */
10973
+ captureScroll() {
10974
+ return this.#host ? captureColumnScroll(this.#host) : void 0;
10975
+ }
10976
+ /** Scroll to `scroll` now if possible, else when enough items have rendered. */
10977
+ restoreScroll(scroll) {
10978
+ this.#pendingScroll = scroll;
10979
+ this.#tryPendingScroll();
10980
+ }
10981
+ clearPendingScroll() {
10982
+ this.#pendingScroll = void 0;
10983
+ }
10984
+ #tryPendingScroll() {
10985
+ const scroll = this.#pendingScroll;
10986
+ if (!scroll || !this.#host) return;
10987
+ if (restoreColumnScroll(this.#host, scroll)) this.#pendingScroll = void 0;
10988
+ }
10989
+ #handleScroll = () => {
10990
+ if (!this.#pendingScroll) this.#onScroll?.(this.id);
10991
+ };
10097
10992
  #restoreAfterSync() {
10098
10993
  if (!this.#listerEl) return;
10099
- this.#listerEl.updateComplete.then(() => {
10994
+ this.#syncsInFlight++;
10995
+ this.#listerEl.updateComplete.finally(() => {
10996
+ this.#syncsInFlight--;
10997
+ }).then(() => {
10100
10998
  if (!this.#listerEl || !this.#host) return;
10101
10999
  this.#syncingSelection = true;
10102
11000
  try {
@@ -10109,6 +11007,7 @@ var MulticolColumnController = class {
10109
11007
  this.#syncingSelection = false;
10110
11008
  }
10111
11009
  this.#syncAllRow();
11010
+ this.#tryPendingScroll();
10112
11011
  });
10113
11012
  }
10114
11013
  #syncAllRow() {
@@ -10140,12 +11039,201 @@ function renderItem(item) {
10140
11039
  }
10141
11040
  return li;
10142
11041
  }
11042
+ /** Shadow styles for `ixfx-multicol-list` (theme fallbacks / handle styles are added by the element). */
11043
+ const multicolListStyles = css`
11044
+ :host {
11045
+ --multicol-list-bg: var(--surface-3);
11046
+ --multicol-list-header-bg: var(--surface-2);
11047
+ --multicol-list-header-text: var(--surface-3-text);
11048
+ --multicol-list-border: var(--border);
11049
+ --multicol-list-min-column-width: ${80}px;
11050
+ --multicol-list-max-column-width: ${480}px;
11051
+ --multicol-list-header-font-size: unset;
11052
+
11053
+ display: block;
11054
+ overflow: hidden;
11055
+ /* A sideways trackpad swipe over the component must never pan it
11056
+ (or the page behind it, or trigger history navigation) */
11057
+ overscroll-behavior-x: none;
11058
+ background: var(--multicol-list-bg);
11059
+ color: var(--surface-3-text);
11060
+ font-family: var(--font-family);
11061
+ }
11062
+ #container {
11063
+ display: flex;
11064
+ flex-direction: row;
11065
+ align-items: stretch;
11066
+ overflow: hidden;
11067
+ overscroll-behavior-x: none;
11068
+ position: relative;
11069
+ height: 100%;
11070
+ }
11071
+ .column {
11072
+ flex-shrink: 0;
11073
+ display: flex;
11074
+ flex-direction: column;
11075
+ border-right: var(--multicol-list-border);
11076
+ min-width: var(--multicol-list-min-column-width);
11077
+ max-width: var(--multicol-list-max-column-width);
11078
+ background: var(--surface-3);
11079
+ }
11080
+ .column-header {
11081
+ display: flex;
11082
+ align-items: center;
11083
+ gap: var(--space-xs);
11084
+ height: 32px;
11085
+ background: var(--multicol-list-header-bg);
11086
+ color: var(--multicol-list-header-text);
11087
+ border-bottom: var(--border);
11088
+ position: relative;
11089
+ user-select: none;
11090
+ min-width: var(--multicol-list-min-column-width);
11091
+ box-sizing: border-box;
11092
+ }
11093
+ .column-header[data-dragging] {
11094
+ opacity: 0.4;
11095
+ }
11096
+ .reorder-handle {
11097
+ cursor: grab;
11098
+ opacity: 0;
11099
+ display: inline-flex;
11100
+ align-items: center;
11101
+ flex-shrink: 0;
11102
+ padding-left: var(--space-xs);
11103
+ touch-action: none;
11104
+ transition: opacity var(--transition, 0.15s ease-out);
11105
+ }
11106
+ .column-header:hover .reorder-handle, .column-header:focus-within .reorder-handle, .column-header[data-dragging] .reorder-handle { opacity: var(--ixfx-handle-opacity, 0.5); }
11107
+ .reorder-handle:active {
11108
+ cursor: grabbing;
11109
+ }
11110
+ .reorder-handle .ixfx-handle {
11111
+ --ixfx-handle-length: 14px;
11112
+ --ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
11113
+ --ixfx-handle-dot-size: 3px;
11114
+ }
11115
+ .column-label {
11116
+ flex: 1;
11117
+ overflow: hidden;
11118
+ text-overflow: ellipsis;
11119
+ white-space: nowrap;
11120
+ padding: 0 var(--space-s);
11121
+ font-size: var(--multicol-list-header-font-size);
11122
+ font-weight: 600;
11123
+ background: transparent;
11124
+ border: none;
11125
+ color: inherit;
11126
+ cursor: pointer;
11127
+ text-align: left;
11128
+ min-width: 0;
11129
+ }
11130
+ .column-label:hover {
11131
+ text-decoration: underline;
11132
+ }
11133
+ .burger-btn {
11134
+ flex-shrink: 0;
11135
+ width: 28px;
11136
+ height: 28px;
11137
+ display: flex;
11138
+ align-items: center;
11139
+ justify-content: center;
11140
+ background: transparent;
11141
+ border: none;
11142
+ cursor: pointer;
11143
+ color: var(--surface-3-text);
11144
+ opacity: 0;
11145
+ transition: opacity var(--transition);
11146
+ border-radius: var(--radius-s);
11147
+ }
11148
+ .column:hover .burger-btn,
11149
+ .column-header:focus-within .burger-btn {
11150
+ opacity: 1;
11151
+ }
11152
+ .burger-btn:hover {
11153
+ background: var(--surface-4);
11154
+ }
11155
+ .resize-handle {
11156
+ position: absolute;
11157
+ right: -4px;
11158
+ top: 0;
11159
+ bottom: 0;
11160
+ width: 8px;
11161
+ cursor: col-resize;
11162
+ z-index: 1;
11163
+ display: flex;
11164
+ align-items: stretch;
11165
+ justify-content: center;
11166
+ touch-action: none;
11167
+ }
11168
+ .resize-handle::before {
11169
+ content: '';
11170
+ width: 2px;
11171
+ margin: 2px 0;
11172
+ border-radius: 1px;
11173
+ background: transparent;
11174
+ transition: background-color var(--transition);
11175
+ }
11176
+ .resize-handle:hover::before,
11177
+ .resize-handle[data-active]::before {
11178
+ background: var(--accent);
11179
+ }
11180
+ .lister-host {
11181
+ flex: 1;
11182
+ overflow-y: auto;
11183
+ overflow-x: hidden;
11184
+ /* Vertical scrolling only: horizontal deltas are swallowed here
11185
+ instead of chaining up and panning the whole component */
11186
+ overscroll-behavior-x: none;
11187
+ touch-action: pan-y;
11188
+ position: relative;
11189
+ display: flex;
11190
+ flex-direction: column;
11191
+ --list-gutter: 0;
11192
+ }
11193
+ .all-row {
11194
+ display: flex;
11195
+ align-items: center;
11196
+ padding: var(--space-xs) var(--space-s);
11197
+ cursor: pointer;
11198
+ user-select: none;
11199
+ min-height: 28px;
11200
+ box-sizing: border-box;
11201
+ outline: 1px solid transparent;
11202
+ background: var(--surface-3);
11203
+ }
11204
+ .all-row:hover,
11205
+ .all-row[data-selected] {
11206
+ background: var(--surface-5);
11207
+ }
11208
+ .all-row[data-selected] {
11209
+ outline-color: var(--accent);
11210
+ font-weight: 600;
11211
+ }
11212
+ ixfx-grouped-item-lister {
11213
+ display: block;
11214
+ }
11215
+ .drop-caret {
11216
+ position: absolute;
11217
+ top: 0;
11218
+ bottom: 0;
11219
+ width: 2px;
11220
+ background: var(--accent);
11221
+ border-radius: 1px;
11222
+ pointer-events: none;
11223
+ z-index: 10;
11224
+ }
11225
+ .item-label {
11226
+ flex: 1;
11227
+ }
11228
+ .count {
11229
+ opacity: 0.6;
11230
+ margin-left: var(--space-s);
11231
+ font-size: var(--text-s);
11232
+ }
11233
+ `;
10143
11234
  //#endregion
10144
11235
  //#region src/multicol-list/multicol-list.ts
10145
11236
  registerIcon(`menu`, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`);
10146
- const DEFAULT_COLUMN_WIDTH = 220;
10147
- const MIN_COLUMN_WIDTH = 80;
10148
- const MAX_COLUMN_WIDTH = 480;
10149
11237
  const DRAG_THRESHOLD = 5;
10150
11238
  let MulticolListElement = class MulticolListElement extends LitElement {
10151
11239
  constructor(..._args) {
@@ -10170,240 +11258,33 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10170
11258
  ghost: null,
10171
11259
  insert: -1
10172
11260
  };
11261
+ this.#viewState = new ViewStateController(this, {
11262
+ kind: MULTICOL_VIEW_STATE_KIND,
11263
+ capture: () => this.getViewState()
11264
+ });
11265
+ this.#pendingColumns = [];
11266
+ this.#pendingSelected = /* @__PURE__ */ new Map();
11267
+ this.#pendingScroll = /* @__PURE__ */ new Map();
10173
11268
  }
10174
11269
  static {
10175
11270
  this.styles = [
10176
11271
  themeFallbacks,
10177
11272
  handleStyles,
10178
- css`
10179
- :host {
10180
- --multicol-list-bg: var(--surface-3);
10181
- --multicol-list-header-bg: var(--surface-2);
10182
- --multicol-list-header-text: var(--surface-3-text);
10183
- --multicol-list-border: var(--border);
10184
- --multicol-list-min-column-width: ${MIN_COLUMN_WIDTH}px;
10185
- --multicol-list-max-column-width: ${MAX_COLUMN_WIDTH}px;
10186
- --multicol-list-header-font-size: unset;
10187
-
10188
- display: block;
10189
- overflow: hidden;
10190
- /* A sideways trackpad swipe over the component must never pan it
10191
- (or the page behind it, or trigger history navigation) */
10192
- overscroll-behavior-x: none;
10193
- background: var(--multicol-list-bg);
10194
- color: var(--surface-3-text);
10195
- font-family: var(--font-family);
10196
- }
10197
- #container {
10198
- display: flex;
10199
- flex-direction: row;
10200
- align-items: stretch;
10201
- overflow: hidden;
10202
- overscroll-behavior-x: none;
10203
- position: relative;
10204
- height: 100%;
10205
- }
10206
- .column {
10207
- flex-shrink: 0;
10208
- display: flex;
10209
- flex-direction: column;
10210
- border-right: var(--multicol-list-border);
10211
- min-width: var(--multicol-list-min-column-width);
10212
- max-width: var(--multicol-list-max-column-width);
10213
- background: var(--surface-3);
10214
- }
10215
- .column-header {
10216
- display: flex;
10217
- align-items: center;
10218
- gap: var(--space-xs);
10219
- height: 32px;
10220
- background: var(--multicol-list-header-bg);
10221
- color: var(--multicol-list-header-text);
10222
- border-bottom: var(--border);
10223
- position: relative;
10224
- user-select: none;
10225
- min-width: var(--multicol-list-min-column-width);
10226
- box-sizing: border-box;
10227
- }
10228
- .column-header[data-dragging] {
10229
- opacity: 0.4;
10230
- }
10231
- .reorder-handle {
10232
- cursor: grab;
10233
- opacity: 0;
10234
- display: inline-flex;
10235
- align-items: center;
10236
- flex-shrink: 0;
10237
- padding-left: var(--space-xs);
10238
- touch-action: none;
10239
- transition: opacity var(--transition, 0.15s ease-out);
10240
- }
10241
- .column-header:hover .reorder-handle, .column-header:focus-within .reorder-handle, .column-header[data-dragging] .reorder-handle { opacity: var(--ixfx-handle-opacity, 0.5); }
10242
- .reorder-handle:active {
10243
- cursor: grabbing;
10244
- }
10245
- .reorder-handle .ixfx-handle {
10246
- --ixfx-handle-length: 14px;
10247
- --ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
10248
- --ixfx-handle-dot-size: 3px;
10249
- }
10250
- .column-label {
10251
- flex: 1;
10252
- overflow: hidden;
10253
- text-overflow: ellipsis;
10254
- white-space: nowrap;
10255
- padding: 0 var(--space-s);
10256
- font-size: var(--multicol-list-header-font-size);
10257
- font-weight: 600;
10258
- background: transparent;
10259
- border: none;
10260
- color: inherit;
10261
- cursor: pointer;
10262
- text-align: left;
10263
- min-width: 0;
10264
- }
10265
- .column-label:hover {
10266
- text-decoration: underline;
10267
- }
10268
- .burger-btn {
10269
- flex-shrink: 0;
10270
- width: 28px;
10271
- height: 28px;
10272
- display: flex;
10273
- align-items: center;
10274
- justify-content: center;
10275
- background: transparent;
10276
- border: none;
10277
- cursor: pointer;
10278
- color: var(--surface-3-text);
10279
- opacity: 0;
10280
- transition: opacity var(--transition);
10281
- border-radius: var(--radius-s);
10282
- }
10283
- .column:hover .burger-btn,
10284
- .column-header:focus-within .burger-btn {
10285
- opacity: 1;
10286
- }
10287
- .burger-btn:hover {
10288
- background: var(--surface-4);
10289
- }
10290
- .resize-handle {
10291
- position: absolute;
10292
- right: -4px;
10293
- top: 0;
10294
- bottom: 0;
10295
- width: 8px;
10296
- cursor: col-resize;
10297
- z-index: 1;
10298
- display: flex;
10299
- align-items: stretch;
10300
- justify-content: center;
10301
- touch-action: none;
10302
- }
10303
- .resize-handle::before {
10304
- content: '';
10305
- width: 2px;
10306
- margin: 2px 0;
10307
- border-radius: 1px;
10308
- background: transparent;
10309
- transition: background-color var(--transition);
10310
- }
10311
- .resize-handle:hover::before,
10312
- .resize-handle[data-active]::before {
10313
- background: var(--accent);
10314
- }
10315
- .lister-host {
10316
- flex: 1;
10317
- overflow-y: auto;
10318
- overflow-x: hidden;
10319
- /* Vertical scrolling only: horizontal deltas are swallowed here
10320
- instead of chaining up and panning the whole component */
10321
- overscroll-behavior-x: none;
10322
- touch-action: pan-y;
10323
- position: relative;
10324
- display: flex;
10325
- flex-direction: column;
10326
- --list-gutter: 0;
10327
- }
10328
- .all-row {
10329
- display: flex;
10330
- align-items: center;
10331
- padding: var(--space-xs) var(--space-s);
10332
- cursor: pointer;
10333
- user-select: none;
10334
- min-height: 28px;
10335
- box-sizing: border-box;
10336
- outline: 1px solid transparent;
10337
- background: var(--surface-3);
10338
- }
10339
- .all-row:hover,
10340
- .all-row[data-selected] {
10341
- background: var(--surface-5);
10342
- }
10343
- .all-row[data-selected] {
10344
- outline-color: var(--accent);
10345
- font-weight: 600;
10346
- }
10347
- ixfx-grouped-item-lister {
10348
- display: block;
10349
- }
10350
- .drop-caret {
10351
- position: absolute;
10352
- top: 0;
10353
- bottom: 0;
10354
- width: 2px;
10355
- background: var(--accent);
10356
- border-radius: 1px;
10357
- pointer-events: none;
10358
- z-index: 10;
10359
- }
10360
- .item-label {
10361
- flex: 1;
10362
- }
10363
- .count {
10364
- opacity: 0.6;
10365
- margin-left: var(--space-s);
10366
- font-size: var(--text-s);
10367
- }
10368
- `
11273
+ multicolListStyles
10369
11274
  ];
10370
11275
  }
10371
- get columns() {
10372
- return this._columns.map((c) => c.getState());
10373
- }
10374
- setColumns(states) {
10375
- const existingMap = new Map(this._columns.map((c) => [c.id, c]));
10376
- const newColumns = [];
10377
- for (const colState of states) {
10378
- const existing = existingMap.get(colState.id);
10379
- if (existing) {
10380
- if (existing.categoryKey !== colState.categoryKey) {
10381
- const newCategory = findLeafCategory(this.categories, colState.categoryKey);
10382
- if (newCategory) existing.setCategory(newCategory);
10383
- }
10384
- existing.updateState(colState);
10385
- newColumns.push(existing);
10386
- existingMap.delete(colState.id);
10387
- continue;
10388
- }
10389
- const category = findLeafCategory(this.categories, colState.categoryKey);
10390
- if (!category) continue;
10391
- const controller = this.#createController(colState.id, category);
10392
- controller.updateState(colState);
10393
- newColumns.push(controller);
10394
- }
10395
- for (const col of existingMap.values()) col.unmount();
10396
- this._columns = newColumns;
10397
- this._updateColOffsets();
10398
- this._emitColumnsChange(`add`);
10399
- this._requeryAllColumns();
10400
- this.requestUpdate();
10401
- }
11276
+ #viewState;
11277
+ /** Saved columns whose category isn't in `categories` (yet). */
11278
+ #pendingColumns;
11279
+ /** Saved selection / scroll for column ids that don't exist (yet). */
11280
+ #pendingSelected;
11281
+ #pendingScroll;
10402
11282
  addColumn(categoryKey, position) {
10403
11283
  if (this.maxColumns > 0 && this._columns.length >= this.maxColumns) return;
10404
11284
  const category = findLeafCategory(this.categories, categoryKey);
10405
11285
  if (!category) return;
10406
- const id = `col-${++this._idCounter}`;
11286
+ let id = `col-${++this._idCounter}`;
11287
+ while (this._columns.some((c) => c.id === id) || this.#pendingColumns.some((p) => p.state.id === id)) id = `col-${++this._idCounter}`;
10407
11288
  const controller = this.#createController(id, category);
10408
11289
  if (position !== void 0 && position >= 0 && position < this._columns.length) this._columns.splice(position, 0, controller);
10409
11290
  else this._columns.push(controller);
@@ -10431,9 +11312,15 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10431
11312
  }));
10432
11313
  }
10433
11314
  clearSelection() {
11315
+ this.#dropPendingSelection();
10434
11316
  for (const col of this._columns) col.clearSelection();
10435
11317
  this.requestUpdate();
10436
11318
  }
11319
+ createRenderRoot() {
11320
+ const root = super.createRenderRoot();
11321
+ root.addEventListener(VIEW_STATE_CHANGE, (e) => e.stopPropagation());
11322
+ return root;
11323
+ }
10437
11324
  render() {
10438
11325
  return html`
10439
11326
  <div id="container">
@@ -10442,7 +11329,136 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10442
11329
  </div>
10443
11330
  `;
10444
11331
  }
11332
+ /**
11333
+ * Snapshot the column layout (in display order) and each column's
11334
+ * selection and scroll, keyed by column id. Includes pending state —
11335
+ * saved columns whose category isn't known and keys not yet in the data.
11336
+ */
11337
+ getViewState(options) {
11338
+ const withPending = options?.includePending ?? true;
11339
+ const selectedByColumn = {};
11340
+ const scrollByColumn = {};
11341
+ if (withPending) {
11342
+ for (const [id, keys] of this.#pendingSelected) selectedByColumn[id] = [...keys];
11343
+ for (const [id, scroll] of this.#pendingScroll) scrollByColumn[id] = scroll;
11344
+ }
11345
+ for (const col of this._columns) {
11346
+ const keys = keysToArray(col.selectedValues, withPending ? col.pendingSelection : []);
11347
+ if (keys.length > 0) selectedByColumn[col.id] = keys;
11348
+ const scroll = (withPending ? col.pendingScroll : void 0) ?? col.captureScroll();
11349
+ if (scroll) scrollByColumn[col.id] = scroll;
11350
+ }
11351
+ return this.#viewState.shape({
11352
+ columns: mergeColumns(this._columns.map((c) => c.getState()), withPending ? this.#pendingColumns : []),
11353
+ selectedByColumn,
11354
+ scrollByColumn
11355
+ }, options);
11356
+ }
11357
+ /**
11358
+ * Apply saved view state; fields present replace the current ones.
11359
+ * `columns` creates, reorders, re-points and removes columns to match
11360
+ * (columns whose category isn't in `categories` wait until it is).
11361
+ * Selection keys not in a column's data are applied when they arrive.
11362
+ */
11363
+ setViewState(state) {
11364
+ if (!this.#viewState.accepts(state)) return;
11365
+ this.#viewState.restoring(() => {
11366
+ if (state.columns) this.#applyColumns(sanitiseColumns(state.columns));
11367
+ if (state.selectedByColumn) {
11368
+ const wanted = mapFromRecord(state.selectedByColumn, (v) => Array.isArray(v) ? keysFromJson(v) : void 0);
11369
+ for (const col of this._columns) {
11370
+ col.restoreSelection(wanted.get(col.id) ?? EMPTY_KEYS);
11371
+ wanted.delete(col.id);
11372
+ }
11373
+ this.#pendingSelected = wanted;
11374
+ }
11375
+ if (state.scrollByColumn) {
11376
+ const wanted = mapFromRecord(state.scrollByColumn, (v) => isScrollState(v) ? v : void 0);
11377
+ for (const col of this._columns) {
11378
+ const scroll = wanted.get(col.id);
11379
+ if (scroll) col.restoreScroll(scroll);
11380
+ else col.clearPendingScroll();
11381
+ wanted.delete(col.id);
11382
+ }
11383
+ this.#pendingScroll = wanted;
11384
+ }
11385
+ this.#viewState.notify();
11386
+ });
11387
+ this.requestUpdate();
11388
+ }
11389
+ clearPendingViewState() {
11390
+ this.#pendingColumns = [];
11391
+ this.#pendingSelected = /* @__PURE__ */ new Map();
11392
+ this.#pendingScroll = /* @__PURE__ */ new Map();
11393
+ for (const col of this._columns) {
11394
+ col.clearPendingSelection();
11395
+ col.clearPendingScroll();
11396
+ }
11397
+ }
11398
+ /** Reuse live columns by id (re-pointing their category), create the rest, drop the others. */
11399
+ #applyColumns(saved) {
11400
+ const hasCategory = (key) => findLeafCategory(this.categories, key) !== void 0;
11401
+ const { ready, pending } = partitionColumns(saved, hasCategory);
11402
+ const existing = new Map(this._columns.map((c) => [c.id, c]));
11403
+ const next = ready.map((colState) => this.#columnFromState(colState, existing));
11404
+ for (const col of existing.values()) col.unmount();
11405
+ if (saved.length > 0) this._autoSeeded = true;
11406
+ this.#pendingColumns = pending;
11407
+ this._columns = next;
11408
+ this._updateColOffsets();
11409
+ this._emitColumnsChange(`add`);
11410
+ this._requeryAllColumns();
11411
+ }
11412
+ /** Live column for `colState`: the existing one with that id (taken from `existing`), or a new one. */
11413
+ #columnFromState(colState, existing) {
11414
+ const category = findLeafCategory(this.categories, colState.categoryKey);
11415
+ const live = existing.get(colState.id);
11416
+ existing.delete(colState.id);
11417
+ if (live) {
11418
+ if (live.categoryKey !== colState.categoryKey) live.setCategory(category);
11419
+ live.updateState(colState);
11420
+ return live;
11421
+ }
11422
+ const controller = this.#createController(colState.id, category);
11423
+ controller.updateState(colState);
11424
+ const keys = this.#pendingSelected.get(colState.id);
11425
+ if (keys) {
11426
+ controller.restoreSelection(keys);
11427
+ this.#pendingSelected = withoutKey(this.#pendingSelected, colState.id);
11428
+ }
11429
+ const scroll = this.#pendingScroll.get(colState.id);
11430
+ if (scroll) {
11431
+ controller.restoreScroll(scroll);
11432
+ this.#pendingScroll = withoutKey(this.#pendingScroll, colState.id);
11433
+ }
11434
+ return controller;
11435
+ }
11436
+ /** Insert pending columns whose category has now appeared, at their saved positions. */
11437
+ #resolvePendingColumns() {
11438
+ const hasCategory = (key) => findLeafCategory(this.categories, key) !== void 0;
11439
+ const { ready, pending } = resolvePendingColumns(this.#pendingColumns, hasCategory);
11440
+ if (ready.length === 0) return;
11441
+ this.#viewState.restoring(() => {
11442
+ this.#pendingColumns = pending;
11443
+ const columns = [...this._columns];
11444
+ const none = /* @__PURE__ */ new Map();
11445
+ for (const p of [...ready].sort((a, b) => a.index - b.index)) {
11446
+ if (columns.some((c) => c.id === p.state.id)) continue;
11447
+ columns.splice(Math.min(p.index, columns.length), 0, this.#columnFromState(p.state, none));
11448
+ }
11449
+ this._columns = columns;
11450
+ this._updateColOffsets();
11451
+ this._emitColumnsChange(`add`);
11452
+ this._requeryAllColumns();
11453
+ });
11454
+ }
11455
+ /** An explicit selection change supersedes any saved selection still waiting. */
11456
+ #dropPendingSelection() {
11457
+ this.#pendingSelected = /* @__PURE__ */ new Map();
11458
+ for (const col of this._columns) col.clearPendingSelection();
11459
+ }
10445
11460
  updated(changedProperties) {
11461
+ if (changedProperties.has(`categories`)) this.#resolvePendingColumns();
10446
11462
  const seedCategory = flattenCategories(this.categories)[0];
10447
11463
  if (!this._autoSeeded && this._columns.length === 0 && seedCategory) {
10448
11464
  this._autoSeeded = true;
@@ -10459,8 +11475,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10459
11475
  this._updateColOffsets();
10460
11476
  }
10461
11477
  _renderColumn(col, idx) {
10462
- const rawWidth = col.getState().width ?? DEFAULT_COLUMN_WIDTH;
10463
- const width = Number.isFinite(rawWidth) ? rawWidth : DEFAULT_COLUMN_WIDTH;
11478
+ const rawWidth = col.getState().width ?? 220;
11479
+ const width = Number.isFinite(rawWidth) ? rawWidth : 220;
10464
11480
  return html`
10465
11481
  <div
10466
11482
  class="column"
@@ -10522,7 +11538,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10522
11538
  selectionMode: this.selectionMode,
10523
11539
  dataProvider: this.dataProvider,
10524
11540
  onSelectionChange: this._onColumnSelectionChange.bind(this),
10525
- onRequestData: this._onColumnRequestData.bind(this)
11541
+ onRequestData: this._onColumnRequestData.bind(this),
11542
+ onScroll: () => this.#viewState.notify()
10526
11543
  });
10527
11544
  }
10528
11545
  _onHeaderPointerDown(e, col, idx) {
@@ -10601,11 +11618,11 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10601
11618
  e.preventDefault();
10602
11619
  e.stopPropagation();
10603
11620
  const startX = e.clientX;
10604
- const rawStartW = col.getState().width ?? DEFAULT_COLUMN_WIDTH;
10605
- const startW = Number.isFinite(rawStartW) ? rawStartW : DEFAULT_COLUMN_WIDTH;
11621
+ const rawStartW = col.getState().width ?? 220;
11622
+ const startW = Number.isFinite(rawStartW) ? rawStartW : 220;
10606
11623
  this._resizingColumnId = col.id;
10607
11624
  const applyWidth = (clientX) => {
10608
- const w = Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, startW + clientX - startX));
11625
+ const w = Math.max(80, Math.min(480, startW + clientX - startX));
10609
11626
  col.setWidth(w);
10610
11627
  return w;
10611
11628
  };
@@ -10734,8 +11751,10 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10734
11751
  this._requeryAllColumns();
10735
11752
  this.requestUpdate();
10736
11753
  }
10737
- _onColumnSelectionChange(columnId, _values) {
11754
+ _onColumnSelectionChange(columnId, _values, origin) {
10738
11755
  if (!this._columns.find((c) => c.id === columnId)) return;
11756
+ if (origin === `user`) this.#dropPendingSelection();
11757
+ this.#viewState.notify(origin);
10739
11758
  for (const otherCol of this._columns) if (otherCol.id !== columnId) otherCol.requestData(this._buildQuery(otherCol));
10740
11759
  const detail = {
10741
11760
  columnId,
@@ -10770,22 +11789,23 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10770
11789
  col.setCategory(leaves[0]);
10771
11790
  changed = true;
10772
11791
  }
10773
- if (changed) this._emitColumnsChange(`category`);
11792
+ if (changed) this._emitColumnsChange(`category`, `data`);
10774
11793
  }
10775
11794
  _updateColOffsets() {
10776
11795
  const offsets = [0];
10777
11796
  let sum = 0;
10778
11797
  for (const col of this._columns) {
10779
- const rawWidth = col.getState().width ?? DEFAULT_COLUMN_WIDTH;
10780
- sum += Number.isFinite(rawWidth) ? rawWidth : DEFAULT_COLUMN_WIDTH;
11798
+ const rawWidth = col.getState().width ?? 220;
11799
+ sum += Number.isFinite(rawWidth) ? rawWidth : 220;
10781
11800
  offsets.push(sum);
10782
11801
  }
10783
11802
  if (!(offsets.length === this._colOffsets.length && offsets.every((v, i) => v === this._colOffsets[i] || Number.isNaN(v) && Number.isNaN(this._colOffsets[i])))) this._colOffsets = offsets;
10784
11803
  }
10785
- _emitColumnsChange(reason) {
11804
+ _emitColumnsChange(reason, viewStateReason = `user`) {
11805
+ this.#viewState.notify(viewStateReason);
10786
11806
  this.dispatchEvent(new CustomEvent(`multicol-columns-change`, {
10787
11807
  detail: {
10788
- columns: this.columns,
11808
+ columns: this._columns.map((c) => c.getState()),
10789
11809
  reason
10790
11810
  },
10791
11811
  bubbles: true,
@@ -10795,8 +11815,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
10795
11815
  async autoSizeColumns(options) {
10796
11816
  return new Promise((resolve) => {
10797
11817
  setTimeout(() => {
10798
- const minWidth = options?.minWidth ?? MIN_COLUMN_WIDTH;
10799
- const maxWidth = options?.maxWidth ?? MAX_COLUMN_WIDTH;
11818
+ const minWidth = options?.minWidth ?? 80;
11819
+ const maxWidth = options?.maxWidth ?? 480;
10800
11820
  const measure = (text) => {
10801
11821
  const span = document.createElement(`span`);
10802
11822
  span.style.position = `absolute`;
@@ -10874,6 +11894,11 @@ __decorate([state()], MulticolListElement.prototype, "_draggingColumnId", void 0
10874
11894
  __decorate([state()], MulticolListElement.prototype, "_dropCaretX", void 0);
10875
11895
  __decorate([query(`#container`)], MulticolListElement.prototype, "_containerEl", void 0);
10876
11896
  MulticolListElement = __decorate([safeCustomElement(`ixfx-multicol-list`)], MulticolListElement);
11897
+ function withoutKey(map, key) {
11898
+ const next = new Map(map);
11899
+ next.delete(key);
11900
+ return next;
11901
+ }
10877
11902
  //#endregion
10878
11903
  //#region src/notifier/pill-renderer.ts
10879
11904
  const ROW_SPACING = 8;
@@ -14536,6 +15561,242 @@ function computeTreemapLayout(root, options = {}) {
14536
15561
  return rects;
14537
15562
  }
14538
15563
  //#endregion
15564
+ //#region src/treemap/treemap-view-state.ts
15565
+ function indexTreemapKeys(root) {
15566
+ const out = /* @__PURE__ */ new Map();
15567
+ if (!root) return out;
15568
+ const walk = (node, parentKey, depth) => {
15569
+ if (!out.has(node.item.key)) out.set(node.item.key, {
15570
+ node,
15571
+ parentKey,
15572
+ depth
15573
+ });
15574
+ for (const child of node.children ?? []) walk(child, node.item.key, depth + 1);
15575
+ };
15576
+ walk(root, void 0, -1);
15577
+ return out;
15578
+ }
15579
+ /** Key path from the root's children down to `key` (`[]` for the root itself). */
15580
+ function treemapKeyPath(index, key) {
15581
+ const path = [];
15582
+ for (let entry = index.get(key); entry !== void 0; entry = entry.parentKey === void 0 ? void 0 : index.get(entry.parentKey)) {
15583
+ if (entry.parentKey === void 0) return path;
15584
+ path.unshift(entry.node.item.key);
15585
+ }
15586
+ }
15587
+ /**
15588
+ * Where a saved zoom path stands against the loaded tree. The target is
15589
+ * matched by key anywhere (it may have moved); failing that, the deepest
15590
+ * loaded ancestor on the path is reported for loading when its children
15591
+ * haven't been fetched.
15592
+ */
15593
+ function resolveZoomPath(index, path) {
15594
+ const target = path.at(-1);
15595
+ if (target === void 0) return {
15596
+ kind: `found`,
15597
+ key: void 0
15598
+ };
15599
+ if (index.has(target)) return {
15600
+ kind: `found`,
15601
+ key: target
15602
+ };
15603
+ for (let i = path.length - 2; i >= 0; i--) {
15604
+ const entry = index.get(path[i]);
15605
+ if (!entry) continue;
15606
+ const { node, depth } = entry;
15607
+ return node.children === void 0 && node.item.isLeaf !== true ? {
15608
+ kind: `load`,
15609
+ node,
15610
+ depth
15611
+ } : { kind: `missing` };
15612
+ }
15613
+ return { kind: `missing` };
15614
+ }
15615
+ /**
15616
+ * Zoom: a live zoom root that left the tree goes pending (view falls back to
15617
+ * the full tree); a pending path whose target is now loaded is applied.
15618
+ * Selection: see `reconcileSelection` in `common/view-state/tree.ts`.
15619
+ */
15620
+ function reconcileTreemapViewState(input) {
15621
+ const { index } = input;
15622
+ let zoom;
15623
+ let pendingZoom = input.pendingZoom;
15624
+ if (input.zoomKey !== void 0 && !index.has(input.zoomKey)) {
15625
+ pendingZoom ??= input.zoomPath;
15626
+ zoom = {
15627
+ key: void 0,
15628
+ path: [],
15629
+ restored: false
15630
+ };
15631
+ } else if (input.zoomKey !== void 0) {
15632
+ const path = treemapKeyPath(index, input.zoomKey) ?? input.zoomPath;
15633
+ if (!samePath(path, input.zoomPath)) zoom = {
15634
+ key: input.zoomKey,
15635
+ path,
15636
+ restored: false
15637
+ };
15638
+ }
15639
+ if (pendingZoom) {
15640
+ const resolved = resolveZoomPath(index, pendingZoom);
15641
+ if (resolved.kind === `found`) {
15642
+ const key = resolved.key;
15643
+ zoom = {
15644
+ key,
15645
+ path: key === void 0 ? [] : treemapKeyPath(index, key) ?? pendingZoom,
15646
+ restored: true
15647
+ };
15648
+ pendingZoom = void 0;
15649
+ }
15650
+ }
15651
+ const selection = reconcileSelection$1(input.selected, input.pendingSelected, (key) => index.get(key)?.node, input.canSelect, input.singleSelect);
15652
+ return {
15653
+ zoom,
15654
+ pendingZoom,
15655
+ selection
15656
+ };
15657
+ }
15658
+ function samePath(a, b) {
15659
+ return a.length === b.length && a.every((k, i) => k === b[i]);
15660
+ }
15661
+ //#endregion
15662
+ //#region src/treemap/treemap-view-state-binding.ts
15663
+ function createTreemapViewState(host) {
15664
+ const { controller } = host;
15665
+ /** Key path of the zoom root as last seen — what goes pending if it leaves the tree. */
15666
+ let zoomPath = [];
15667
+ let pendingZoom;
15668
+ let pendingSelected = EMPTY_KEYS;
15669
+ let pendingFocus;
15670
+ /** The cursor was last moved by keyboard / search (only then is it saved as `focus`). */
15671
+ let keyboardCursor = false;
15672
+ let structureVersion = 0;
15673
+ let reconciledVersion = -1;
15674
+ let cache;
15675
+ const index = () => {
15676
+ if (cache?.version !== structureVersion) cache = {
15677
+ version: structureVersion,
15678
+ index: indexTreemapKeys(host.root())
15679
+ };
15680
+ return cache.index;
15681
+ };
15682
+ const cursorKey = () => keyboardCursor ? host.tickled() : void 0;
15683
+ const applyZoom = (key, path, restored) => {
15684
+ zoomPath = path;
15685
+ if (host.zoomKey() === key) return;
15686
+ host.setZoom(key, restored);
15687
+ controller.notify();
15688
+ };
15689
+ const reconcileCursor$1 = (idx) => {
15690
+ const cursor = reconcileCursor(cursorKey(), pendingFocus, (key) => idx.has(key), host.isVisible);
15691
+ pendingFocus = cursor.pending;
15692
+ const tickled = host.tickled();
15693
+ if (cursor.clear || tickled !== void 0 && !idx.has(tickled)) host.setTickled(void 0);
15694
+ if (cursor.apply !== void 0) {
15695
+ host.setTickled(cursor.apply);
15696
+ keyboardCursor = true;
15697
+ controller.notify();
15698
+ }
15699
+ };
15700
+ /** Move state for missing keys to pending; apply pending state for keys now loaded. */
15701
+ const reconcile = () => {
15702
+ reconciledVersion = structureVersion;
15703
+ if (!host.root()) return;
15704
+ const idx = index();
15705
+ controller.restoring(() => {
15706
+ const r = reconcileTreemapViewState({
15707
+ index: idx,
15708
+ zoomKey: host.zoomKey(),
15709
+ zoomPath,
15710
+ pendingZoom,
15711
+ selected: host.selected(),
15712
+ pendingSelected,
15713
+ canSelect: host.canSelect,
15714
+ singleSelect: host.singleSelect()
15715
+ });
15716
+ pendingZoom = r.pendingZoom;
15717
+ if (r.zoom) applyZoom(r.zoom.key, r.zoom.path, r.zoom.restored);
15718
+ const sel = r.selection;
15719
+ if (sel.changed) {
15720
+ pendingSelected = sel.pending;
15721
+ const primaryKey = host.selectedNode()?.item.key;
15722
+ const primary = [...sel.selected].find((n) => n.item.key === primaryKey) ?? [...sel.selected].at(-1);
15723
+ host.setSelection(sel.selected, primary, sel.restored);
15724
+ }
15725
+ reconcileCursor$1(idx);
15726
+ });
15727
+ };
15728
+ return {
15729
+ /** The structure changed (new root / model, invalidation, lazy load). */
15730
+ invalidate() {
15731
+ structureVersion++;
15732
+ },
15733
+ /** Run in `willUpdate`. `zoomGone` — the zoom root vanished through in-place mutation. */
15734
+ reconcileIfNeeded(zoomGone) {
15735
+ if (zoomGone) structureVersion++;
15736
+ if (reconciledVersion !== structureVersion) reconcile();
15737
+ },
15738
+ /** An explicit zoom: supersedes a saved one still waiting. */
15739
+ zoomed(key) {
15740
+ pendingZoom = void 0;
15741
+ zoomPath = key === void 0 ? [] : treemapKeyPath(index(), key) ?? [key];
15742
+ controller.notify();
15743
+ },
15744
+ /** Selection changed — explicit changes supersede a saved selection still waiting. */
15745
+ selectionChanged() {
15746
+ if (!controller.isRestoring) pendingSelected = EMPTY_KEYS;
15747
+ controller.notify();
15748
+ },
15749
+ clearPendingSelection() {
15750
+ pendingSelected = EMPTY_KEYS;
15751
+ },
15752
+ /** The cursor moved by keyboard / search navigation. */
15753
+ keyboardCursorMoved() {
15754
+ keyboardCursor = true;
15755
+ pendingFocus = void 0;
15756
+ controller.notify();
15757
+ },
15758
+ /** The cursor moved by pointer. */
15759
+ pointerCursorMoved() {
15760
+ if (!keyboardCursor) return;
15761
+ keyboardCursor = false;
15762
+ controller.notify();
15763
+ },
15764
+ get(options) {
15765
+ const withPending = options?.includePending ?? true;
15766
+ return controller.shape({
15767
+ selected: keysToArray([...host.selected()].map((n) => n.item.key), withPending ? pendingSelected : []),
15768
+ focus: cursorKey() ?? (withPending ? pendingFocus : void 0),
15769
+ zoom: [...(withPending ? pendingZoom : void 0) ?? zoomPath]
15770
+ }, options);
15771
+ },
15772
+ set(state) {
15773
+ if (!controller.accepts(state)) return;
15774
+ controller.restoring(() => {
15775
+ if (state.selected) {
15776
+ pendingSelected = capKeys(new Set(state.selected));
15777
+ if (host.selected().size > 0) host.setSelection(/* @__PURE__ */ new Set(), void 0, true);
15778
+ }
15779
+ if (state.focus !== void 0) pendingFocus = state.focus;
15780
+ const zoom = pathFromJson(state.zoom);
15781
+ if (zoom) pendingZoom = zoom;
15782
+ reconcile();
15783
+ if (zoom && pendingZoom) applyZoom(void 0, [], true);
15784
+ });
15785
+ },
15786
+ clearPending() {
15787
+ pendingSelected = EMPTY_KEYS;
15788
+ pendingFocus = void 0;
15789
+ pendingZoom = void 0;
15790
+ },
15791
+ /** Load the next unloaded ancestor on a pending zoom path (one load at a time). */
15792
+ continueRestoreLoads() {
15793
+ if (!pendingZoom || !host.restoreLoads() || !host.canLoad()) return;
15794
+ const resolved = resolveZoomPath(index(), pendingZoom);
15795
+ if (resolved.kind === `load` && !host.isLoading(resolved.node.item.key)) host.load(resolved.node, resolved.depth);
15796
+ }
15797
+ };
15798
+ }
15799
+ //#endregion
14539
15800
  //#region src/treemap/treemap-list.ts
14540
15801
  let TreemapListElement = class TreemapListElement extends LitElement {
14541
15802
  constructor(..._args) {
@@ -14557,6 +15818,33 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14557
15818
  this.disableKeyboardNavigation = false;
14558
15819
  this.registry = void 0;
14559
15820
  this._zoomKey = void 0;
15821
+ this.restoreLoads = true;
15822
+ this.#viewState = createTreemapViewState({
15823
+ controller: new ViewStateController(this, {
15824
+ kind: `treemap-list`,
15825
+ capture: () => this.getViewState()
15826
+ }),
15827
+ root: () => this.#effectiveRoot,
15828
+ zoomKey: () => this._zoomKey,
15829
+ setZoom: (key, restored) => this.#restoreZoom(key, restored),
15830
+ selected: () => this._selectedNodes,
15831
+ selectedNode: () => this.selectedNode,
15832
+ canSelect: (node) => this.#canSelect(node),
15833
+ singleSelect: () => this.interactionMode === `implicit`,
15834
+ setSelection: (next, primary, fire) => {
15835
+ const prev = this._selectedNodes;
15836
+ this._selectedNodes = new Set(next);
15837
+ this.selectedNode = primary;
15838
+ if (fire) this.#dispatchSelect(prev);
15839
+ },
15840
+ tickled: () => this.tickled.tickledId,
15841
+ setTickled: (key) => key === void 0 ? this.tickled.clearTickled() : this.tickled.setTickled(key),
15842
+ isVisible: (key) => this.#rects().some((r) => r.node.item.key === key),
15843
+ restoreLoads: () => this.restoreLoads,
15844
+ canLoad: () => this.model !== void 0 || this.loadChildren !== void 0,
15845
+ isLoading: (key) => this.#pendingKeys.has(key),
15846
+ load: (node, depth) => void this.#loadChildren(node, depth)
15847
+ });
14560
15848
  this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
14561
15849
  labelSelector: `.treemap-label`,
14562
15850
  highlightKey: `incr-search-highlight`
@@ -14595,6 +15883,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14595
15883
  if (this._zoomKey === void 0) return void 0;
14596
15884
  return findTreemapNode(this.#effectiveRoot, this._zoomKey);
14597
15885
  }
15886
+ #viewState;
14598
15887
  #modelUnsubscribe;
14599
15888
  #pendingKeys;
14600
15889
  #aborters;
@@ -14690,40 +15979,38 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14690
15979
  this.#unregisterSearchCommands?.();
14691
15980
  this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
14692
15981
  }
15982
+ willUpdate(changedProps) {
15983
+ if (changedProps.has(`root`) && this.root || changedProps.has(`model`)) this.#onDataReset();
15984
+ this.#viewState.reconcileIfNeeded(this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey));
15985
+ }
14693
15986
  updated(changedProps) {
14694
- if (changedProps.has(`root`) && this.root) this.#resetViewState();
14695
15987
  if (changedProps.has(`model`)) {
14696
15988
  this.#modelUnsubscribe?.();
14697
15989
  this.#modelUnsubscribe = void 0;
14698
- if (this.model) {
14699
- this.#modelUnsubscribe = this.model._subscribe((event) => {
14700
- if (event.type === `root`) {
14701
- this.#resetViewState();
14702
- this.requestUpdate();
14703
- } else if (event.type === `invalidate`) this.requestUpdate();
14704
- });
14705
- this.#resetViewState();
14706
- }
15990
+ if (this.model) this.#modelUnsubscribe = this.model._subscribe((event) => {
15991
+ if (event.type === `root`) this.#onDataReset();
15992
+ else if (event.type === `invalidate`) this.#viewState.invalidate();
15993
+ this.requestUpdate();
15994
+ });
14707
15995
  }
14708
- if (changedProps.has(`interactionMode`)) {
15996
+ if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
14709
15997
  this.#anchorIndex = void 0;
14710
15998
  this.clearSelection();
14711
15999
  }
14712
16000
  if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
14713
- if (this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
14714
16001
  this.#ensureVisibleChildren();
16002
+ this.#viewState.continueRestoreLoads();
14715
16003
  if (this.#pendingFlip) {
14716
16004
  const flip = this.#pendingFlip;
14717
16005
  this.#pendingFlip = void 0;
14718
16006
  this.#applyFlip(flip);
14719
16007
  }
14720
16008
  }
14721
- #resetViewState() {
16009
+ /** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
16010
+ #onDataReset() {
14722
16011
  this.#pendingFlip = void 0;
14723
- this.#clearSelectionInternal();
14724
- this.tickled.clearTickled();
14725
- this._zoomKey = void 0;
14726
16012
  this.#anchorIndex = void 0;
16013
+ this.#viewState.invalidate();
14727
16014
  }
14728
16015
  #ensureVisibleChildren() {
14729
16016
  if (!this.model && !this.loadChildren) return;
@@ -14754,7 +16041,10 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14754
16041
  } finally {
14755
16042
  this.#pendingKeys.delete(key);
14756
16043
  this.#aborters.delete(key);
14757
- if (!controller.signal.aborted) this.requestUpdate();
16044
+ if (!controller.signal.aborted) {
16045
+ this.#viewState.invalidate();
16046
+ this.requestUpdate();
16047
+ }
14758
16048
  }
14759
16049
  }
14760
16050
  #canSelect(node) {
@@ -14768,6 +16058,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14768
16058
  this.selectedNode = void 0;
14769
16059
  }
14770
16060
  #dispatchSelect(previous) {
16061
+ this.#viewState.selectionChanged();
14771
16062
  this.dispatchEvent(new CustomEvent(`select`, {
14772
16063
  detail: {
14773
16064
  selected: new Set(this._selectedNodes),
@@ -14795,6 +16086,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14795
16086
  this.requestUpdate();
14796
16087
  }
14797
16088
  clearSelection() {
16089
+ this.#viewState.clearPendingSelection();
14798
16090
  if (this._selectedNodes.size === 0) return;
14799
16091
  const prev = new Set(this._selectedNodes);
14800
16092
  this.#clearSelectionInternal();
@@ -14833,7 +16125,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14833
16125
  zoomTo(node) {
14834
16126
  if (this._zoomKey === node.item.key) return;
14835
16127
  this.#pendingFlip = this.#planFlip(node.item.key);
14836
- this._zoomKey = node.item.key;
16128
+ this.#setZoom(node.item.key);
14837
16129
  this.tickled.clearTickled();
14838
16130
  const depth = findTreemapPath(this.#effectiveRoot, node.item.key)?.length;
14839
16131
  const absoluteDepth = depth === void 0 ? -1 : depth - 2;
@@ -14865,7 +16157,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14865
16157
  const previous = this.focusedNode;
14866
16158
  const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
14867
16159
  const absoluteDepth = path === void 0 ? -1 : path.length - 2;
14868
- this._zoomKey = void 0;
16160
+ this.#setZoom(void 0);
14869
16161
  this.tickled.clearTickled();
14870
16162
  this.dispatchEvent(new CustomEvent(`zoom`, {
14871
16163
  detail: {
@@ -14885,6 +16177,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14885
16177
  }));
14886
16178
  this.requestUpdate();
14887
16179
  }
16180
+ /** An explicit zoom change: supersedes any saved zoom still waiting. */
16181
+ #setZoom(key) {
16182
+ this._zoomKey = key;
16183
+ this.#viewState.zoomed(key);
16184
+ }
16185
+ /** Zoom set by view-state reconcile. `restored` fires `zoom` like an explicit zoom, without animating. */
16186
+ #restoreZoom(key, restored) {
16187
+ this._zoomKey = key;
16188
+ if (!restored) return;
16189
+ const path = key === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, key);
16190
+ this.dispatchEvent(new CustomEvent(`zoom`, {
16191
+ detail: {
16192
+ node: path?.at(-1),
16193
+ depth: path === void 0 ? -1 : path.length - 2
16194
+ },
16195
+ bubbles: true,
16196
+ composed: true
16197
+ }));
16198
+ }
14888
16199
  /**
14889
16200
  * Work out (before `_zoomKey` changes) whether the upcoming zoom should
14890
16201
  * animate, and how — see `FlipPlan`. Returns `undefined` to skip the
@@ -14994,10 +16305,20 @@ let TreemapListElement = class TreemapListElement extends LitElement {
14994
16305
  const ancestor = this.#resolveKeyPath(path.slice(0, path.length - this.maxDepth));
14995
16306
  if (ancestor) this.zoomTo(ancestor);
14996
16307
  }
14997
- this.tickled.setTickled(key);
16308
+ this.#setKeyboardCursor(key);
14998
16309
  this.requestUpdate();
14999
16310
  return true;
15000
16311
  }
16312
+ /** Move the cursor as keyboard / search navigation does — recorded as `focus` in the view state. */
16313
+ #setKeyboardCursor(key) {
16314
+ this.tickled.setTickled(key);
16315
+ this.#viewState.keyboardCursorMoved();
16316
+ }
16317
+ #onTilePointerEnter(key) {
16318
+ const before = this.tickled.tickledId;
16319
+ this.tickled.handlePointerEnter(key);
16320
+ if (this.tickled.tickledId !== before) this.#viewState.pointerCursorMoved();
16321
+ }
15001
16322
  #resolveKeyPath(keys) {
15002
16323
  let current = this.#effectiveRoot;
15003
16324
  for (const key of keys) {
@@ -15027,6 +16348,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
15027
16348
  depth: r.depth
15028
16349
  }));
15029
16350
  }
16351
+ /**
16352
+ * Snapshot selection, zoom path and keyboard cursor as plain JSON, keyed by
16353
+ * `item.key`. Includes pending state for keys not currently loaded.
16354
+ */
16355
+ getViewState(options) {
16356
+ return this.#viewState.get(options);
16357
+ }
16358
+ /**
16359
+ * Apply saved view state. Fields present replace the current ones; keys
16360
+ * that aren't loaded yet are applied as they appear — a deep `zoom` loads
16361
+ * its ancestors via `loadChildren` / `model` when `restoreLoads`.
16362
+ */
16363
+ setViewState(state) {
16364
+ this.#viewState.set(state);
16365
+ this.requestUpdate();
16366
+ }
16367
+ clearPendingViewState() {
16368
+ this.#viewState.clearPending();
16369
+ }
15030
16370
  #matchesFilter(node, depth) {
15031
16371
  if (!this.filterPredicate) return true;
15032
16372
  if (this.filterPredicate(node, depth)) return true;
@@ -15128,7 +16468,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
15128
16468
  const moveTo = (rect) => {
15129
16469
  if (!rect) return;
15130
16470
  event.preventDefault();
15131
- this.tickled.setTickled(rect.node.item.key);
16471
+ this.#setKeyboardCursor(rect.node.item.key);
15132
16472
  if (this.interactionMode === `implicit`) this.select(rect.node);
15133
16473
  };
15134
16474
  switch (event.key) {
@@ -15309,7 +16649,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
15309
16649
  e.stopPropagation();
15310
16650
  this.#handleItemDblClick(rect);
15311
16651
  }}
15312
- @pointerenter=${() => this.tickled.handlePointerEnter(node.item.key)}
16652
+ @pointerenter=${() => this.#onTilePointerEnter(node.item.key)}
15313
16653
  >
15314
16654
  ${showHeader ? html`<div class="tile-header" part="tile-header"><span class="treemap-label" part="label">${node.item.label}</span></div>` : html`<span class="treemap-label ${hideLabel ? `hide-label` : ``}" part="label">${node.item.label}</span>`}
15315
16655
  ${childrenContent}
@@ -15623,6 +16963,7 @@ __decorate([property({
15623
16963
  reflect: true
15624
16964
  })], TreemapListElement.prototype, "searchModes", void 0);
15625
16965
  __decorate([state()], TreemapListElement.prototype, "_zoomKey", void 0);
16966
+ __decorate([property({ attribute: false })], TreemapListElement.prototype, "restoreLoads", void 0);
15626
16967
  TreemapListElement = __decorate([safeCustomElement(`ixfx-treemap-list`)], TreemapListElement);
15627
16968
  //#endregion
15628
16969
  //#region src/twosplit-layout/split-layout.ts
@@ -15649,6 +16990,10 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
15649
16990
  this.#nestedSplitInitialSizeA = ``;
15650
16991
  this.#pendingNestedUpdate = null;
15651
16992
  this.#hysteresisThreshold = .1;
16993
+ this.#viewState = new ViewStateController(this, {
16994
+ kind: `twosplit-layout`,
16995
+ capture: () => this.getViewState()
16996
+ });
15652
16997
  this.#onResizeHandleDown = (event) => {
15653
16998
  event.preventDefault();
15654
16999
  const zoneEl = this.getZone();
@@ -15665,6 +17010,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
15665
17010
  if (zoneEl) zoneEl.classList.remove(`user-resizing`);
15666
17011
  window.removeEventListener(`pointerup`, this.#onResizeHandleUp);
15667
17012
  window.removeEventListener(`pointermove`, this.#onResizeHandleMove);
17013
+ if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
15668
17014
  };
15669
17015
  this.#onResizeHandleMove = (event) => {
15670
17016
  const bounds = this.#elementBounds;
@@ -15784,9 +17130,11 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
15784
17130
  if (nestedSplit) {
15785
17131
  const nestedZoneEl = nestedSplit.getZone();
15786
17132
  if (nestedZoneEl) nestedZoneEl.classList.remove(`user-resizing`);
17133
+ if (#viewState in nestedSplit && nestedSplit.sizeA !== this.#nestedSplitInitialSizeA) nestedSplit.#viewState.notify();
15787
17134
  }
15788
17135
  window.removeEventListener(`pointerup`, this.#onCornerResizeUp);
15789
17136
  window.removeEventListener(`pointermove`, this.#onCornerResizeMove);
17137
+ if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
15790
17138
  };
15791
17139
  this.#onCornerResizeMove = (event) => {
15792
17140
  const outerBounds = this.#elementBounds;
@@ -15854,6 +17202,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
15854
17202
  #collapsedAPointerAtCollapse;
15855
17203
  #collapsedBPointerAtCollapse;
15856
17204
  #hysteresisThreshold;
17205
+ #viewState;
15857
17206
  disconnectedCallback() {
15858
17207
  super.disconnectedCallback();
15859
17208
  this.#observer?.disconnect();
@@ -15863,7 +17212,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
15863
17212
  }
15864
17213
  updated(changedProperties) {
15865
17214
  if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateCornerPosition();
15866
- if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`)) this.#updateDataCollapsed();
17215
+ if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateDataCollapsed();
15867
17216
  }
15868
17217
  #updateDataCollapsed() {
15869
17218
  if (this.collapsedA) this.setAttribute(`data-collapsed-a`, ``);
@@ -16022,6 +17371,31 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
16022
17371
  else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) cornerZone.style.cursor = `nesw-resize`;
16023
17372
  }
16024
17373
  }
17374
+ /** Snapshot both sides' size and collapse. */
17375
+ getViewState(options) {
17376
+ return this.#viewState.shape({ layout: {
17377
+ a: sideState(this.sizeA, this.collapsedA),
17378
+ b: sideState(this.sizeB, this.collapsedB)
17379
+ } }, options);
17380
+ }
17381
+ /** Restore sizes / collapse. A side missing from `layout` is left as it is. */
17382
+ setViewState(state) {
17383
+ if (!this.#viewState.accepts(state) || !state.layout) return;
17384
+ const { a, b } = state.layout;
17385
+ this.#viewState.restoring(() => {
17386
+ if (typeof a?.size === `string`) {
17387
+ this.sizeA = a.size;
17388
+ this.collapsedA = a.collapsed === true;
17389
+ }
17390
+ if (typeof b?.size === `string`) {
17391
+ this.sizeB = b.size;
17392
+ this.collapsedB = b.collapsed === true;
17393
+ }
17394
+ this.#viewState.notify();
17395
+ });
17396
+ }
17397
+ /** Both slots always exist, so nothing is ever pending. */
17398
+ clearPendingViewState() {}
16025
17399
  getZone() {
16026
17400
  if (!this.shadowRoot) return;
16027
17401
  const el = this.shadowRoot.querySelector(`#zone`);
@@ -16209,6 +17583,12 @@ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "minSi
16209
17583
  __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "maxSizeB", void 0);
16210
17584
  __decorate([state()], TwoSplitLayoutElement.prototype, "_hasCorner", void 0);
16211
17585
  TwoSplitLayoutElement = __decorate([safeCustomElement(`ixfx-twosplit-layout`)], TwoSplitLayoutElement);
17586
+ function sideState(size, collapsed) {
17587
+ return collapsed ? {
17588
+ size,
17589
+ collapsed
17590
+ } : { size };
17591
+ }
16212
17592
  //#endregion
16213
17593
  //#region src/util/checkbox.ts
16214
17594
  function isCheckboxLike(el) {
@@ -16267,4 +17647,4 @@ function init() {
16267
17647
  console.log(`Init`);
16268
17648
  }
16269
17649
  //#endregion
16270
- export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, CheckedSelect, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EMPTY_GROUP_LABEL, EditableLabel, EditableNumber, FlatListElement, FormElement, FormGroupElement, FormOptionsElement, FormPartElement, FormRowElement, 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, ListGroupHeaderElement, 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, SUNBURST_PATH_SEPARATOR, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SunburstTreeListElement, 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, computeSunburstLayout, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createValueResolver, createVerticalListAdapter, decomposeViewIsometry, defaultGroupLabel, defaultSunburstValueAccessor, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findEmptiedStacks, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findSunburstId, findSunburstNodeById, 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, isSunburstAncestorId, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, partitionByGroup, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveGroupKey, resolveLeafPath, resolveTracks, rotationIsometry, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, snapshotNonEmptyStacks, startListItemOutAnimation, sunburstChildId, sunburstHasChildren, sunburstIdToPath, sunburstParentId, sunburstPathToId, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toGroupKey, toSnapshot, toggleInSet, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, wrapRoot };
17650
+ export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, CheckedSelect, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EMPTY_FLAGS, EMPTY_GROUP_LABEL, EMPTY_KEYS, EditableLabel, EditableNumber, FlatListElement, FormElement, FormGroupElement, FormOptionsElement, FormPartElement, FormRowElement, 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, ListGroupHeaderElement, ListNativeDragController, ListSearchController, ListSelectionController, MAX_PENDING_KEYS, 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, SCROLL_HOVER_SUPPRESS_ATTR, SUNBURST_PATH_SEPARATOR, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SunburstTreeListElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VIEW_STATE_CHANGE, VIEW_STATE_VERSION, VerticalListElement, ViewStateController, XyPad, addPanelToStack, anchorFromRow, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bindViewState, bottomDrawerCommandId, buildHighlightTargets, buildModel, capFlags, capKeys, captionForEntry, captureElementScroll, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeSunburstLayout, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createScrollHoverGuard, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createValueResolver, createVerticalListAdapter, decomposeViewIsometry, defaultGroupLabel, defaultSunburstValueAccessor, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findEmptiedStacks, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleKeyPath, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findSunburstId, findSunburstNodeById, findTreemapNode, findTreemapPath, flagsFromRecord, flagsToRecord, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, isSunburstAncestorId, isViewStateful, keyForElement, keysAsFlags, keysToArray, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, onUserScrollIntent, panelGapIndex, parseColourScaleAttr, parseLayout, partitionByGroup, partitionFlags, partitionKeys, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveGroupKey, resolveIcicleKeyPath, resolveLeafPath, resolveTracks, restoreElementScroll, rotationIsometry, scrollToOffset, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, snapshotNonEmptyStacks, startListItemOutAnimation, sunburstChildId, sunburstHasChildren, sunburstIdToPath, sunburstParentId, sunburstPathToId, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toGroupKey, toSnapshot, toggleInSet, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, withoutFlag, wrapRoot };