@ixfx/components 0.15.5 → 0.16.1

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 (80) hide show
  1. package/bundle/index.d.ts +986 -49
  2. package/bundle/index.js +765 -729
  3. package/dist/ac-token.js +1 -1
  4. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  5. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  6. package/dist/crumbs.d.ts +2 -115
  7. package/dist/crumbs.js +1 -1179
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-Cqxuwgbb.js} +615 -210
  9. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-Csh5EJW9.d.ts} +55 -4
  10. package/dist/data-grid.d.ts +3 -3
  11. package/dist/data-grid.js +1 -1
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-Bo5ze3SN.js} +234 -9
  13. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-DSZ1DDor.d.ts} +48 -4
  14. package/dist/detail-list.d.ts +4 -4
  15. package/dist/detail-list.js +1 -1
  16. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-CT9kX3TQ.d.ts} +27 -4
  17. package/dist/flat-list.d.ts +2 -2
  18. package/dist/flat-list.js +123 -4
  19. package/dist/{grid-list-y6dWJYu3.js → grid-list-D0dJNVEF.js} +278 -76
  20. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-DDLrWRxm.d.ts} +128 -22
  21. package/dist/grid-list.d.ts +3 -3
  22. package/dist/grid-list.js +1 -1
  23. package/dist/{incr-search-JIhwB49y.js → incr-search-CgbL-OlS.js} +140 -10
  24. package/dist/incr-search.d.ts +1 -1
  25. package/dist/incr-search.js +1 -1
  26. package/dist/index-9ttcdwlL.d.ts +71 -0
  27. package/dist/index-Bnm-kKGC.d.ts +475 -0
  28. package/dist/{index-KN2MW_xD.d.ts → index-CUErGhCg.d.ts} +31 -4
  29. package/dist/{index-DEVyVIn8.d.ts → index-D-lZNlHa.d.ts} +52 -6
  30. package/dist/index.d.ts +341 -21
  31. package/dist/index.js +2286 -884
  32. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  33. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  34. package/dist/miller-DUqiUAWQ.js +2230 -0
  35. package/dist/miller.d.ts +4 -437
  36. package/dist/miller.js +1 -1815
  37. package/dist/narrowed-text.js +1 -1
  38. package/dist/pending-BGidHppL.js +91 -0
  39. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  40. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  41. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  42. package/dist/split-layout.d.ts +1 -1
  43. package/dist/split-layout.js +1 -1
  44. package/dist/sunburst-tree.d.ts +2 -2
  45. package/dist/sunburst-tree.js +1 -1
  46. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_M0YTEA.js} +299 -80
  47. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-DePuFYlU.d.ts} +40 -3
  48. package/dist/tree-CftHwtEb.js +146 -0
  49. package/dist/{tree-D8VxvR9u.js → tree-DAlgvaHB.js} +276 -48
  50. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  51. package/dist/tree.d.ts +3 -3
  52. package/dist/tree.js +1 -1
  53. package/dist/types-LU1u0CB8.d.ts +87 -0
  54. package/dist/vertical-list-DyKPKGxK.js +1993 -0
  55. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-SAx56U-l.d.ts} +31 -3
  56. package/dist/vertical-list.d.ts +3 -3
  57. package/dist/vertical-list.js +1 -1789
  58. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  59. package/docs-user/README.md +2 -0
  60. package/docs-user/crumbs.md +27 -0
  61. package/docs-user/data-grid.md +79 -5
  62. package/docs-user/detail-list.md +69 -0
  63. package/docs-user/dock.md +63 -12
  64. package/docs-user/flat-list.md +27 -0
  65. package/docs-user/grid-list.md +69 -13
  66. package/docs-user/hyperbolic-tree.md +31 -4
  67. package/docs-user/icicle-tree.md +36 -0
  68. package/docs-user/index.json +12 -2
  69. package/docs-user/llms.txt +2 -0
  70. package/docs-user/miller.md +52 -21
  71. package/docs-user/multicol-list.md +50 -8
  72. package/docs-user/split-layout.md +45 -17
  73. package/docs-user/sunburst-tree.md +32 -2
  74. package/docs-user/tree.md +45 -6
  75. package/docs-user/treemap.md +32 -0
  76. package/docs-user/twosplit-layout.md +22 -0
  77. package/docs-user/user-catalog.md +1 -1
  78. package/docs-user/vertical-list.md +55 -7
  79. package/llms.txt +2 -0
  80. package/package.json +2 -2
@@ -1,8 +1,11 @@
1
1
  import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
2
2
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
3
3
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
4
- import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-JIhwB49y.js";
4
+ import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-CgbL-OlS.js";
5
+ import { a as capKeys, l as keysToArray, n as EMPTY_KEYS } from "./pending-BGidHppL.js";
5
6
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
7
+ import { t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
8
+ import { a as resolveKeyPath } from "./tree-CftHwtEb.js";
6
9
  import { LitElement, css, html, nothing, svg } from "lit";
7
10
  import { property, state } from "lit/decorators.js";
8
11
  import { createCommandRegistry } from "@clinth/ui-commands";
@@ -313,6 +316,26 @@ function targetHues(layout, view, zoomId) {
313
316
  function siblingShade(index, setSize) {
314
317
  return setSize <= 1 ? 0 : (index + .5) / setSize - .5;
315
318
  }
319
+ /** Settled visual for every arc to draw in `view`: the centre plus `maxDepth` rings. */
320
+ function computeTargets(input) {
321
+ const { layout, view, rings, zoomId, maxDepth } = input;
322
+ const targets = /* @__PURE__ */ new Map();
323
+ if (zoomId === void 0) return targets;
324
+ const hues = targetHues(layout, view, zoomId);
325
+ for (const entry of layout.values()) {
326
+ const p = projectArc(entry, view);
327
+ if (p.depth < -1 || p.depth >= maxDepth) continue;
328
+ if (p.depth === -1 ? entry.id !== zoomId : !hues.has(entry.id)) continue;
329
+ if (p.depth >= 0 && arcLength(p.a0, p.a1, arcRadii(p.depth, rings).outer) < input.minArcLength) continue;
330
+ targets.set(entry.id, {
331
+ ...p,
332
+ hue: hues.get(entry.id) ?? input.displayed.get(entry.id)?.hue ?? 0,
333
+ shade: siblingShade(entry.index, entry.setSize),
334
+ opacity: 1
335
+ });
336
+ }
337
+ return targets;
338
+ }
316
339
  //#endregion
317
340
  //#region src/sunburst-tree/keyboard.ts
318
341
  /**
@@ -343,6 +366,45 @@ function sunburstKeyTarget(arcs, currentId, key) {
343
366
  }
344
367
  }
345
368
  //#endregion
369
+ //#region src/sunburst-tree/render.ts
370
+ function renderSunburstLabels(input) {
371
+ const labels = [];
372
+ const guides = [];
373
+ for (const [id, visual] of input.displayed) {
374
+ const entry = input.layout.get(id);
375
+ if (!entry || visual.opacity < .05) continue;
376
+ const place = labelPlacement(visual, entry.node.item.label, input.rings, input.fontSize, { curved: input.curved });
377
+ if (!place) continue;
378
+ const style = `--arc-hue:${visual.hue.toFixed(2)};opacity:${visual.opacity.toFixed(3)}`;
379
+ if (place.kind === `curved`) {
380
+ const guideId = `${input.idPrefix}${guides.length}`;
381
+ guides.push(svg`<path id=${guideId} d=${place.path}></path>`);
382
+ labels.push(svg`<text class="sunburst-label curved" part="label" style=${style}
383
+ ><textPath href="#${guideId}" startOffset="50%">${place.text}</textPath></text>`);
384
+ } else labels.push(svg`<text
385
+ class="sunburst-label"
386
+ part="label"
387
+ transform="translate(${place.x.toFixed(2)} ${place.y.toFixed(2)}) rotate(${place.rotate.toFixed(2)})"
388
+ style=${style}
389
+ >${place.text}</text>`);
390
+ }
391
+ return svg`<defs>${guides}</defs>${labels}`;
392
+ }
393
+ /** Label, value and share of the centre (`zoom`), or of the tickled arc when there is one. */
394
+ function renderSunburstReadout(zoom, tickled, rings, fontSize, formatter) {
395
+ if (!zoom) return nothing;
396
+ const shown = tickled ?? zoom;
397
+ const maxChars = Math.max(4, Math.floor(rings.center * 1.7 / (fontSize * .6)));
398
+ const label = shown.node.item.label;
399
+ const text = label.length <= maxChars ? label : `${label.slice(0, maxChars - 1)}…`;
400
+ const share = tickled && zoom.value > 0 ? ` · ${(tickled.value / zoom.value * 100).toFixed(tickled.value / zoom.value < .1 ? 1 : 0)}%` : ``;
401
+ const line = fontSize * 1.3;
402
+ return svg`<g class="center-readout" part="center-readout" aria-hidden="true">
403
+ <text class="center-label" y=${(-line / 2).toFixed(2)}>${text}</text>
404
+ <text class="center-meta" y=${(line / 2).toFixed(2)}>${formatter(shown.value, shown.node)}${share}</text>
405
+ </g>`;
406
+ }
407
+ //#endregion
346
408
  //#region src/sunburst-tree/styles.ts
347
409
  /**
348
410
  * Shadow-DOM styles for `ixfx-sunbursttree-list`.
@@ -467,6 +529,98 @@ const sunburstTreeStyles = css`
467
529
  }
468
530
  `;
469
531
  //#endregion
532
+ //#region src/sunburst-tree/sunburst-view-state.ts
533
+ /** Index the loaded tree by id, node and key. */
534
+ function indexSunburstTree(root) {
535
+ const byId = /* @__PURE__ */ new Map();
536
+ const idOf = /* @__PURE__ */ new Map();
537
+ const byKey = /* @__PURE__ */ new Map();
538
+ const visit = (node, id) => {
539
+ byId.set(id, node);
540
+ idOf.set(node, id);
541
+ if (!byKey.has(node.item.key)) byKey.set(node.item.key, node);
542
+ for (const child of node.children ?? []) visit(child, sunburstChildId(id, child.item.key));
543
+ };
544
+ if (root) visit(root, root.item.key);
545
+ return {
546
+ byId,
547
+ idOf,
548
+ byKey
549
+ };
550
+ }
551
+ /**
552
+ * As `reconcileSelection` (tree view state), but live nodes are re-found by
553
+ * key path rather than by bare key:
554
+ * - selected nodes still in the tree stay; replaced ones (immutable rebuild)
555
+ * swap for the node now at their old path; ones whose path has gone go
556
+ * pending (their `item.key`)
557
+ * - pending keys that resolve (and pass `canSelect`) are selected
558
+ */
559
+ function reconcileSunburstSelection(input) {
560
+ const { selected, pending, primary } = input;
561
+ const unchanged = {
562
+ selected,
563
+ pending,
564
+ primary,
565
+ restored: false,
566
+ changed: false
567
+ };
568
+ if (selected.size === 0 && pending.size === 0) return unchanged;
569
+ const previousIds = /* @__PURE__ */ new Map();
570
+ for (const entry of input.previousLayout.values()) previousIds.set(entry.node, entry.id);
571
+ if (pending.size === 0 && [...selected].every((node) => {
572
+ const id = previousIds.get(node);
573
+ return id !== void 0 && findSunburstNodeById(input.root, id) === node;
574
+ })) return unchanged;
575
+ const { byId, idOf, byKey } = indexSunburstTree(input.root);
576
+ const locate = (node) => {
577
+ if (idOf.has(node)) return node;
578
+ const id = previousIds.get(node);
579
+ return id === void 0 ? void 0 : byId.get(id);
580
+ };
581
+ const next = /* @__PURE__ */ new Set();
582
+ const nextPending = /* @__PURE__ */ new Set();
583
+ let changed = false;
584
+ for (const node of selected) {
585
+ const current = locate(node);
586
+ if (current) {
587
+ next.add(current);
588
+ changed ||= current !== node;
589
+ } else {
590
+ nextPending.add(node.item.key);
591
+ changed = true;
592
+ }
593
+ }
594
+ let restored;
595
+ for (const key of pending) {
596
+ const node = byKey.get(key);
597
+ if (node && input.canSelect(node)) {
598
+ next.add(node);
599
+ restored = node;
600
+ changed = true;
601
+ } else nextPending.add(key);
602
+ }
603
+ if (!changed) return unchanged;
604
+ if (input.single && next.size > 0) {
605
+ const only = restored ?? [...next].at(-1);
606
+ return {
607
+ selected: /* @__PURE__ */ new Set([only]),
608
+ pending: EMPTY_KEYS,
609
+ primary: only,
610
+ restored: restored !== void 0,
611
+ changed: true
612
+ };
613
+ }
614
+ const located = primary && locate(primary);
615
+ return {
616
+ selected: next,
617
+ pending: capKeys(nextPending),
618
+ primary: located && next.has(located) ? located : [...next].at(-1),
619
+ restored: restored !== void 0,
620
+ changed: true
621
+ };
622
+ }
623
+ //#endregion
470
624
  //#region src/sunburst-tree/tween.ts
471
625
  function easeInOutCubic(t) {
472
626
  return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
@@ -565,6 +719,12 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
565
719
  this.disableKeyboardNavigation = false;
566
720
  this.registry = void 0;
567
721
  this._zoomId = void 0;
722
+ this.restoreLoads = true;
723
+ this.#viewState = new ViewStateController(this, {
724
+ kind: `sunbursttree-list`,
725
+ capture: () => this.getViewState()
726
+ });
727
+ this.#pendingSelected = EMPTY_KEYS;
568
728
  this._size = {
569
729
  width: 0,
570
730
  height: 0
@@ -648,6 +808,12 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
648
808
  if (this._zoomId === void 0) return void 0;
649
809
  return findSunburstNodeById(this.#effectiveRoot, this._zoomId);
650
810
  }
811
+ #viewState;
812
+ /** Saved selection for keys not (yet) loaded. */
813
+ #pendingSelected;
814
+ /** Saved zoom path not (yet) resolvable. */
815
+ #pendingZoom;
816
+ #pendingFocus;
651
817
  #layout;
652
818
  #view;
653
819
  #rings;
@@ -759,14 +925,16 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
759
925
  this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
760
926
  }
761
927
  willUpdate(changed) {
762
- if (changed.has(`root`) && changed.get(`root`) !== void 0) this.#remapSelection();
928
+ if (changed.has(`root`) && changed.get(`root`) !== void 0) this.#anchorIndex = void 0;
763
929
  if (changed.has(`interactionMode`) && changed.get(`interactionMode`) !== void 0) {
764
930
  this.#anchorIndex = void 0;
765
931
  this.#clearSelectionInternal();
766
932
  }
767
933
  if (this._layoutDirty) {
768
934
  this._layoutDirty = false;
935
+ this.#reconcileViewState();
769
936
  this.#retarget();
937
+ this.#viewState.restoring(() => this.#reconcileFocus());
770
938
  }
771
939
  }
772
940
  updated(changed) {
@@ -776,6 +944,7 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
776
944
  }
777
945
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
778
946
  this.#ensureVisibleChildren();
947
+ this.#continueRestoreLoads();
779
948
  }
780
949
  #outerRadius() {
781
950
  const width = this._size.width || FALLBACK_SIZE;
@@ -797,7 +966,15 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
797
966
  const zoomId = this.#zoomEntryId;
798
967
  const view = sunburstViewFor(zoomId === void 0 ? void 0 : layout.get(zoomId));
799
968
  const rings = computeRings(this.#outerRadius(), maxDepth, this.centerRatio);
800
- const targets = this.#computeTargets(layout, view, rings, zoomId, maxDepth);
969
+ const targets = computeTargets({
970
+ layout,
971
+ view,
972
+ rings,
973
+ zoomId,
974
+ maxDepth,
975
+ minArcLength: this.minArcLength,
976
+ displayed: this.#displayed
977
+ });
801
978
  this.#layout = layout;
802
979
  this.#view = view;
803
980
  this.#rings = rings;
@@ -839,24 +1016,6 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
839
1016
  });
840
1017
  this.#startTransition(transition);
841
1018
  }
842
- #computeTargets(layout, view, rings, zoomId, maxDepth) {
843
- const targets = /* @__PURE__ */ new Map();
844
- if (zoomId === void 0) return targets;
845
- const hues = targetHues(layout, view, zoomId);
846
- for (const entry of layout.values()) {
847
- const p = projectArc(entry, view);
848
- if (p.depth < -1 || p.depth >= maxDepth) continue;
849
- if (p.depth === -1 ? entry.id !== zoomId : !hues.has(entry.id)) continue;
850
- if (p.depth >= 0 && arcLength(p.a0, p.a1, arcRadii(p.depth, rings).outer) < this.minArcLength) continue;
851
- targets.set(entry.id, {
852
- ...p,
853
- hue: hues.get(entry.id) ?? this.#displayed.get(entry.id)?.hue ?? 0,
854
- shade: siblingShade(entry.index, entry.setSize),
855
- opacity: 1
856
- });
857
- }
858
- return targets;
859
- }
860
1019
  #startTransition(transition) {
861
1020
  if ((prefersReducedMotion() ? 0 : this.animationDuration) <= 0 || !this.isConnected) {
862
1021
  this.#transition = void 0;
@@ -870,31 +1029,116 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
870
1029
  this.#raf ??= requestAnimationFrame(this.#tick);
871
1030
  }
872
1031
  #tick;
873
- /** After `root` is replaced, carry the selection over by key path. */
874
- #remapSelection() {
875
- if (this._selectedNodes.size === 0) return;
876
- const idOf = /* @__PURE__ */ new Map();
877
- for (const entry of this.#layout.values()) idOf.set(entry.node, entry.id);
1032
+ /**
1033
+ * Snapshot selection, zoom path and keyboard cursor as plain JSON, keyed by
1034
+ * `item.key`. Includes pending state for keys not currently loaded.
1035
+ */
1036
+ getViewState(options) {
1037
+ const withPending = options?.includePending ?? true;
1038
+ const tickled = this.tickled.inputMode === `keyboard` ? this.tickled.tickledId : void 0;
1039
+ const focus = tickled === void 0 ? void 0 : sunburstIdToPath(tickled).at(-1);
1040
+ return this.#viewState.shape({
1041
+ selected: keysToArray([...this._selectedNodes].map((n) => n.item.key), withPending ? this.#pendingSelected : []),
1042
+ zoom: (withPending ? this.#pendingZoom : void 0) ?? this.#zoomPath(),
1043
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0)
1044
+ }, options);
1045
+ }
1046
+ /**
1047
+ * Apply saved view state. Fields present replace the current ones; keys
1048
+ * that aren't loaded yet are applied as they appear — including via
1049
+ * `loadChildren` / `model` down a saved zoom path when `restoreLoads`.
1050
+ */
1051
+ setViewState(state) {
1052
+ if (!this.#viewState.accepts(state)) return;
1053
+ this.#viewState.restoring(() => {
1054
+ if (state.selected) {
1055
+ this.#pendingSelected = capKeys(new Set(state.selected));
1056
+ if (this._selectedNodes.size > 0) this.#commitSelection(/* @__PURE__ */ new Set());
1057
+ }
1058
+ if (state.zoom) this.#pendingZoom = [...state.zoom];
1059
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
1060
+ this.#reconcileViewState();
1061
+ if (this.#pendingZoom !== void 0) this.#setZoom(void 0);
1062
+ });
1063
+ this.requestUpdate();
1064
+ }
1065
+ clearPendingViewState() {
1066
+ this.#pendingSelected = EMPTY_KEYS;
1067
+ this.#pendingZoom = void 0;
1068
+ this.#pendingFocus = void 0;
1069
+ }
1070
+ #zoomPath() {
1071
+ return this._zoomId === void 0 ? [] : sunburstIdToPath(this._zoomId).slice(1);
1072
+ }
1073
+ /** Move zoom / selection whose path has gone to pending; apply pending state now resolvable. */
1074
+ #reconcileViewState() {
878
1075
  const root = this.#effectiveRoot;
879
- const remap = (node) => {
880
- const id = node === void 0 ? void 0 : idOf.get(node);
881
- return id === void 0 ? void 0 : findSunburstNodeById(root, id);
882
- };
883
- this._selectedNodes = new Set([...this._selectedNodes].map(remap).filter((n) => n !== void 0));
884
- this.selectedNode = remap(this.selectedNode) ?? [...this._selectedNodes].at(-1);
885
- this.#anchorIndex = void 0;
1076
+ this.#viewState.restoring(() => {
1077
+ if (this._zoomId !== void 0 && !findSunburstNodeById(root, this._zoomId)) {
1078
+ this.#pendingZoom ??= this.#zoomPath();
1079
+ this._zoomId = void 0;
1080
+ this.#viewState.notify(`data`);
1081
+ }
1082
+ const zoom = this.#pendingZoom;
1083
+ if (root && zoom !== void 0 && resolveKeyPath(root, zoom, () => false).status === `found`) {
1084
+ this.#pendingZoom = void 0;
1085
+ this.#setZoom(sunburstPathToId([root.item.key, ...zoom]));
1086
+ }
1087
+ if (this._selectedNodes.size > 0 || this.#pendingSelected.size > 0) this.#reconcileSelection(root);
1088
+ });
1089
+ }
1090
+ #reconcileSelection(root) {
1091
+ const sel = reconcileSunburstSelection({
1092
+ root,
1093
+ previousLayout: this.#layout,
1094
+ selected: this._selectedNodes,
1095
+ primary: this.selectedNode,
1096
+ pending: this.#pendingSelected,
1097
+ canSelect: (node) => this.#canSelect(node),
1098
+ single: this.interactionMode === `implicit`
1099
+ });
1100
+ if (!sel.changed) return;
1101
+ this.#pendingSelected = sel.pending;
1102
+ if (sel.restored) this.#commitSelection(new Set(sel.selected), sel.primary);
1103
+ else {
1104
+ this._selectedNodes = new Set(sel.selected);
1105
+ this.selectedNode = sel.primary;
1106
+ this.#viewState.notify(`data`);
1107
+ }
1108
+ }
1109
+ /** Runs after layout: a cursor whose arc has gone goes pending; a pending one lands once its arc is drawn. */
1110
+ #reconcileFocus() {
1111
+ const tickled = this.tickled.tickledId;
1112
+ if (tickled !== void 0 && !this.#layout.has(tickled)) {
1113
+ if (this.tickled.inputMode === `keyboard`) this.#pendingFocus ??= sunburstIdToPath(tickled).at(-1);
1114
+ this.tickled.clearTickled();
1115
+ }
1116
+ const focus = this.#pendingFocus;
1117
+ const arc = focus === void 0 ? void 0 : this.#visibleArcs().find((a) => a.node.item.key === focus);
1118
+ if (!arc) return;
1119
+ this.#pendingFocus = void 0;
1120
+ this.tickled.setInputMode(`keyboard`);
1121
+ this.tickled.setTickled(arc.id);
1122
+ }
1123
+ /** Load the next unloaded node along a pending zoom path (one load at a time). */
1124
+ #continueRestoreLoads() {
1125
+ const root = this.#effectiveRoot;
1126
+ const zoom = this.#pendingZoom;
1127
+ if (!this.restoreLoads || !root || zoom === void 0 || !this.model && !this.loadChildren) return;
1128
+ const next = resolveKeyPath(root, zoom, (node) => node.item.isLeaf !== true);
1129
+ if (next.status !== `load`) return;
1130
+ const id = sunburstPathToId([root.item.key, ...zoom.slice(0, next.depth + 1)]);
1131
+ if (!this.#pendingIds.has(id)) this.#loadChildren(id, next.node, next.depth);
886
1132
  }
887
1133
  #ensureVisibleChildren() {
888
1134
  if (!this.model && !this.loadChildren) return;
889
1135
  for (const id of this.#targets.keys()) {
890
1136
  const entry = this.#layout.get(id);
891
1137
  if (!entry || entry.node.children !== void 0 || entry.node.item.isLeaf === true || this.#pendingIds.has(id)) continue;
892
- this.#loadChildren(entry);
1138
+ this.#loadChildren(id, entry.node, entry.depth - 1);
893
1139
  }
894
1140
  }
895
- async #loadChildren(entry) {
896
- const { id, node } = entry;
897
- const depth = entry.depth - 1;
1141
+ async #loadChildren(id, node, depth) {
898
1142
  this.#pendingIds.add(id);
899
1143
  const controller = new AbortController();
900
1144
  this.#aborters.set(id, controller);
@@ -928,6 +1172,8 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
928
1172
  const previous = this._selectedNodes;
929
1173
  this._selectedNodes = next;
930
1174
  this.selectedNode = primary;
1175
+ if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
1176
+ this.#viewState.notify();
931
1177
  this.dispatchEvent(new CustomEvent(`select`, {
932
1178
  detail: {
933
1179
  selected: new Set(next),
@@ -947,6 +1193,7 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
947
1193
  this.#commitSelection(next, this.selectedNode === node ? [...next].at(-1) : this.selectedNode);
948
1194
  }
949
1195
  clearSelection() {
1196
+ this.#pendingSelected = EMPTY_KEYS;
950
1197
  if (this._selectedNodes.size > 0) this.#commitSelection(/* @__PURE__ */ new Set());
951
1198
  }
952
1199
  selectAll() {
@@ -963,12 +1210,14 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
963
1210
  return findSunburstId(this.#effectiveRoot, (n) => n === node);
964
1211
  }
965
1212
  #setZoom(id) {
1213
+ if (!this.#viewState.isRestoring) this.#pendingZoom = void 0;
966
1214
  const zoomId = id === this.#effectiveRoot?.item.key ? void 0 : id;
967
1215
  if (zoomId === this._zoomId) return;
968
1216
  const previousId = this._zoomId;
969
1217
  const previous = this.focusedNode;
970
1218
  this._zoomId = zoomId;
971
1219
  this.tickled.clearTickled();
1220
+ this.#viewState.notify();
972
1221
  const node = this.focusedNode;
973
1222
  const depthOf = (key) => key === void 0 ? -1 : sunburstIdToPath(key).length - 2;
974
1223
  this.dispatchEvent(new CustomEvent(`zoom`, {
@@ -1151,6 +1400,8 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
1151
1400
  event.preventDefault();
1152
1401
  this.tickled.setInputMode(`keyboard`);
1153
1402
  this.tickled.setTickled(target);
1403
+ this.#pendingFocus = void 0;
1404
+ this.#viewState.notify();
1154
1405
  const entry = this.#layout.get(target);
1155
1406
  if (entry && this.interactionMode === `implicit`) this.select(entry.node);
1156
1407
  }
@@ -1202,53 +1453,13 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
1202
1453
  @dblclick=${() => this.#handleArcDblClick(entry)}
1203
1454
  @pointerenter=${() => this.tickled.handlePointerEnter(id)}
1204
1455
  ></path>`;
1205
- }
1206
- #renderLabels() {
1207
- if (this.disableLabels) return nothing;
1208
- const labels = [];
1209
- const guides = [];
1210
- for (const [id, visual] of this.#displayed) {
1211
- const entry = this.#layout.get(id);
1212
- if (!entry || visual.opacity < .05) continue;
1213
- const place = labelPlacement(visual, entry.node.item.label, this.#rings, this.#fontSize, { curved: this.curvedLabels });
1214
- if (!place) continue;
1215
- const style = `--arc-hue:${visual.hue.toFixed(2)};opacity:${visual.opacity.toFixed(3)}`;
1216
- if (place.kind === `curved`) {
1217
- const guideId = `${this.#labelIdPrefix}${guides.length}`;
1218
- guides.push(svg`<path id=${guideId} d=${place.path}></path>`);
1219
- labels.push(svg`<text class="sunburst-label curved" part="label" style=${style}
1220
- ><textPath href="#${guideId}" startOffset="50%">${place.text}</textPath></text>`);
1221
- } else labels.push(svg`<text
1222
- class="sunburst-label"
1223
- part="label"
1224
- transform="translate(${place.x.toFixed(2)} ${place.y.toFixed(2)}) rotate(${place.rotate.toFixed(2)})"
1225
- style=${style}
1226
- >${place.text}</text>`);
1227
- }
1228
- return svg`<defs>${guides}</defs>${labels}`;
1229
- }
1230
- /** Label, value and share of the centre (or of the tickled arc). */
1231
- #renderReadout(zoomId) {
1232
- const zoom = zoomId === void 0 ? void 0 : this.#layout.get(zoomId);
1233
- if (!zoom) return nothing;
1234
- const tickledId = this.tickled.tickledId;
1235
- const tickled = tickledId === void 0 ? void 0 : this.#layout.get(tickledId);
1236
- const shown = tickled ?? zoom;
1237
- const maxChars = Math.max(4, Math.floor(this.#rings.center * 1.7 / (this.#fontSize * .6)));
1238
- const label = shown.node.item.label;
1239
- const text = label.length <= maxChars ? label : `${label.slice(0, maxChars - 1)}…`;
1240
- const share = tickled && zoom.value > 0 ? ` · ${(tickled.value / zoom.value * 100).toFixed(tickled.value / zoom.value < .1 ? 1 : 0)}%` : ``;
1241
- const line = this.#fontSize * 1.3;
1242
- return svg`<g class="center-readout" part="center-readout" aria-hidden="true">
1243
- <text class="center-label" y=${(-line / 2).toFixed(2)}>${text}</text>
1244
- <text class="center-meta" y=${(line / 2).toFixed(2)}>${this.valueFormatter(shown.value, shown.node)}${share}</text>
1245
- </g>`;
1246
1456
  }
1247
1457
  render() {
1248
1458
  if (!this.#effectiveRoot) return html`<div class="empty-state">No data</div>${this.search.renderField()}`;
1249
1459
  const width = this._size.width || FALLBACK_SIZE;
1250
1460
  const height = this._size.height || FALLBACK_SIZE;
1251
1461
  const zoomId = this.#zoomEntryId;
1462
+ const tickledId = this.tickled.tickledId;
1252
1463
  return html`
1253
1464
  <svg
1254
1465
  class="sunburst"
@@ -1261,8 +1472,15 @@ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
1261
1472
  @pointerleave=${() => this.tickled.clearTickled()}
1262
1473
  >
1263
1474
  <g class="arcs">${repeat(this.#displayed, ([id]) => id, ([id, visual]) => this.#renderArc(id, visual, zoomId))}</g>
1264
- <g class="labels">${this.#renderLabels()}</g>
1265
- ${this.#renderReadout(zoomId)}
1475
+ <g class="labels">${this.disableLabels ? nothing : renderSunburstLabels({
1476
+ displayed: this.#displayed,
1477
+ layout: this.#layout,
1478
+ rings: this.#rings,
1479
+ fontSize: this.#fontSize,
1480
+ curved: this.curvedLabels,
1481
+ idPrefix: this.#labelIdPrefix
1482
+ })}</g>
1483
+ ${renderSunburstReadout(zoomId === void 0 ? void 0 : this.#layout.get(zoomId), tickledId === void 0 ? void 0 : this.#layout.get(tickledId), this.#rings, this.#fontSize, this.valueFormatter)}
1266
1484
  </svg>
1267
1485
  ${this.search.renderField()}
1268
1486
  `;
@@ -1331,6 +1549,7 @@ __decorate([property({
1331
1549
  reflect: true
1332
1550
  })], SunburstTreeListElement.prototype, "searchModes", void 0);
1333
1551
  __decorate([state()], SunburstTreeListElement.prototype, "_zoomId", void 0);
1552
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "restoreLoads", void 0);
1334
1553
  __decorate([state()], SunburstTreeListElement.prototype, "_size", void 0);
1335
1554
  SunburstTreeListElement = __decorate([safeCustomElement(`ixfx-sunbursttree-list`)], SunburstTreeListElement);
1336
1555
  //#endregion
@@ -1,6 +1,8 @@
1
- import { A as TreeTickleEvent, C as TreeItemDblClickEvent, E as TreeSearchHost, O as TreeSelectEvent, a as TreeComponent, i as TreeCollapseEvent, j as TreeDataModel, n as TreeActivateEvent, o as TreeComponentEventMap, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-C6UHO-w9.js";
1
+ import { C as TreeItemDblClickEvent, E as TreeSearchHost, M as TreeDataModel, a as TreeComponent, i as TreeCollapseEvent, j as TreeTickleEvent, k as TreeSelectEvent, n as TreeActivateEvent, o as TreeComponentEventMap, x as TreeItemClickEvent, y as TreeExpandEvent } from "./tree-component-DgfkxmVY.js";
2
2
  import { a as TreeItem, n as TreeExclusivity, o as TreeNode, r as TreeFilterPredicate, s as TreeSelectionFilter, t as LoadChildrenCallback } from "./types-Jky6oNAp.js";
3
- import { S as ListSearchController } from "./index-KN2MW_xD.js";
3
+ import { a as ViewStateBase, t as GetViewStateOptions } from "./types-LU1u0CB8.js";
4
+ import "./index-9ttcdwlL.js";
5
+ import { S as ListSearchController } from "./index-CUErGhCg.js";
4
6
  import { LitElement, PropertyValues } from "lit";
5
7
  import { CommandRegistry } from "@clinth/ui-commands";
6
8
  //#region src/sunburst-tree/types.d.ts
@@ -120,6 +122,24 @@ type SunburstLabelPlacement = {
120
122
  /** Ids are key paths; keys only need to be unique among siblings. */
121
123
  declare const SUNBURST_PATH_SEPARATOR = "";
122
124
  //#endregion
125
+ //#region src/sunburst-tree/sunburst-view-state.d.ts
126
+ /**
127
+ * Pure helpers behind `ixfx-sunbursttree-list` view state. Arcs are
128
+ * identified by key *path* (keys need only be unique among siblings), so live
129
+ * selection follows its path across data changes; saved keys — which are
130
+ * bare `item.key`s — resolve to the first node, depth-first, carrying them.
131
+ * See `docs/infra-view-state.md`.
132
+ */
133
+ /** `ixfx-sunbursttree-list` view state: `selected`, `focus`, plus the zoom path. */
134
+ type SunburstViewState = ViewStateBase & {
135
+ /**
136
+ * Keys from the root's children down to the centre node; `[]` = the full
137
+ * tree. A path because keys are only unique among siblings, and so a
138
+ * lazily-loaded tree can be opened down to it, one load at a time.
139
+ */
140
+ readonly zoom?: readonly string[];
141
+ };
142
+ //#endregion
123
143
  //#region src/sunburst-tree/sunbursttree-list.d.ts
124
144
  /** Fired when the zoom root changes. `node` is `undefined` on zoom-out to the full tree. */
125
145
  type SunburstZoomDetail = {
@@ -209,6 +229,11 @@ declare class SunburstTreeListElement extends LitElement implements TreeComponen
209
229
  get focusedNode(): TreeNode | undefined;
210
230
  /** Id (key path) of the zoom node; `undefined` = the true root. */
211
231
  private _zoomId;
232
+ /**
233
+ * Whether restoring a saved zoom may call `loadChildren` / `model` to load
234
+ * the nodes along its path (one at a time). Default `true`.
235
+ */
236
+ restoreLoads: boolean;
212
237
  private _size;
213
238
  readonly search: ListSearchController<{
214
239
  node: TreeNode;
@@ -223,6 +248,18 @@ declare class SunburstTreeListElement extends LitElement implements TreeComponen
223
248
  disconnectedCallback(): void;
224
249
  protected willUpdate(changed: PropertyValues): void;
225
250
  protected updated(changed: PropertyValues): void;
251
+ /**
252
+ * Snapshot selection, zoom path and keyboard cursor as plain JSON, keyed by
253
+ * `item.key`. Includes pending state for keys not currently loaded.
254
+ */
255
+ getViewState(options?: GetViewStateOptions<SunburstViewState>): SunburstViewState;
256
+ /**
257
+ * Apply saved view state. Fields present replace the current ones; keys
258
+ * that aren't loaded yet are applied as they appear — including via
259
+ * `loadChildren` / `model` down a saved zoom path when `restoreLoads`.
260
+ */
261
+ setViewState(state: Partial<SunburstViewState>): void;
262
+ clearPendingViewState(): void;
226
263
  select(node: TreeNode): void;
227
264
  deselect(node: TreeNode): void;
228
265
  clearSelection(): void;
@@ -263,4 +300,4 @@ interface SunburstTreeListElement {
263
300
  removeEventListener: (<K extends keyof SunburstTreeListElementEventMap>(type: K, listener: (this: SunburstTreeListElement, event: SunburstTreeListElementEventMap[K]) => void, options?: boolean | EventListenerOptions) => void) & ((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) => void);
264
301
  }
265
302
  //#endregion
266
- export { ComputeSunburstLayoutOptions as a, SunburstInteractionMode as c, SunburstLayoutNode as d, SunburstRings as f, SunburstView as h, SunburstZoomEvent as i, SunburstLabelPlacement as l, SunburstValueFormatter as m, SunburstTreeListElementEventMap as n, SUNBURST_PATH_SEPARATOR as o, SunburstValueAccessor as p, SunburstZoomDetail as r, SunburstArcVisual as s, SunburstTreeListElement as t, SunburstLayout as u };
303
+ export { SunburstViewState as a, SunburstArcVisual as c, SunburstLayout as d, SunburstLayoutNode as f, SunburstView as g, SunburstValueFormatter as h, SunburstZoomEvent as i, SunburstInteractionMode as l, SunburstValueAccessor as m, SunburstTreeListElementEventMap as n, ComputeSunburstLayoutOptions as o, SunburstRings as p, SunburstZoomDetail as r, SUNBURST_PATH_SEPARATOR as s, SunburstTreeListElement as t, SunburstLabelPlacement as u };