@ixfx/components 0.7.3 → 0.7.4

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 (121) hide show
  1. package/bundle/index.d.ts +727 -256
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2046 -35
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/ac-text.js +1 -1
  7. package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  8. package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  9. package/dist/ac-token.d.ts +1 -1
  10. package/dist/ac-token.js +1 -1
  11. package/dist/button.d.ts +1 -1
  12. package/dist/button.d.ts.map +1 -1
  13. package/dist/colour-picker.d.ts +1 -1
  14. package/dist/crumbs.d.ts +1 -1
  15. package/dist/crumbs.js +3 -3
  16. package/dist/{data-grid-Db4QpnII.d.ts → data-grid-BddUjAti.d.ts} +34 -2
  17. package/dist/data-grid-BddUjAti.d.ts.map +1 -0
  18. package/dist/data-grid.d.ts +1 -1
  19. package/dist/data-grid.js +180 -7
  20. package/dist/data-grid.js.map +1 -1
  21. package/dist/detail-list-Br0WAnry.d.ts +167 -0
  22. package/dist/detail-list-Br0WAnry.d.ts.map +1 -0
  23. package/dist/detail-list.d.ts +3 -0
  24. package/dist/detail-list.js +1115 -0
  25. package/dist/detail-list.js.map +1 -0
  26. package/dist/grid-list-B_orPsot.d.ts +304 -0
  27. package/dist/grid-list-B_orPsot.d.ts.map +1 -0
  28. package/dist/grid-list.d.ts +3 -0
  29. package/dist/grid-list.js +933 -0
  30. package/dist/grid-list.js.map +1 -0
  31. package/dist/{grouped-item-lister-BWlC9S4e.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +3 -3
  32. package/dist/{grouped-item-lister-BWlC9S4e.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
  33. package/dist/grouped-item-lister.d.ts +1 -1
  34. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
  35. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  36. package/dist/icons.d.ts +2 -2
  37. package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
  38. package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  39. package/dist/{index-DsjpDksO.d.ts → index-CS9yM8L-.d.ts} +2 -2
  40. package/dist/{index-DsjpDksO.d.ts.map → index-CS9yM8L-.d.ts.map} +1 -1
  41. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
  42. package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
  43. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
  44. package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
  45. package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
  46. package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
  47. package/dist/index.d.ts +185 -240
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +375 -1002
  50. package/dist/index.js.map +1 -1
  51. package/dist/interaction-Cr13azbM.js +3 -0
  52. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
  53. package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
  54. package/dist/labelled-radial-input.d.ts +2 -2
  55. package/dist/labelled-radial-input.d.ts.map +1 -1
  56. package/dist/labelled-range-input.d.ts +1 -1
  57. package/dist/labelled-slider-input.d.ts +1 -1
  58. package/dist/list-item-animation-DuiwgN3l.js +112 -0
  59. package/dist/list-item-animation-DuiwgN3l.js.map +1 -0
  60. package/dist/list-selection-controller-CUo-Hhcm.js +338 -0
  61. package/dist/list-selection-controller-CUo-Hhcm.js.map +1 -0
  62. package/dist/{list-selection-types-DSuRNWpx.d.ts → list-selection-types-B5ioL-dG.d.ts} +6 -1
  63. package/dist/list-selection-types-B5ioL-dG.d.ts.map +1 -0
  64. package/dist/{menu-DMzjkVnK.js → menu-Bg90Qr0B.js} +3 -3
  65. package/dist/{menu-DMzjkVnK.js.map → menu-Bg90Qr0B.js.map} +1 -1
  66. package/dist/{menu-item-D2S4i8u1.js → menu-item-Bx-Fs7jq.js} +3 -3
  67. package/dist/{menu-item-D2S4i8u1.js.map → menu-item-Bx-Fs7jq.js.map} +1 -1
  68. package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
  69. package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
  70. package/dist/menu.d.ts +2 -2
  71. package/dist/menu.js +2 -2
  72. package/dist/miller.d.ts +1 -1
  73. package/dist/miller.js +2 -2
  74. package/dist/narrowed-text.js +2 -2
  75. package/dist/panel.d.ts +1 -1
  76. package/dist/plots.js +1 -1
  77. package/dist/select-horiz.js +2 -2
  78. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
  79. package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
  80. package/dist/split-layout.d.ts +1 -1
  81. package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
  82. package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
  83. package/dist/tabs.d.ts +1 -1
  84. package/dist/{tickled-controller-Ds1DMyG7.d.ts → tickled-controller-Bn3YdQsM.d.ts} +12 -1
  85. package/dist/{tickled-controller-Ds1DMyG7.d.ts.map → tickled-controller-Bn3YdQsM.d.ts.map} +1 -1
  86. package/dist/{tickled-controller-h9GmJ_bU.js → tickled-controller-C66DgJHl.js} +16 -1
  87. package/dist/tickled-controller-C66DgJHl.js.map +1 -0
  88. package/dist/{titlebar-B23MrIbI.d.ts → titlebar-Bj5pfdw_.d.ts} +2 -2
  89. package/dist/{titlebar-B23MrIbI.d.ts.map → titlebar-Bj5pfdw_.d.ts.map} +1 -1
  90. package/dist/titlebar.d.ts +1 -1
  91. package/dist/{tree-Ch4W8qXQ.js → tree-B74zdI60.js} +116 -17
  92. package/dist/tree-B74zdI60.js.map +1 -0
  93. package/dist/tree.d.ts +6 -3
  94. package/dist/tree.d.ts.map +1 -1
  95. package/dist/tree.js +1 -1
  96. package/dist/vertical-list-CQSEVeOT.d.ts +140 -0
  97. package/dist/vertical-list-CQSEVeOT.d.ts.map +1 -0
  98. package/dist/vertical-list.d.ts +3 -121
  99. package/dist/vertical-list.js +710 -1
  100. package/dist/vertical-list.js.map +1 -0
  101. package/dist/{xy-axis-DWizs0bt.js → xy-axis-VrX8dmvS.js} +2 -2
  102. package/dist/{xy-axis-DWizs0bt.js.map → xy-axis-VrX8dmvS.js.map} +1 -1
  103. package/docs-user/README.md +1 -0
  104. package/docs-user/data-grid.md +51 -2
  105. package/docs-user/detail-list.md +38 -0
  106. package/docs-user/grid-list.md +274 -0
  107. package/docs-user/index.json +7 -2
  108. package/docs-user/llms.txt +1 -0
  109. package/docs-user/tree.md +13 -0
  110. package/docs-user/vertical-list.md +51 -1
  111. package/llms.txt +1 -0
  112. package/package.json +17 -2
  113. package/dist/data-grid-Db4QpnII.d.ts.map +0 -1
  114. package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
  115. package/dist/interaction-D5XdKzbR.js +0 -2
  116. package/dist/list-selection-types-DSuRNWpx.d.ts.map +0 -1
  117. package/dist/tickled-controller-h9GmJ_bU.js.map +0 -1
  118. package/dist/tree-Ch4W8qXQ.js.map +0 -1
  119. package/dist/vertical-list-B4w_ksJk.js +0 -814
  120. package/dist/vertical-list-B4w_ksJk.js.map +0 -1
  121. package/dist/vertical-list.d.ts.map +0 -1
package/bundle/index.js CHANGED
@@ -8,8 +8,9 @@ import { createCommandRegistry } from "@clinth/ui-commands";
8
8
  import { Trees } from "@ixfx/collections";
9
9
  import { clamp, clamper, rangeStream, round, scaler } from "@ixfx/numbers";
10
10
  import { repeat } from "lit/directives/repeat.js";
11
- import { Easings } from "@ixfx/modulation";
11
+ import { createRef, ref } from "lit/directives/ref.js";
12
12
  import { classMap } from "lit/directives/class-map.js";
13
+ import { Easings } from "@ixfx/modulation";
13
14
  import "@clinth/vcanvas";
14
15
  import { keyed } from "lit/directives/keyed.js";
15
16
  //#region node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js
@@ -752,6 +753,21 @@ var TickledController = class TickledController {
752
753
  return this._tickledId;
753
754
  }
754
755
  /**
756
+ * The item currently under the cursor, or `undefined`. Alias of `tickledId`
757
+ * for parity with other controllers.
758
+ */
759
+ get current() {
760
+ return this._tickledId;
761
+ }
762
+ /**
763
+ * Move the cursor programmatically (e.g. to reveal a match or re-seat it
764
+ * after the item set changed). Does not change the input mode. Alias of
765
+ * `setTickled`.
766
+ */
767
+ focus(id) {
768
+ this.setTickled(id);
769
+ }
770
+ /**
755
771
  * Check if a specific item is tickled
756
772
  */
757
773
  isTickled(id) {
@@ -3861,6 +3877,188 @@ __decorate([state()], RadioGroup.prototype, "_icons", void 0);
3861
3877
  __decorate([query(`slot`)], RadioGroup.prototype, "_slot", void 0);
3862
3878
  RadioGroup = __decorate([safeCustomElement(`ixfx-radio-group`)], RadioGroup);
3863
3879
  //#endregion
3880
+ //#region src/interaction/keyed-list.ts
3881
+ /**
3882
+ * Helpers for key-based selection on the Element-flavoured list components
3883
+ * (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
3884
+ *
3885
+ * A "key" is a stable string the host puts on each item element — `data-key`
3886
+ * (preferred) or `data-value`. It survives the host rebuilding the item set
3887
+ * (re-fetch, re-sort, filter) where raw `Element` references do not.
3888
+ */
3889
+ /**
3890
+ * The stable key for an item element: `data-key`, then `data-value`, else
3891
+ * `undefined`.
3892
+ */
3893
+ function keyForElement(el) {
3894
+ return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
3895
+ }
3896
+ /**
3897
+ * A lazily-built key → element index over a container. Rebuilt on first use
3898
+ * and after any child-list mutation under `root`; `invalidate()` forces a
3899
+ * rebuild on the next `get()`.
3900
+ *
3901
+ * ```ts
3902
+ * #keyIndex = new KeyedElementIndex(this);
3903
+ * // ...
3904
+ * const el = this.#keyIndex.get(`row-42`);
3905
+ * // on disconnect:
3906
+ * this.#keyIndex.dispose();
3907
+ * ```
3908
+ */
3909
+ var KeyedElementIndex = class {
3910
+ #root;
3911
+ #selector;
3912
+ #map = /* @__PURE__ */ new Map();
3913
+ #dirty = true;
3914
+ #observer;
3915
+ /**
3916
+ * @param root Container whose descendants are indexed.
3917
+ * @param itemSelector Which descendants count as items (default: any `[data-key]`).
3918
+ */
3919
+ constructor(root, itemSelector = `[data-key]`) {
3920
+ this.#root = root;
3921
+ this.#selector = itemSelector;
3922
+ this.#observer = new MutationObserver(() => {
3923
+ this.#dirty = true;
3924
+ });
3925
+ this.#observer.observe(root, {
3926
+ childList: true,
3927
+ subtree: true
3928
+ });
3929
+ }
3930
+ /** The element with this key, or `undefined`. First match wins on duplicates. */
3931
+ get(key) {
3932
+ if (this.#dirty) this.#rebuild();
3933
+ return this.#map.get(key);
3934
+ }
3935
+ /** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
3936
+ invalidate() {
3937
+ this.#dirty = true;
3938
+ }
3939
+ /** Stop observing and drop the cache. */
3940
+ dispose() {
3941
+ this.#observer.disconnect();
3942
+ this.#map.clear();
3943
+ }
3944
+ #rebuild() {
3945
+ this.#map.clear();
3946
+ for (const el of this.#root.querySelectorAll(this.#selector)) {
3947
+ const key = keyForElement(el);
3948
+ if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
3949
+ }
3950
+ this.#dirty = false;
3951
+ }
3952
+ };
3953
+ //#endregion
3954
+ //#region src/interaction/list-item-animation.ts
3955
+ /**
3956
+ * Shared insertion / removal animation for the list & tree components.
3957
+ *
3958
+ * Two flavours, because the components split into two layout families:
3959
+ *
3960
+ * - **Flow layouts** (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-tree-*`) —
3961
+ * rows sit in normal flow, so a new row can collapse in from zero height and a
3962
+ * leaving row can collapse back to zero, and the rows around it slide to make
3963
+ * or close the gap for free. Use {@link animateListItemIn} /
3964
+ * {@link animateListItemOut} (Web Animations API, exact measured sizes).
3965
+ *
3966
+ * - **Virtualised layouts** (`ixfx-grid-list`, `ixfx-data-grid`) — rows are
3967
+ * absolutely positioned (`transform` / `top`), so collapsing a row's box moves
3968
+ * nothing. Those components instead fade the entering / leaving row with the
3969
+ * {@link listItemFadeKeyframes} keyframes and slide the survivors by
3970
+ * transitioning their position for a short window after the change.
3971
+ *
3972
+ * Everything here is a no-op under `prefers-reduced-motion: reduce`.
3973
+ */
3974
+ /** Default duration (ms) for both flavours; overridable per call / per component. */
3975
+ const LIST_ITEM_MOTION_MS = 200;
3976
+ function prefersReducedMotion() {
3977
+ return typeof window !== `undefined` && typeof window.matchMedia === `function` && window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
3978
+ }
3979
+ /**
3980
+ * Opacity keyframes for the virtualised components. Each one adds its own
3981
+ * one-line rule binding `.<row>.ixfx-item-entering` / `.ixfx-item-leaving` to
3982
+ * these, plus a `prefers-reduced-motion` guard on its own selectors.
3983
+ */
3984
+ const listItemFadeKeyframes = i$3`
3985
+ @keyframes ixfx-list-item-in {
3986
+ from { opacity: 0; }
3987
+ to { opacity: 1; }
3988
+ }
3989
+ @keyframes ixfx-list-item-out {
3990
+ from { opacity: 1; }
3991
+ to { opacity: 0; }
3992
+ }
3993
+ `;
3994
+ function measure(el, axis) {
3995
+ const rect = el.getBoundingClientRect();
3996
+ return axis === `inline` ? rect.width : rect.height;
3997
+ }
3998
+ function resolveDuration(el, explicit) {
3999
+ if (explicit !== void 0) return explicit;
4000
+ const raw = getComputedStyle(el).getPropertyValue(`--ixfx-list-item-move-duration`).trim();
4001
+ if (raw) {
4002
+ const n = Number.parseFloat(raw);
4003
+ if (!Number.isNaN(n)) return raw.endsWith(`ms`) ? n : raw.endsWith(`s`) ? n * 1e3 : n;
4004
+ }
4005
+ return 200;
4006
+ }
4007
+ function collapsedFrame(axis) {
4008
+ const prop = axis === `inline` ? `width` : `height`;
4009
+ const minProp = axis === `inline` ? `minWidth` : `minHeight`;
4010
+ return {
4011
+ [prop]: `0px`,
4012
+ [minProp]: `0px`,
4013
+ opacity: 0,
4014
+ overflow: `hidden`
4015
+ };
4016
+ }
4017
+ function fullFrame(size, axis) {
4018
+ const prop = axis === `inline` ? `width` : `height`;
4019
+ const minProp = axis === `inline` ? `minWidth` : `minHeight`;
4020
+ return {
4021
+ [prop]: `${size}px`,
4022
+ [minProp]: `0px`,
4023
+ opacity: 1,
4024
+ overflow: `hidden`
4025
+ };
4026
+ }
4027
+ /**
4028
+ * Collapse + fade `el` in from nothing to its natural size. Call it just after
4029
+ * the element has been inserted (it must already be laid out at full size so it
4030
+ * can be measured). No-op under reduced motion.
4031
+ */
4032
+ function animateListItemIn(el, options = {}) {
4033
+ if (prefersReducedMotion()) return void 0;
4034
+ const { axis = `block`, easing = `ease` } = options;
4035
+ const duration = resolveDuration(el, options.duration);
4036
+ const size = measure(el, axis);
4037
+ if (size <= 0) return void 0;
4038
+ return el.animate([collapsedFrame(axis), fullFrame(size, axis)], {
4039
+ duration,
4040
+ easing
4041
+ });
4042
+ }
4043
+ /**
4044
+ * Collapse + fade `el` out. Resolves once the animation finishes (or straight
4045
+ * away under reduced motion); the caller detaches the element after. The final
4046
+ * collapsed state is held (`fill: forwards`) so there's no flash back to full
4047
+ * size in the gap before removal.
4048
+ */
4049
+ function animateListItemOut(el, options = {}) {
4050
+ if (prefersReducedMotion()) return Promise.resolve();
4051
+ const { axis = `block`, easing = `ease` } = options;
4052
+ const duration = resolveDuration(el, options.duration);
4053
+ const size = measure(el, axis);
4054
+ if (size <= 0) return Promise.resolve();
4055
+ return el.animate([fullFrame(size, axis), collapsedFrame(axis)], {
4056
+ duration,
4057
+ easing,
4058
+ fill: `forwards`
4059
+ }).finished.then(() => void 0, () => void 0);
4060
+ }
4061
+ //#endregion
3864
4062
  //#region src/interaction/list-selection-controller.ts
3865
4063
  /** Whether the interaction mode allows building a multi-item selection. */
3866
4064
  function isMultiMode(mode) {
@@ -3901,6 +4099,20 @@ var ListSelectionController = class {
3901
4099
  get selected() {
3902
4100
  return this.#selected;
3903
4101
  }
4102
+ /**
4103
+ * The current selection as stable string keys (read-only). Empty unless a
4104
+ * `getKey` was supplied in config. Items whose `getKey` returns `undefined`
4105
+ * are omitted.
4106
+ */
4107
+ get selectedKeys() {
4108
+ const getKey = this.#config.getKey;
4109
+ const out = /* @__PURE__ */ new Set();
4110
+ if (getKey) for (const item of this.#selected) {
4111
+ const key = getKey(item);
4112
+ if (key !== void 0) out.add(key);
4113
+ }
4114
+ return out;
4115
+ }
3904
4116
  /** Anchor for range (shift) selection — the last plainly-selected item. */
3905
4117
  get anchor() {
3906
4118
  return this.#anchor;
@@ -3984,6 +4196,66 @@ var ListSelectionController = class {
3984
4196
  next.delete(item);
3985
4197
  this.#apply(next);
3986
4198
  }
4199
+ /**
4200
+ * Whether the current modes permit a selection of more than one item.
4201
+ * Stricter than `canSelectMultiple()`: `standard` / `implicit` only count
4202
+ * as multi when `selectionMode="multiple"`.
4203
+ */
4204
+ #effectiveMulti() {
4205
+ const { selectionMode, interactionMode } = this.#config.getModes();
4206
+ return selectionMode === `multiple` || interactionMode === `checked` || interactionMode === `sticky` || interactionMode === `vscode`;
4207
+ }
4208
+ /**
4209
+ * Replace the selection with the items resolved from `keys`. Keys that
4210
+ * `resolve` cannot map, or that resolve to a non-selectable item, are
4211
+ * ignored. `selectionMode="none"` is a no-op. When multi-select is not
4212
+ * available, only the last resolvable key is kept. `onChange` fires once.
4213
+ */
4214
+ selectKeys(keys, resolve) {
4215
+ if (this.#config.getModes().selectionMode === `none`) return;
4216
+ let next = /* @__PURE__ */ new Set();
4217
+ for (const key of new Set(keys)) {
4218
+ const item = resolve(key);
4219
+ if (item !== void 0 && this.#config.isSelectable(item)) next.add(item);
4220
+ }
4221
+ if (!this.#effectiveMulti() && next.size > 1) {
4222
+ const last = [...next].at(-1);
4223
+ next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
4224
+ }
4225
+ this.#anchor = [...next].at(-1);
4226
+ this.#apply(next);
4227
+ }
4228
+ /**
4229
+ * Add the items resolved from `keys` to the selection (multi-select only;
4230
+ * a no-op otherwise). `onChange` fires once if anything changed.
4231
+ */
4232
+ addKeys(keys, resolve) {
4233
+ if (!this.#effectiveMulti()) return;
4234
+ const next = new Set(this.#selected);
4235
+ let changed = false;
4236
+ for (const key of new Set(keys)) {
4237
+ const item = resolve(key);
4238
+ if (item !== void 0 && !next.has(item) && this.#config.isSelectable(item)) {
4239
+ next.add(item);
4240
+ changed = true;
4241
+ }
4242
+ }
4243
+ if (changed) this.#apply(next);
4244
+ }
4245
+ /**
4246
+ * Remove the items resolved from `keys` from the selection. `onChange`
4247
+ * fires once if anything changed.
4248
+ */
4249
+ removeKeys(keys, resolve) {
4250
+ if (this.#selected.size === 0) return;
4251
+ const next = new Set(this.#selected);
4252
+ let changed = false;
4253
+ for (const key of new Set(keys)) {
4254
+ const item = resolve(key);
4255
+ if (item !== void 0 && next.delete(item)) changed = true;
4256
+ }
4257
+ if (changed) this.#apply(next);
4258
+ }
3987
4259
  /** Route a click on `item` according to the current interaction mode. */
3988
4260
  handleClick(item, gesture) {
3989
4261
  const { selectionMode, interactionMode: mode } = this.#config.getModes();
@@ -10772,6 +11044,7 @@ let DataGridElement = class DataGridElement extends i {
10772
11044
  this.groupBy = null;
10773
11045
  this.allowGrouping = false;
10774
11046
  this.crossGroupSelection = false;
11047
+ this.columnMenu = `auto`;
10775
11048
  this._columnFormatters = /* @__PURE__ */ new Map();
10776
11049
  this._selectedKeys = /* @__PURE__ */ new Set();
10777
11050
  this._scrollTop = 0;
@@ -10794,10 +11067,16 @@ let DataGridElement = class DataGridElement extends i {
10794
11067
  this._sortedRows = [];
10795
11068
  this._flat = [];
10796
11069
  this._flatKeys = [];
11070
+ this.#prevFlatKeys = [];
11071
+ this.#enteringRowKeys = /* @__PURE__ */ new Set();
11072
+ this.#rowReflowTimer = 0;
11073
+ this.#dataGridRenderedOnce = false;
10797
11074
  this._visibleColumns = [];
10798
11075
  this._colOffsets = [];
10799
11076
  this._colWidthSum = 0;
10800
11077
  this._raf = 0;
11078
+ this.#appendInProgress = false;
11079
+ this.#resortRaf = 0;
10801
11080
  this._drag = null;
10802
11081
  this._onFocusOut = (e) => {
10803
11082
  if (this._suppressBlurCommit) return;
@@ -10992,6 +11271,7 @@ let DataGridElement = class DataGridElement extends i {
10992
11271
  themeFallbacks,
10993
11272
  tickledStyles,
10994
11273
  tickledItemStyles,
11274
+ listItemFadeKeyframes,
10995
11275
  i$3`
10996
11276
  :host {
10997
11277
  --data-grid-row-height: 28px;
@@ -11048,6 +11328,22 @@ let DataGridElement = class DataGridElement extends i {
11048
11328
  .row.item.tickled { background: var(--item-bg-tickled); border: var(--item-border-tickled); color: var(--item-text-tickled); }
11049
11329
  .row.item.selected { background: var(--item-bg-selected); border: var(--item-border-selected); color: var(--item-text-selected); }
11050
11330
  .row.item.selected.tickled { background: var(--item-bg-selected-tickled); }
11331
+
11332
+ /* Insertion / removal animation. Only while [reflowing] — the ~0.3s after
11333
+ the row set changes — do rows slide (their top transitions) to make or
11334
+ close a gap; a freshly inserted row also fades in. Kept scoped so it
11335
+ never fires while scrolling the virtualised list. */
11336
+ :host([reflowing]) .row.item,
11337
+ :host([reflowing]) .group-row {
11338
+ transition: top var(--data-grid-row-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1);
11339
+ }
11340
+ .row.item.entering {
11341
+ animation: ixfx-list-item-in var(--data-grid-row-move-duration, 200ms) ease both;
11342
+ }
11343
+ @media (prefers-reduced-motion: reduce) {
11344
+ :host([reflowing]) .row.item, :host([reflowing]) .group-row { transition: none; }
11345
+ .row.item.entering { animation: none; }
11346
+ }
11051
11347
  .cell {
11052
11348
  flex-shrink: 0; display: flex; align-items: center; padding: 0 var(--space-s);
11053
11349
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-right: var(--border-subtle);
@@ -11077,6 +11373,13 @@ let DataGridElement = class DataGridElement extends i {
11077
11373
  `
11078
11374
  ];
11079
11375
  }
11376
+ /** Row-key snapshot from the previous update, for the insertion / removal animation. */
11377
+ #prevFlatKeys;
11378
+ #enteringRowKeys;
11379
+ #rowReflowTimer;
11380
+ #dataGridRenderedOnce;
11381
+ #appendInProgress;
11382
+ #resortRaf;
11080
11383
  connectedCallback() {
11081
11384
  super.connectedCallback();
11082
11385
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -11088,6 +11391,10 @@ let DataGridElement = class DataGridElement extends i {
11088
11391
  this.removeEventListener(`keydown`, this._onKeyDown);
11089
11392
  this.removeEventListener(`focusout`, this._onFocusOut);
11090
11393
  cancelAnimationFrame(this._raf);
11394
+ if (this.#rowReflowTimer) {
11395
+ clearTimeout(this.#rowReflowTimer);
11396
+ this.#rowReflowTimer = 0;
11397
+ }
11091
11398
  }
11092
11399
  firstUpdated() {
11093
11400
  new ResizeObserver(() => {
@@ -11114,8 +11421,10 @@ let DataGridElement = class DataGridElement extends i {
11114
11421
  willUpdate(changed) {
11115
11422
  super.willUpdate(changed);
11116
11423
  this.setAttribute(`data-input-mode`, this._tickled.inputMode);
11117
- if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`)) this._sortedRows = this._applySort([...this.rows]);
11118
- this._rebuildFlat();
11424
+ if (!this.#appendInProgress) {
11425
+ if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`)) this._sortedRows = this._applySort([...this.rows]);
11426
+ this._rebuildFlat();
11427
+ }
11119
11428
  this._visibleColumns = this.columns.filter((c) => c.visible !== false);
11120
11429
  let off = 0;
11121
11430
  this._colOffsets = this._visibleColumns.map((c) => {
@@ -11125,6 +11434,40 @@ let DataGridElement = class DataGridElement extends i {
11125
11434
  });
11126
11435
  this._colWidthSum = off;
11127
11436
  if (this.interactionMode === `checked`) this._colWidthSum += 28;
11437
+ this.#noteFlatMutation();
11438
+ }
11439
+ /**
11440
+ * Diff the row-key list against the previous update. Rows that appeared get an
11441
+ * enter fade; any add/remove opens a short `[reflowing]` window so the rows
11442
+ * that shift slide (their `top` transitions) instead of snapping. Bounded in
11443
+ * time so it never bleeds into scroll virtualisation.
11444
+ */
11445
+ #noteFlatMutation() {
11446
+ const now = this._flatKeys;
11447
+ const prev = this.#prevFlatKeys;
11448
+ this.#prevFlatKeys = now.slice();
11449
+ if (!this.#dataGridRenderedOnce) {
11450
+ this.#dataGridRenderedOnce = true;
11451
+ return;
11452
+ }
11453
+ if (prev.length === 0 || now.length === 0) {
11454
+ this.#enteringRowKeys.clear();
11455
+ return;
11456
+ }
11457
+ const before = new Set(prev);
11458
+ const after = new Set(now);
11459
+ const added = now.filter((k) => !before.has(k));
11460
+ const removed = prev.filter((k) => !after.has(k));
11461
+ if (added.length === now.length || added.length === 0 && removed.length === 0) return;
11462
+ this.#enteringRowKeys = new Set(added);
11463
+ this.toggleAttribute(`reflowing`, true);
11464
+ if (this.#rowReflowTimer) clearTimeout(this.#rowReflowTimer);
11465
+ this.#rowReflowTimer = window.setTimeout(() => {
11466
+ this.#rowReflowTimer = 0;
11467
+ this.#enteringRowKeys.clear();
11468
+ this.toggleAttribute(`reflowing`, false);
11469
+ this.requestUpdate();
11470
+ }, 320);
11128
11471
  }
11129
11472
  _getGroupKey(row) {
11130
11473
  if (!this.groupBy) return null;
@@ -11372,6 +11715,102 @@ let DataGridElement = class DataGridElement extends i {
11372
11715
  _getRowForKey(key) {
11373
11716
  return this._flat.find((f) => f.key === key)?.row;
11374
11717
  }
11718
+ /** The selected row keys (read-only). */
11719
+ get selectedKeys() {
11720
+ return this._selectedKeys;
11721
+ }
11722
+ /** Replace the selection with these row keys (clamped to known rows). */
11723
+ set selectedKeys(keys) {
11724
+ this.selectKeys(keys);
11725
+ }
11726
+ /**
11727
+ * Replace the selection with the given row keys. Unknown keys are dropped;
11728
+ * in a single-select mode only the last surviving key is kept;
11729
+ * `selectionMode="none"` clears. Fires `datagrid-select` with the previous
11730
+ * set when the selection actually changes.
11731
+ */
11732
+ selectKeys(keys) {
11733
+ const known = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
11734
+ let next = new Set([...keys].filter((k) => known.has(k)));
11735
+ const multi = this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
11736
+ if (this.selectionMode === `none`) next = /* @__PURE__ */ new Set();
11737
+ else if (!multi && next.size > 1) {
11738
+ const last = [...next].at(-1);
11739
+ next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
11740
+ }
11741
+ const prev = new Set(this._selectedKeys);
11742
+ if (prev.size === next.size && [...next].every((k) => prev.has(k))) return;
11743
+ this._selectedKeys = next;
11744
+ this._anchorKey = [...next].at(-1);
11745
+ this.dispatchEvent(new CustomEvent(`datagrid-select`, {
11746
+ detail: {
11747
+ selectedKeys: new Set(next),
11748
+ previous: prev
11749
+ },
11750
+ bubbles: true,
11751
+ composed: true
11752
+ }));
11753
+ }
11754
+ /** The row key under the cursor, or `undefined`. */
11755
+ get tickledKey() {
11756
+ return this._tickled.tickledId;
11757
+ }
11758
+ /** Move the cursor to this row key and scroll it into view. */
11759
+ tickleRow(key) {
11760
+ if (!this._flatKeys.includes(key)) return;
11761
+ this._tickled.focus(key);
11762
+ this._scrollRowIntoView(key);
11763
+ }
11764
+ /**
11765
+ * Append rows without the full O(n log n) rebuild that reassigning `rows`
11766
+ * incurs. `rows` still gets a new array reference (reactivity is preserved),
11767
+ * but the derived arrays are extended over the appended slice only. When a
11768
+ * sort is active the new rows show unsorted for one frame, then a single
11769
+ * coalesced re-sort runs. Scroll position, selection and cursor are kept.
11770
+ */
11771
+ appendRows(rows) {
11772
+ if (rows.length === 0) return;
11773
+ const startIdx = this.rows.length;
11774
+ this.#appendInProgress = true;
11775
+ this.rows = [...this.rows, ...rows];
11776
+ const appended = this.rows.slice(startIdx);
11777
+ if (this._sortedRows.length === startIdx) this._sortedRows = [...this._sortedRows, ...appended];
11778
+ else {
11779
+ this._sortedRows = this._applySort([...this.rows]);
11780
+ this._rebuildFlat();
11781
+ this.#appendInProgress = false;
11782
+ this.requestUpdate();
11783
+ return;
11784
+ }
11785
+ this.#appendToFlat(appended, startIdx);
11786
+ this.#appendInProgress = false;
11787
+ this.requestUpdate();
11788
+ if (this.sortState != null) {
11789
+ cancelAnimationFrame(this.#resortRaf);
11790
+ this.#resortRaf = requestAnimationFrame(() => {
11791
+ this._sortedRows = this._applySort([...this.rows]);
11792
+ this._rebuildFlat();
11793
+ this.requestUpdate();
11794
+ });
11795
+ }
11796
+ }
11797
+ #appendToFlat(appended, startIdx) {
11798
+ if (this.groupBy) {
11799
+ this._rebuildFlat();
11800
+ return;
11801
+ }
11802
+ for (let j = 0; j < appended.length; j++) {
11803
+ const idx = startIdx + j;
11804
+ const key = this.getRowKey(appended[j], idx);
11805
+ this._flat.push({
11806
+ type: `row`,
11807
+ key,
11808
+ row: appended[j],
11809
+ idx
11810
+ });
11811
+ this._flatKeys.push(key);
11812
+ }
11813
+ }
11375
11814
  _replaceSelection(key) {
11376
11815
  if (this.selectionMode === `none`) return;
11377
11816
  const prev = new Set(this._selectedKeys);
@@ -11727,6 +12166,7 @@ let DataGridElement = class DataGridElement extends i {
11727
12166
  return last >= 0 ? this._colOffsets[last] + (this._visibleColumns[last].width ?? 150) : 0;
11728
12167
  })()}px"></div>` : A}
11729
12168
  </div>
12169
+ ${this.columnMenu === `none` ? A : b`
11730
12170
  <ixfx-menu-trigger label="⚙" placement="bottom-end" @menu-command=${(e) => {
11731
12171
  const cmd = e.detail.command;
11732
12172
  const checked = e.detail.checked;
@@ -11766,7 +12206,7 @@ let DataGridElement = class DataGridElement extends i {
11766
12206
  </ixfx-menu-radio-group>
11767
12207
  ` : A}
11768
12208
  </ixfx-menu-container>
11769
- </ixfx-menu-trigger>
12209
+ </ixfx-menu-trigger>`}
11770
12210
  </div>
11771
12211
  <div class="viewport" @scroll=${this._onScroll}>
11772
12212
  <div class="spacer" style="height:${this._flat.length * rh}px; width:${this._colWidthSum}px">
@@ -11783,7 +12223,7 @@ let DataGridElement = class DataGridElement extends i {
11783
12223
  const selected = this._selectedKeys.has(rowKey);
11784
12224
  const tickled = this._tickled.isTickled(rowKey);
11785
12225
  return b`
11786
- <div class="row item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``}" style="top:${absIdx * rh}px; height:${rh}px"
12226
+ <div class="row item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${this.#enteringRowKeys.has(rowKey) ? `entering` : ``}" style="top:${absIdx * rh}px; height:${rh}px"
11787
12227
  data-key=${rowKey}
11788
12228
  @click=${(e) => {
11789
12229
  this._onRowClick(rowKey, null, e);
@@ -11954,6 +12394,10 @@ __decorate([property({
11954
12394
  attribute: `cross-group-selection`,
11955
12395
  reflect: true
11956
12396
  })], DataGridElement.prototype, "crossGroupSelection", void 0);
12397
+ __decorate([property({
12398
+ type: String,
12399
+ attribute: `column-menu`
12400
+ })], DataGridElement.prototype, "columnMenu", void 0);
11957
12401
  __decorate([property({ attribute: false })], DataGridElement.prototype, "globalFormatter", void 0);
11958
12402
  __decorate([state()], DataGridElement.prototype, "_selectedKeys", void 0);
11959
12403
  __decorate([state()], DataGridElement.prototype, "_scrollTop", void 0);
@@ -11970,8 +12414,273 @@ __decorate([query(`.viewport`)], DataGridElement.prototype, "_viewportEl", void
11970
12414
  __decorate([query(`.header-scroll`)], DataGridElement.prototype, "_headerScrollEl", void 0);
11971
12415
  DataGridElement = __decorate([safeCustomElement(`ixfx-data-grid`)], DataGridElement);
11972
12416
  //#endregion
12417
+ //#region src/data-table/data-table.ts
12418
+ let DataTableElement = class DataTableElement extends i {
12419
+ constructor(..._args) {
12420
+ super(..._args);
12421
+ this.rows = [];
12422
+ this.tickleDelay = 400;
12423
+ this.layout = `auto`;
12424
+ this.narrowBreakpoint = 420;
12425
+ this._tickled = new TickledController(this, { getItems: () => this.rows.map((_, i) => ({
12426
+ id: i,
12427
+ enabled: true
12428
+ })) });
12429
+ this._stacked = false;
12430
+ this.#lastWidth = 0;
12431
+ }
12432
+ static {
12433
+ this.styles = [
12434
+ tickledStyles,
12435
+ tickledItemStyles,
12436
+ i$3`
12437
+ :host {
12438
+ display: block;
12439
+ container-type: inline-size;
12440
+ }
12441
+
12442
+ .properties {
12443
+ display: grid;
12444
+ grid-template-columns: var(--data-table-name-width, max-content)
12445
+ var(--data-table-value-width, minmax(0, 1fr));
12446
+ /* No column gap: the tickled highlight reads as one solid block.
12447
+ Separation comes from cell padding, which the highlight covers. */
12448
+ gap: 2px 0;
12449
+ }
12450
+
12451
+ /* Widening: 2 then 3 property-pairs per row on roomy containers.
12452
+ Suppressed when stacked, or when locked to 'columns'. */
12453
+ @container (min-width: 650px) {
12454
+ :host(:not([layout='columns'])) .properties:not(.stacked) {
12455
+ grid-template-columns:
12456
+ var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr))
12457
+ var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr));
12458
+ }
12459
+ }
12460
+
12461
+ @container (min-width: 1000px) {
12462
+ :host(:not([layout='columns'])) .properties:not(.stacked) {
12463
+ grid-template-columns:
12464
+ var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr))
12465
+ var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr))
12466
+ var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr));
12467
+ }
12468
+ }
12469
+
12470
+ /* Stacked (key above value). Threshold is JS-driven via 'narrowBreakpoint'.
12471
+ minmax(0, 1fr) — not 1fr — so a long value's min-content size can't
12472
+ blow the column out past the container; the value then ellipsises. */
12473
+ .properties.stacked {
12474
+ grid-template-columns: minmax(0, 1fr);
12475
+ }
12476
+
12477
+ /* A pair is its own subgrid so the whole row is one element:
12478
+ the tickled highlight can't split, and key/value share a baseline. */
12479
+ .pair {
12480
+ display: grid;
12481
+ grid-column: span 2;
12482
+ grid-template-columns: subgrid;
12483
+ align-items: var(--data-table-cell-align, baseline);
12484
+ cursor: pointer;
12485
+ }
12486
+
12487
+ .pair[data-full] {
12488
+ grid-column: 1 / -1;
12489
+ }
12490
+
12491
+ .pair[data-full] > .value {
12492
+ grid-column: 2 / -1;
12493
+ }
12494
+
12495
+ .properties.stacked .pair {
12496
+ display: block;
12497
+ grid-column: 1 / -1;
12498
+ /* Let the block shrink below its content so .value-text can clip. */
12499
+ min-width: 0;
12500
+ }
12501
+
12502
+ .properties.stacked .pair:not(:last-child) {
12503
+ margin-bottom: var(--space-s);
12504
+ }
12505
+
12506
+ /* Stacked keys get their own line, so a long label must wrap rather than
12507
+ run off the edge (nowrap is only wanted for the max-content column). */
12508
+ .properties.stacked .key {
12509
+ white-space: normal;
12510
+ overflow-wrap: break-word;
12511
+ }
12512
+
12513
+ .key,
12514
+ .value {
12515
+ min-width: 0;
12516
+ /* Row rhythm comes from this padding (tunable), not line-height. */
12517
+ padding: var(--data-table-row-padding, 0.35em) 0;
12518
+ }
12519
+
12520
+ .key {
12521
+ padding-right: var(--space-m);
12522
+ opacity: 0.6;
12523
+ white-space: nowrap;
12524
+ text-align: var(--data-table-name-align, left);
12525
+ font-size: 10px;
12526
+ text-transform: uppercase;
12527
+ letter-spacing: 0.05em;
12528
+ /* Uppercase keys have no descenders, so both edges can be trimmed to
12529
+ the glyphs for tight baseline / centre alignment. */
12530
+ text-box: trim-both cap alphabetic;
12531
+ }
12532
+
12533
+ .value {
12534
+ position: relative;
12535
+ padding-right: var(--space-l);
12536
+ font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
12537
+ }
12538
+
12539
+ /* The value text is clipped here — never on .value itself — so a long
12540
+ value stays within its cell even while tickled, and the detail
12541
+ popover (a child of .value) can still escape the cell. */
12542
+ .value-text {
12543
+ display: block;
12544
+ overflow: hidden;
12545
+ text-overflow: ellipsis;
12546
+ white-space: nowrap;
12547
+ /* No text-box trim here: the natural line box gives descenders the room
12548
+ overflow:hidden would otherwise clip. Baseline alignment of key and
12549
+ value comes from align-items on .pair, which needs no trimming. */
12550
+ }
12551
+
12552
+ .pair.tickled {
12553
+ background: var(--item-bg-tickled, color-mix(in oklch, var(--surface-2), var(--accent) 10%));
12554
+ }
12555
+
12556
+ /* Lift the row whose detail is open above the rows it slides over. */
12557
+ .pair.show-detail {
12558
+ position: relative;
12559
+ z-index: 1;
12560
+ }
12561
+
12562
+ .detail {
12563
+ position: absolute;
12564
+ z-index: 2;
12565
+ /* Flush with the bottom of the value (no gap) so the pointer can travel
12566
+ from value to panel without crossing a dead zone that would dismiss it.
12567
+ Left-aligned with the value; only as wide as its content, capped at
12568
+ the table width. */
12569
+ top: 100%;
12570
+ left: 0;
12571
+ right: auto;
12572
+ width: max-content;
12573
+ max-width: 100cqi;
12574
+ padding: var(--space-xs);
12575
+ background: var(--surface-2);
12576
+ visibility: hidden;
12577
+ transform: translateY(-4px);
12578
+ pointer-events: none;
12579
+ transition: transform var(--transition);
12580
+ }
12581
+
12582
+ .pair.show-detail .detail {
12583
+ visibility: visible;
12584
+ transform: translateY(0);
12585
+ pointer-events: auto;
12586
+ }
12587
+ `
12588
+ ];
12589
+ }
12590
+ #detailTimer;
12591
+ #resizeObserver;
12592
+ #lastWidth;
12593
+ connectedCallback() {
12594
+ super.connectedCallback();
12595
+ this.#resizeObserver = new ResizeObserver((entries) => {
12596
+ const entry = entries[0];
12597
+ if (!entry) return;
12598
+ this.#lastWidth = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
12599
+ this.#updateStacked();
12600
+ });
12601
+ this.#resizeObserver.observe(this);
12602
+ }
12603
+ disconnectedCallback() {
12604
+ super.disconnectedCallback();
12605
+ clearTimeout(this.#detailTimer);
12606
+ this.#resizeObserver?.disconnect();
12607
+ this.#resizeObserver = void 0;
12608
+ }
12609
+ willUpdate(changed) {
12610
+ if (changed.has(`layout`) || changed.has(`narrowBreakpoint`)) this.#updateStacked();
12611
+ }
12612
+ #updateStacked() {
12613
+ const stacked = this.layout === `rows` ? true : this.layout === `columns` ? false : this.#lastWidth > 0 && this.#lastWidth <= this.narrowBreakpoint;
12614
+ if (stacked !== this._stacked) this._stacked = stacked;
12615
+ }
12616
+ #handlePointerEnter(i) {
12617
+ this._tickled.handlePointerEnter(i);
12618
+ clearTimeout(this.#detailTimer);
12619
+ if (this._detailIndex !== void 0) this._detailIndex = void 0;
12620
+ this.#detailTimer = setTimeout(() => {
12621
+ if (this._tickled.isTickled(i)) this._detailIndex = i;
12622
+ }, this.tickleDelay);
12623
+ }
12624
+ #handlePointerLeave(e) {
12625
+ clearTimeout(this.#detailTimer);
12626
+ this._detailIndex = void 0;
12627
+ this._tickled.handlePointerLeave(e);
12628
+ }
12629
+ #select(i) {
12630
+ clearTimeout(this.#detailTimer);
12631
+ this.selectedIndex = this.selectedIndex === i ? void 0 : i;
12632
+ this._tickled.setTickled(i);
12633
+ this._detailIndex = i;
12634
+ this.dispatchEvent(new CustomEvent(`data-table-row-click`, {
12635
+ bubbles: true,
12636
+ composed: true,
12637
+ detail: {
12638
+ row: this.rows[i],
12639
+ index: i,
12640
+ selected: this.selectedIndex === i
12641
+ }
12642
+ }));
12643
+ }
12644
+ render() {
12645
+ return b`<div
12646
+ class="properties ${this._stacked ? `stacked` : ``}"
12647
+ @pointerleave=${(e) => this.#handlePointerLeave(e)}
12648
+ >
12649
+ ${this.rows.map((r, i) => b`<div
12650
+ class="pair ${this._tickled.isTickled(i) ? `tickled` : ``} ${this._detailIndex === i ? `show-detail` : ``}"
12651
+ ?data-full=${r.fullWidth}
12652
+ @pointerenter=${() => this.#handlePointerEnter(i)}
12653
+ @click=${() => this.#select(i)}
12654
+ ><div class="key">${r.label}</div><div class="value"><span class="value-text">${typeof r.value === `boolean` ? r.value ? `Yes` : `No` : r.value}</span>${r.detail === void 0 ? A : b`<div class="detail" part="detail">${r.detail}</div>`}</div></div>`)}</div>`;
12655
+ }
12656
+ };
12657
+ __decorate([property({
12658
+ type: Array,
12659
+ attribute: false
12660
+ })], DataTableElement.prototype, "rows", void 0);
12661
+ __decorate([property({
12662
+ type: Number,
12663
+ attribute: false
12664
+ })], DataTableElement.prototype, "selectedIndex", void 0);
12665
+ __decorate([property({
12666
+ type: Number,
12667
+ attribute: `tickle-delay`
12668
+ })], DataTableElement.prototype, "tickleDelay", void 0);
12669
+ __decorate([property({
12670
+ type: String,
12671
+ reflect: true
12672
+ })], DataTableElement.prototype, "layout", void 0);
12673
+ __decorate([property({
12674
+ type: Number,
12675
+ attribute: `narrow-breakpoint`
12676
+ })], DataTableElement.prototype, "narrowBreakpoint", void 0);
12677
+ __decorate([state()], DataTableElement.prototype, "_tickled", void 0);
12678
+ __decorate([state()], DataTableElement.prototype, "_detailIndex", void 0);
12679
+ __decorate([state()], DataTableElement.prototype, "_stacked", void 0);
12680
+ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableElement);
12681
+ //#endregion
11973
12682
  //#region src/detail-list/detail-list.ts
11974
- const ITEM_SELECTOR$1 = `:scope > li`;
12683
+ const ITEM_SELECTOR$2 = `:scope > li`;
11975
12684
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
11976
12685
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
11977
12686
  let DetailListElement = class DetailListElement extends i {
@@ -11992,6 +12701,7 @@ let DetailListElement = class DetailListElement extends i {
11992
12701
  this.#grid = [];
11993
12702
  this.#positions = /* @__PURE__ */ new Map();
11994
12703
  this.#geometryScheduled = false;
12704
+ this.#animatedItems = /* @__PURE__ */ new WeakSet();
11995
12705
  this.#builtFromProvider = [];
11996
12706
  this.#providerBuilt = 0;
11997
12707
  this.#data = new DataController(this);
@@ -12002,11 +12712,13 @@ let DetailListElement = class DetailListElement extends i {
12002
12712
  selectionMode: this.selectionMode,
12003
12713
  interactionMode: this.interactionMode
12004
12714
  }),
12715
+ getKey: keyForElement,
12005
12716
  onChange: (selected, previous) => {
12006
12717
  this._syncItemStates();
12007
12718
  this._dispatch(`list-select`, {
12008
12719
  selected,
12009
- previous
12720
+ previous,
12721
+ selectedKeys: this.selection.selectedKeys
12010
12722
  });
12011
12723
  }
12012
12724
  });
@@ -12158,6 +12870,8 @@ let DetailListElement = class DetailListElement extends i {
12158
12870
  #geometryScheduled;
12159
12871
  #resizeObserver;
12160
12872
  #mutationObserver;
12873
+ /** Items that have already played (or been skipped for) their enter animation. */
12874
+ #animatedItems;
12161
12875
  #builtFromProvider;
12162
12876
  #providerBuilt;
12163
12877
  #data;
@@ -12172,8 +12886,9 @@ let DetailListElement = class DetailListElement extends i {
12172
12886
  this.addEventListener(`wheel`, this._onWheel);
12173
12887
  this.#resizeObserver = new ResizeObserver(() => this.#scheduleGeometry());
12174
12888
  this.#resizeObserver.observe(this);
12175
- this.#mutationObserver = new MutationObserver(() => {
12889
+ this.#mutationObserver = new MutationObserver((records) => {
12176
12890
  this.#processChildren();
12891
+ this.#animateChildMutations(records);
12177
12892
  this.#scheduleGeometry();
12178
12893
  });
12179
12894
  this.#mutationObserver.observe(this, {
@@ -12181,6 +12896,27 @@ let DetailListElement = class DetailListElement extends i {
12181
12896
  subtree: false,
12182
12897
  attributeFilter: [`hidden`]
12183
12898
  });
12899
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#animatedItems.add(el);
12900
+ }
12901
+ /**
12902
+ * Collapse-in items appended to the light DOM (however they got there). A bulk
12903
+ * populate is left alone. Geometry is re-read once the animations settle so
12904
+ * keyboard navigation lines back up with the final layout.
12905
+ */
12906
+ #animateChildMutations(records) {
12907
+ if (!this.isConnected) return;
12908
+ const fresh = [];
12909
+ for (const record of records) for (const node of record.addedNodes) {
12910
+ if (!(node instanceof HTMLElement) || node.parentElement !== this || node.localName !== `li`) continue;
12911
+ if (this.#animatedItems.has(node)) continue;
12912
+ this.#animatedItems.add(node);
12913
+ fresh.push(node);
12914
+ }
12915
+ if (fresh.length === 0) return;
12916
+ const total = this.querySelectorAll(ITEM_SELECTOR$2).length;
12917
+ if (fresh.length >= total) return;
12918
+ for (const el of fresh) animateListItemIn(el);
12919
+ window.setTimeout(() => this.#scheduleGeometry(), 230);
12184
12920
  }
12185
12921
  disconnectedCallback() {
12186
12922
  super.disconnectedCallback();
@@ -12202,7 +12938,7 @@ let DetailListElement = class DetailListElement extends i {
12202
12938
  if (this.filterPredicate) this.#applyFilterPredicate();
12203
12939
  this.#search = createElementIncrSearch({
12204
12940
  container: this,
12205
- itemSelector: ITEM_SELECTOR$1,
12941
+ itemSelector: ITEM_SELECTOR$2,
12206
12942
  getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
12207
12943
  getKey: (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
12208
12944
  highlightKey: SEARCH_HIGHLIGHT_KEY
@@ -12261,8 +12997,21 @@ let DetailListElement = class DetailListElement extends i {
12261
12997
  if (this.sort !== `none`) this.#applySort();
12262
12998
  if (this.interactionMode === `checked`) this._syncCheckboxes();
12263
12999
  }
13000
+ /**
13001
+ * Collapse + fade `item` out, then detach it; the rows after it slide up to
13002
+ * close the gap. Resolves once removed. Immediate under reduced motion.
13003
+ */
12264
13004
  removeItem(item) {
12265
- item.remove();
13005
+ this.selection.deselect(item);
13006
+ return animateListItemOut(item).then(() => {
13007
+ item.remove();
13008
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
13009
+ this.#recomputeGeometry();
13010
+ });
13011
+ }
13012
+ /** {@link removeItem} for several items at once. */
13013
+ removeItems(items) {
13014
+ return Promise.all([...items].map((item) => this.removeItem(item))).then(() => void 0);
12266
13015
  }
12267
13016
  clearItems() {
12268
13017
  this.replaceChildren();
@@ -12308,6 +13057,40 @@ let DetailListElement = class DetailListElement extends i {
12308
13057
  selectAll() {
12309
13058
  if (this.selection.canSelectAll()) this.selection.selectAll();
12310
13059
  }
13060
+ /** The selection as stable `data-key` / `data-value` strings. */
13061
+ get selectedKeys() {
13062
+ return this.selection.selectedKeys;
13063
+ }
13064
+ /** The key of the item under the cursor, or `undefined`. */
13065
+ get tickledKey() {
13066
+ const id = this.tickled.tickledId;
13067
+ return id ? keyForElement(id) : void 0;
13068
+ }
13069
+ /** Replace the selection with the items carrying these keys (unknown keys ignored). */
13070
+ selectKeys(keys) {
13071
+ this.selection.selectKeys(keys, (key) => this.#itemForKey(key));
13072
+ }
13073
+ /** Add items by key to the selection (multi-select modes only). */
13074
+ addSelectedKeys(keys) {
13075
+ this.selection.addKeys(keys, (key) => this.#itemForKey(key));
13076
+ }
13077
+ /** Remove items by key from the selection. */
13078
+ removeSelectedKeys(keys) {
13079
+ this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
13080
+ }
13081
+ /** Move the cursor to the item with this key and scroll it into view. */
13082
+ focusKey(key) {
13083
+ const el = this.#itemForKey(key);
13084
+ if (!el) return;
13085
+ this.tickled.focus(el);
13086
+ el.scrollIntoView({
13087
+ block: `nearest`,
13088
+ inline: `nearest`
13089
+ });
13090
+ }
13091
+ #itemForKey(key) {
13092
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) if (keyForElement(el) === key) return el;
13093
+ }
12311
13094
  *selectedFilter(kind) {
12312
13095
  const wanted = (el) => {
12313
13096
  switch (kind) {
@@ -12316,7 +13099,7 @@ let DetailListElement = class DetailListElement extends i {
12316
13099
  case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);
12317
13100
  }
12318
13101
  };
12319
- for (const el of this.querySelectorAll(ITEM_SELECTOR$1)) {
13102
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
12320
13103
  if (el.hidden) continue;
12321
13104
  if (wanted(el)) yield el;
12322
13105
  }
@@ -12329,7 +13112,7 @@ let DetailListElement = class DetailListElement extends i {
12329
13112
  if (previousKeys.size > 0 || anchorKey !== null) {
12330
13113
  const next = [];
12331
13114
  let anchor;
12332
- for (const el of this.querySelectorAll(ITEM_SELECTOR$1)) {
13115
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
12333
13116
  const key = keyOf(el);
12334
13117
  if (key !== null && previousKeys.has(key)) next.push(el);
12335
13118
  if (key !== null && key === anchorKey) anchor = el;
@@ -12384,15 +13167,15 @@ let DetailListElement = class DetailListElement extends i {
12384
13167
  }
12385
13168
  #applyFilterPredicate() {
12386
13169
  if (!this.filterPredicate) {
12387
- for (const el of this.querySelectorAll(ITEM_SELECTOR$1)) el.removeAttribute(`hidden`);
13170
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) el.removeAttribute(`hidden`);
12388
13171
  return;
12389
13172
  }
12390
- for (const el of this.querySelectorAll(ITEM_SELECTOR$1)) if (this.filterPredicate(el)) el.removeAttribute(`hidden`);
13173
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) if (this.filterPredicate(el)) el.removeAttribute(`hidden`);
12391
13174
  else el.setAttribute(`hidden`, ``);
12392
13175
  }
12393
13176
  #applySort() {
12394
13177
  if (this.sort === `none`) return;
12395
- const rows = [...this.querySelectorAll(ITEM_SELECTOR$1)];
13178
+ const rows = [...this.querySelectorAll(ITEM_SELECTOR$2)];
12396
13179
  if (rows.length < 2) return;
12397
13180
  const dir = this.sort === `descending` ? -1 : 1;
12398
13181
  const meta = (el) => ({
@@ -12584,7 +13367,7 @@ let DetailListElement = class DetailListElement extends i {
12584
13367
  else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
12585
13368
  }
12586
13369
  _getVisibleItems() {
12587
- return Array.from(this.querySelectorAll(ITEM_SELECTOR$1)).filter((el) => {
13370
+ return Array.from(this.querySelectorAll(ITEM_SELECTOR$2)).filter((el) => {
12588
13371
  if (el.hidden) return false;
12589
13372
  if (this.filterPredicate && !this.filterPredicate(el)) return false;
12590
13373
  return true;
@@ -12639,10 +13422,10 @@ let DetailListElement = class DetailListElement extends i {
12639
13422
  }
12640
13423
  /** Scan light-DOM children for unprocessed `data-icon` items. */
12641
13424
  #processChildren() {
12642
- for (const el of this.querySelectorAll(ITEM_SELECTOR$1)) this.#ensureIcon(el);
13425
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#ensureIcon(el);
12643
13426
  }
12644
13427
  _syncItemStates() {
12645
- for (const el of this.querySelectorAll(ITEM_SELECTOR$1)) {
13428
+ for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
12646
13429
  el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
12647
13430
  el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
12648
13431
  if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !el.hasAttribute(`not-checked`) && this.selection.selected.has(el));
@@ -18701,6 +19484,924 @@ let FormElement = class FormElement extends i {
18701
19484
  };
18702
19485
  FormElement = __decorate([safeCustomElement(`ixfx-form`)], FormElement);
18703
19486
  //#endregion
19487
+ //#region src/grid-list/grid-list.ts
19488
+ const ITEM_SELECTOR$1 = `:scope > *`;
19489
+ const MIN_ITEM_SIZE = 16;
19490
+ /**
19491
+ * How long the host keeps the `[reflowing]` attribute after items are inserted
19492
+ * mid-list — long enough for the cell move-transition and the enter animation
19493
+ * to finish. Kept a touch above the default `--grid-list-item-move-duration`.
19494
+ */
19495
+ const REFLOW_WINDOW_MS = 320;
19496
+ function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
19497
+ const w = Math.max(1, itemWidth);
19498
+ const h = Math.max(1, rowHeight);
19499
+ const g = Math.max(0, gap);
19500
+ const colStride = w + g;
19501
+ const rowStride = h + g;
19502
+ const offset = Math.max(0, scrollOffset);
19503
+ if (orientation === `row`) {
19504
+ const rows = count === 0 ? 0 : 1;
19505
+ const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
19506
+ if (count === 0 || viewportWidth <= 0) return {
19507
+ cols: count,
19508
+ rows,
19509
+ totalHeight: 0,
19510
+ totalWidth,
19511
+ startIndex: 0,
19512
+ endIndex: 0,
19513
+ colStride,
19514
+ rowStride,
19515
+ horizontal: true
19516
+ };
19517
+ const firstVisible = Math.max(0, Math.floor(offset / colStride));
19518
+ const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
19519
+ return {
19520
+ cols: count,
19521
+ rows,
19522
+ totalHeight: 0,
19523
+ totalWidth,
19524
+ startIndex: Math.max(0, firstVisible - overscan),
19525
+ endIndex: Math.min(count, lastVisible + overscan),
19526
+ colStride,
19527
+ rowStride,
19528
+ horizontal: true
19529
+ };
19530
+ }
19531
+ const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
19532
+ const rows = count === 0 ? 0 : Math.ceil(count / cols);
19533
+ const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
19534
+ if (count === 0 || viewportHeight <= 0) return {
19535
+ cols,
19536
+ rows,
19537
+ totalHeight,
19538
+ totalWidth: 0,
19539
+ startIndex: 0,
19540
+ endIndex: 0,
19541
+ colStride,
19542
+ rowStride,
19543
+ horizontal: false
19544
+ };
19545
+ const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
19546
+ const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
19547
+ const rowStart = Math.max(0, firstVisibleRow - overscan);
19548
+ const rowEnd = Math.min(rows, lastVisibleRow + overscan);
19549
+ return {
19550
+ cols,
19551
+ rows,
19552
+ totalHeight,
19553
+ totalWidth: 0,
19554
+ startIndex: rowStart * cols,
19555
+ endIndex: Math.min(count, rowEnd * cols),
19556
+ colStride,
19557
+ rowStride,
19558
+ horizontal: false
19559
+ };
19560
+ }
19561
+ function columnOf(index, cols) {
19562
+ return cols <= 1 ? 0 : index % cols;
19563
+ }
19564
+ function rowOf(index, cols) {
19565
+ return cols <= 1 ? index : Math.floor(index / cols);
19566
+ }
19567
+ let GridListElement = class GridListElement extends i {
19568
+ constructor(..._args) {
19569
+ super(..._args);
19570
+ this.selectionMode = `single`;
19571
+ this.interactionMode = `standard`;
19572
+ this.itemSize = 160;
19573
+ this.gap = 8;
19574
+ this.orientation = `grid`;
19575
+ this.captionStyle = `under`;
19576
+ this.captionAlign = `middle`;
19577
+ this.captionPosition = `bottom`;
19578
+ this.captionBackdrop = `scrim`;
19579
+ this.captionHeight = 24;
19580
+ this.itemWrap = `stop`;
19581
+ this._scrollTop = 0;
19582
+ this._scrollLeft = 0;
19583
+ this._viewportHeight = 0;
19584
+ this._viewportWidth = 0;
19585
+ this.#viewportRef = createRef();
19586
+ this.#scrollRaf = 0;
19587
+ this.#items = [];
19588
+ this.#itemWrap = `stop`;
19589
+ this.#entering = /* @__PURE__ */ new Set();
19590
+ this.#reflowTimer = 0;
19591
+ this.#slotNames = /* @__PURE__ */ new WeakMap();
19592
+ this.#slotSeq = 0;
19593
+ this.selection = new ListSelectionController({
19594
+ getOrderedItems: () => this._getVisibleItems(),
19595
+ isSelectable: () => true,
19596
+ getModes: () => ({
19597
+ selectionMode: this.selectionMode,
19598
+ interactionMode: this.interactionMode
19599
+ }),
19600
+ getKey: keyForElement,
19601
+ onChange: (selected, previous) => {
19602
+ this._syncItemStates();
19603
+ this.requestUpdate();
19604
+ this._dispatch(`list-select`, {
19605
+ selected,
19606
+ previous,
19607
+ selectedKeys: this.selection.selectedKeys
19608
+ });
19609
+ }
19610
+ });
19611
+ this.tickled = new TickledController(this, {
19612
+ getItems: () => this.#items.map((el) => ({
19613
+ id: el,
19614
+ enabled: !el.hasAttribute(`not-checked`)
19615
+ })),
19616
+ onTickle: (id) => {
19617
+ if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
19618
+ },
19619
+ onStateChange: () => this._syncItemStates(),
19620
+ itemWrap: this.#itemWrap
19621
+ });
19622
+ this.#onScroll = (e) => {
19623
+ const el = e.target;
19624
+ this.#cancelScrollRaf();
19625
+ this.#scrollRaf = requestAnimationFrame(() => {
19626
+ this.#scrollRaf = 0;
19627
+ this._scrollTop = el.scrollTop;
19628
+ this._scrollLeft = el.scrollLeft;
19629
+ this.#emitViewportChange();
19630
+ });
19631
+ };
19632
+ this._onKeyDown = (e) => {
19633
+ if ((e.ctrlKey || e.metaKey) && e.key === `f`) return;
19634
+ if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
19635
+ if (this.selection.canSelectAll()) {
19636
+ e.preventDefault();
19637
+ this.selection.selectAll();
19638
+ }
19639
+ return;
19640
+ }
19641
+ const count = this.#items.length;
19642
+ if (count === 0) return;
19643
+ const win = this.#currentWindow();
19644
+ const move = (delta) => {
19645
+ const from = this.tickled.tickledId ? this.#items.indexOf(this.tickled.tickledId) : -1;
19646
+ const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
19647
+ const next = this.#items[nextIndex];
19648
+ if (next) this.tickled.handleKeyboardNavigation(next);
19649
+ };
19650
+ const perLine = this.orientation === `grid` ? win.cols : 1;
19651
+ switch (e.key) {
19652
+ case `ArrowLeft`:
19653
+ move(-1);
19654
+ break;
19655
+ case `ArrowRight`:
19656
+ move(1);
19657
+ break;
19658
+ case `ArrowUp`:
19659
+ move(-perLine);
19660
+ break;
19661
+ case `ArrowDown`:
19662
+ move(perLine);
19663
+ break;
19664
+ case `Home`:
19665
+ this.tickled.handleKeyboardNavigation(`first`);
19666
+ break;
19667
+ case `End`:
19668
+ this.tickled.handleKeyboardNavigation(`last`);
19669
+ break;
19670
+ case `PageUp`:
19671
+ case `PageDown`: {
19672
+ const mainExtent = win.horizontal ? this._viewportWidth : this._viewportHeight;
19673
+ const mainStride = win.horizontal ? win.colStride : win.rowStride;
19674
+ const visibleLines = Math.max(1, Math.floor(mainExtent / mainStride));
19675
+ move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
19676
+ break;
19677
+ }
19678
+ case `Enter`: {
19679
+ const id = this.tickled.tickledId;
19680
+ if (id) {
19681
+ e.preventDefault();
19682
+ this._dispatch(`list-item-click`, { item: id });
19683
+ this.#onCellActivate(id);
19684
+ }
19685
+ break;
19686
+ }
19687
+ case ` `: {
19688
+ const id = this.tickled.tickledId;
19689
+ if (id) {
19690
+ e.preventDefault();
19691
+ this.selection.handleSpace(id);
19692
+ }
19693
+ break;
19694
+ }
19695
+ case `Escape`:
19696
+ this.selection.clear();
19697
+ break;
19698
+ default: return;
19699
+ }
19700
+ e.preventDefault();
19701
+ };
19702
+ }
19703
+ static {
19704
+ this.styles = [
19705
+ themeFallbacks,
19706
+ tickledStyles,
19707
+ tickledItemStyles,
19708
+ scrollFadeStyles(`x`),
19709
+ scrollFadeStyles(`y`),
19710
+ listItemFadeKeyframes,
19711
+ i$3`
19712
+ :host {
19713
+ --grid-list-item-bg: var(--item-bg);
19714
+ --grid-list-radius: var(--radius-m);
19715
+ --grid-list-gap: var(--space-xs);
19716
+ display: block;
19717
+ height: 100%;
19718
+ min-height: 2em;
19719
+ overflow: hidden;
19720
+ user-select: none;
19721
+ background: var(--surface-3);
19722
+ color: var(--surface-3-text);
19723
+ border-radius: var(--radius-s);
19724
+ border: 1px solid var(--border);
19725
+ }
19726
+ :host(:focus-visible) {
19727
+ outline: 2px solid var(--accent);
19728
+ outline-offset: -2px;
19729
+ }
19730
+ .viewport {
19731
+ position: relative;
19732
+ height: 100%;
19733
+ overflow-y: auto;
19734
+ overflow-x: hidden;
19735
+ }
19736
+ :host([orientation="row"]) .viewport {
19737
+ overflow-x: auto;
19738
+ overflow-y: hidden;
19739
+ }
19740
+ .spacer {
19741
+ position: relative;
19742
+ width: 100%;
19743
+ }
19744
+ .cell {
19745
+ position: absolute;
19746
+ inset: 0 auto auto 0;
19747
+ box-sizing: border-box;
19748
+ display: flex;
19749
+ flex-direction: column;
19750
+ align-items: stretch;
19751
+ contain: layout style;
19752
+ border-radius: var(--grid-list-radius);
19753
+ }
19754
+
19755
+ /* The square that holds the slotted content. */
19756
+ .thumb {
19757
+ position: relative;
19758
+ flex: 1;
19759
+ min-height: 0;
19760
+ display: flex;
19761
+ align-items: stretch;
19762
+ box-sizing: border-box;
19763
+ overflow: hidden;
19764
+ border-radius: 0;
19765
+ background: transparent;
19766
+ border: 1px solid transparent;
19767
+ transition: background var(--transition), border-color var(--transition);
19768
+ }
19769
+ .thumb ::slotted(*) {
19770
+ flex: 1;
19771
+ min-width: 0;
19772
+ min-height: 0;
19773
+ width: 100%;
19774
+ display: flex;
19775
+ align-items: center;
19776
+ justify-content: center;
19777
+ overflow: hidden;
19778
+ }
19779
+ .thumb ::slotted(*:empty)::after {
19780
+ content: "Empty";
19781
+ color: var(--surface-muted-text);
19782
+ font-size: var(--text-s);
19783
+ }
19784
+ ::slotted([hidden]) {
19785
+ display: none !important;
19786
+ }
19787
+
19788
+ /* Selection / cursor wash — sits over the slotted content so it reads
19789
+ on photos and arbitrary DOM alike. */
19790
+ .thumb::after {
19791
+ content: "";
19792
+ position: absolute;
19793
+ inset: 0;
19794
+ pointer-events: none;
19795
+ border-radius: inherit;
19796
+ background: var(--accent);
19797
+ opacity: 0;
19798
+ transition: opacity var(--transition, 0.1s ease-out);
19799
+ }
19800
+ .cell.selected .thumb::after {
19801
+ opacity: 0.18;
19802
+ }
19803
+
19804
+ /* ---- Caption (base) ---- */
19805
+ .caption {
19806
+ box-sizing: border-box;
19807
+ flex: none;
19808
+ padding: 3px 6px;
19809
+ font-size: var(--text-s, 0.8rem);
19810
+ line-height: 1.3;
19811
+ text-align: center;
19812
+ white-space: nowrap;
19813
+ overflow: hidden;
19814
+ text-overflow: ellipsis;
19815
+ color: var(--surface-3-text);
19816
+ }
19817
+ :host([caption-align="left"]) .caption { text-align: left; }
19818
+ :host([caption-align="right"]) .caption { text-align: right; }
19819
+
19820
+ /* ---- Box modes (hidden / under / overlaid): the cell is one box with a
19821
+ single selection outline ---- */
19822
+ :host(:not([caption-style="separate"])) .cell {
19823
+ background: var(--grid-list-item-bg);
19824
+ border: 1px solid transparent;
19825
+ overflow: hidden;
19826
+ transition: background var(--transition), border-color var(--transition);
19827
+ }
19828
+ :host(:not([caption-style="separate"])) .cell:hover {
19829
+ background: var(--item-bg-tickled);
19830
+ border-color: var(--item-border-tickled);
19831
+ }
19832
+ :host(:not([caption-style="separate"])) .cell.selected {
19833
+ outline: 2px solid var(--accent);
19834
+ outline-offset: -2px;
19835
+ }
19836
+ :host(:not([caption-style="separate"])) .cell.tickled {
19837
+ box-shadow: inset 0 0 0 2px var(--accent);
19838
+ }
19839
+
19840
+ /* under: caption is a strip inside the box, below the content */
19841
+ :host([caption-style="under"]) .caption {
19842
+ border-top: 1px solid var(--border, rgba(128, 128, 128, 0.3));
19843
+ background: var(--surface-4, transparent);
19844
+ }
19845
+
19846
+ /* overlaid: caption floats over the content */
19847
+ :host([caption-style="overlaid"]) .caption {
19848
+ position: absolute;
19849
+ left: 0;
19850
+ right: 0;
19851
+ bottom: 0;
19852
+ color: #fff;
19853
+ }
19854
+ :host([caption-style="overlaid"][caption-position="top"]) .caption {
19855
+ top: 0;
19856
+ bottom: auto;
19857
+ }
19858
+ :host([caption-style="overlaid"][caption-position="middle"]) .caption {
19859
+ top: 50%;
19860
+ bottom: auto;
19861
+ transform: translateY(-50%);
19862
+ }
19863
+ /* overlaid backdrop */
19864
+ :host([caption-style="overlaid"][caption-backdrop="scrim"]) .caption {
19865
+ background: rgba(0, 0, 0, 0.55);
19866
+ }
19867
+ :host([caption-style="overlaid"][caption-backdrop="gradient"]) .caption {
19868
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
19869
+ padding-block: 10px 4px;
19870
+ }
19871
+ :host([caption-style="overlaid"][caption-backdrop="gradient"][caption-position="top"]) .caption {
19872
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
19873
+ padding-block: 4px 10px;
19874
+ }
19875
+ :host([caption-style="overlaid"][caption-backdrop="gradient"][caption-position="middle"]) .caption {
19876
+ background: rgba(0, 0, 0, 0.5);
19877
+ padding-block: 3px;
19878
+ }
19879
+ :host([caption-style="overlaid"][caption-backdrop="blur"]) .caption {
19880
+ background: rgba(20, 20, 20, 0.3);
19881
+ backdrop-filter: blur(6px);
19882
+ -webkit-backdrop-filter: blur(6px);
19883
+ }
19884
+ :host([caption-style="overlaid"][caption-backdrop="none"]) .caption {
19885
+ background: none;
19886
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95), 0 0 6px rgba(0, 0, 0, 0.7);
19887
+ }
19888
+
19889
+ /* ---- separate mode: square + detached caption, each highlighted ---- */
19890
+ :host([caption-style="separate"]) .thumb {
19891
+ flex: none;
19892
+ border-radius: var(--grid-list-radius);
19893
+ background: var(--grid-list-item-bg);
19894
+ border: 1px solid transparent;
19895
+ }
19896
+ :host([caption-style="separate"]) .cell:hover .thumb {
19897
+ background: var(--item-bg-tickled);
19898
+ border-color: var(--item-border-tickled);
19899
+ }
19900
+ :host([caption-style="separate"]) .caption {
19901
+ display: flex;
19902
+ align-items: center;
19903
+ align-self: center;
19904
+ max-width: 100%;
19905
+ margin-top: 4px;
19906
+ border-radius: var(--radius-s, 4px);
19907
+ }
19908
+ :host([caption-style="separate"][caption-align="left"]) .caption { align-self: flex-start; }
19909
+ :host([caption-style="separate"][caption-align="right"]) .caption { align-self: flex-end; }
19910
+ :host([caption-style="separate"]) .cell.selected .thumb,
19911
+ :host([caption-style="separate"]) .cell.tickled .thumb {
19912
+ outline: 2px solid var(--accent);
19913
+ outline-offset: 2px;
19914
+ }
19915
+ :host([caption-style="separate"]) .cell.tickled .thumb {
19916
+ outline-style: dashed;
19917
+ }
19918
+ :host([caption-style="separate"]) .cell.selected .caption {
19919
+ background: var(--accent);
19920
+ color: var(--accent-text, #000);
19921
+ }
19922
+ :host([caption-style="separate"]) .cell.tickled .caption {
19923
+ outline: 1px dashed var(--accent);
19924
+ }
19925
+
19926
+ /* ---- Insertion / removal animation ----
19927
+ Active only while the host carries [reflowing] — the ~0.3s window after
19928
+ the child list changes mid-list (see #noteChildMutation). Kept out of
19929
+ the base rules so it never fires during scroll virtualisation, where
19930
+ cells are recycled to wildly different positions every frame. Placed
19931
+ last so it wins over the earlier cell transition rule regardless of
19932
+ specificity. Survivors slide to their new slot; a freshly inserted cell
19933
+ also fades in. (A removed item's element is already gone, so there's
19934
+ nothing left to fade — the gap simply closes as the others slide.) */
19935
+ :host([reflowing]) .cell {
19936
+ transition:
19937
+ transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1),
19938
+ background var(--transition),
19939
+ border-color var(--transition);
19940
+ }
19941
+ .cell.entering {
19942
+ animation: ixfx-list-item-in var(--grid-list-item-move-duration, 200ms) ease both;
19943
+ }
19944
+ @media (prefers-reduced-motion: reduce) {
19945
+ :host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
19946
+ .cell.entering { animation: none; }
19947
+ }
19948
+ `
19949
+ ];
19950
+ }
19951
+ #viewportRef;
19952
+ #resizeObserver;
19953
+ #mutationObserver;
19954
+ #scrollRaf;
19955
+ #items;
19956
+ #itemWrap;
19957
+ /** Items added mid-list that are still playing their enter animation. */
19958
+ #entering;
19959
+ /** Timer that ends the `[reflowing]` window opened by a mid-list insertion. */
19960
+ #reflowTimer;
19961
+ /**
19962
+ * When cell geometry changes (zoom), the item nearest the viewport centre is
19963
+ * captured here in `willUpdate` and the scroll position is corrected in
19964
+ * `updated` so that item stays put — rather than the grid anchoring to its
19965
+ * top-left corner.
19966
+ */
19967
+ #zoomAnchor;
19968
+ /**
19969
+ * Per-item slot name. Each cell renders its own named `<slot>` and the
19970
+ * matching light-DOM child carries `slot="<name>"`, so a child is projected
19971
+ * into exactly one cell. (A single unnamed slot would collapse every child
19972
+ * into the first cell.)
19973
+ */
19974
+ #slotNames;
19975
+ #slotSeq;
19976
+ connectedCallback() {
19977
+ super.connectedCallback();
19978
+ if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
19979
+ for (const [attr, value] of [
19980
+ [`caption-style`, this.captionStyle],
19981
+ [`caption-align`, this.captionAlign],
19982
+ [`caption-position`, this.captionPosition],
19983
+ [`caption-backdrop`, this.captionBackdrop]
19984
+ ]) if (!this.hasAttribute(attr)) this.setAttribute(attr, value);
19985
+ this.addEventListener(`keydown`, this._onKeyDown);
19986
+ this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
19987
+ this.#mutationObserver.observe(this, { childList: true });
19988
+ }
19989
+ disconnectedCallback() {
19990
+ super.disconnectedCallback();
19991
+ this.removeEventListener(`keydown`, this._onKeyDown);
19992
+ this.#resizeObserver?.disconnect();
19993
+ this.#resizeObserver = void 0;
19994
+ this.#mutationObserver?.disconnect();
19995
+ this.#cancelScrollRaf();
19996
+ if (this.#reflowTimer) {
19997
+ clearTimeout(this.#reflowTimer);
19998
+ this.#reflowTimer = 0;
19999
+ }
20000
+ }
20001
+ #onChildrenChanged(records) {
20002
+ const previousLength = this.#items.length;
20003
+ this.#syncItems();
20004
+ this._syncItemStates();
20005
+ this.#noteChildMutation(previousLength, records);
20006
+ this.requestUpdate();
20007
+ this.updateComplete.then(() => this.#emitViewportChange());
20008
+ }
20009
+ /**
20010
+ * When the child list changes mid-list — not a first populate or a wholesale
20011
+ * replace — mark any newcomers for an enter animation and open a short
20012
+ * `[reflowing]` window so the cells that shift slide to their new position
20013
+ * instead of snapping. Removals get the same window (the survivors slide to
20014
+ * close the gap). The window is bounded in time so the transform transition
20015
+ * never bleeds into scroll virtualisation.
20016
+ *
20017
+ * Reads the `MutationRecord`s directly rather than diffing `#items`, because a
20018
+ * `requestUpdate()` between the DOM change and this callback (e.g. a caller
20019
+ * that clears the selection before removing) lets `updated()` re-sync `#items`
20020
+ * first, which would hide the change from a naive diff.
20021
+ */
20022
+ #noteChildMutation(previousLength, records) {
20023
+ if (!this.hasUpdated || !records || records.length === 0) return;
20024
+ const currentSet = new Set(this.#items);
20025
+ const added = [];
20026
+ let removedCount = 0;
20027
+ for (const record of records) {
20028
+ for (const node of record.addedNodes) if (node.nodeType === Node.ELEMENT_NODE && currentSet.has(node)) added.push(node);
20029
+ for (const node of record.removedNodes) if (node.nodeType === Node.ELEMENT_NODE) removedCount++;
20030
+ }
20031
+ if (added.length === 0 && removedCount === 0) return;
20032
+ if (previousLength === 0 && removedCount === 0) return;
20033
+ if (added.length > 0 && added.length === this.#items.length) return;
20034
+ for (const el of added) this.#entering.add(el);
20035
+ this.toggleAttribute(`reflowing`, true);
20036
+ if (this.#reflowTimer) clearTimeout(this.#reflowTimer);
20037
+ this.#reflowTimer = window.setTimeout(() => {
20038
+ this.#reflowTimer = 0;
20039
+ this.#entering.clear();
20040
+ this.toggleAttribute(`reflowing`, false);
20041
+ this.requestUpdate();
20042
+ }, REFLOW_WINDOW_MS);
20043
+ }
20044
+ #emitViewportChange() {
20045
+ this._dispatch(`grid-list-viewport-change`, { visibleKeys: this.visibleKeys() });
20046
+ }
20047
+ firstUpdated() {
20048
+ const el = this.#viewportRef.value;
20049
+ if (!el) return;
20050
+ this._viewportWidth = el.clientWidth;
20051
+ this._viewportHeight = el.clientHeight;
20052
+ this.#resizeObserver = new ResizeObserver((entries) => {
20053
+ for (const entry of entries) {
20054
+ this._viewportWidth = entry.contentRect.width;
20055
+ this._viewportHeight = entry.contentRect.height;
20056
+ }
20057
+ });
20058
+ this.#resizeObserver.observe(el);
20059
+ this.#syncItems();
20060
+ this.requestUpdate();
20061
+ }
20062
+ willUpdate(changed) {
20063
+ if (changed.has(`itemSize`) || changed.has(`gap`)) {
20064
+ this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
20065
+ this.gap = Math.max(0, Math.round(this.gap));
20066
+ }
20067
+ if (changed.has(`captionHeight`)) this.captionHeight = Math.max(0, Math.round(this.captionHeight));
20068
+ if (changed.has(`itemWrap`)) this.#itemWrap = this.itemWrap;
20069
+ if ((changed.has(`itemSize`) || changed.has(`gap`) || changed.has(`captionStyle`) || changed.has(`captionHeight`) || changed.has(`orientation`)) && this.hasUpdated && this.#items.length > 0) {
20070
+ const prevOrientation = changed.get(`orientation`) ?? this.orientation;
20071
+ const horizontal = prevOrientation === `row`;
20072
+ const prevOffset = horizontal ? this._scrollLeft : this._scrollTop;
20073
+ const prevExtent = horizontal ? this._viewportWidth : this._viewportHeight;
20074
+ if (prevExtent > 0 && prevOffset > 1) {
20075
+ const prevItemSize = changed.get(`itemSize`) ?? this.itemSize;
20076
+ const prevGap = changed.get(`gap`) ?? this.gap;
20077
+ const prevCaptionStyle = changed.get(`captionStyle`) ?? this.captionStyle;
20078
+ const prevCaptionHeight = changed.get(`captionHeight`) ?? this.captionHeight;
20079
+ const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
20080
+ const prev = computeGridWindow(this.#items.length, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
20081
+ const mainStride = horizontal ? prev.colStride : prev.rowStride;
20082
+ const centerLine = (prevOffset + prevExtent / 2) / mainStride;
20083
+ const lineFloor = Math.max(0, Math.floor(centerLine));
20084
+ const index = horizontal ? Math.min(this.#items.length - 1, lineFloor) : Math.min(this.#items.length - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
20085
+ this.#zoomAnchor = {
20086
+ index,
20087
+ lineFrac: centerLine - lineFloor
20088
+ };
20089
+ }
20090
+ }
20091
+ }
20092
+ /** Full cell height: the square, plus a caption band in `separate` / `under`. */
20093
+ #rowHeight() {
20094
+ const captionBand = this.captionStyle === `separate` || this.captionStyle === `under`;
20095
+ return this.itemSize + (captionBand ? this.captionHeight : 0);
20096
+ }
20097
+ #captionFor(el) {
20098
+ return el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? ``;
20099
+ }
20100
+ updated(changed) {
20101
+ super.updated(changed);
20102
+ this.#syncItems();
20103
+ if (changed.has(`selectionMode`) || changed.has(`interactionMode`)) this.selection.clearAnchor();
20104
+ if (changed.has(`orientation`)) {
20105
+ const vp = this.#viewportRef.value;
20106
+ if (vp) {
20107
+ if (this.orientation === `row`) {
20108
+ vp.scrollTop = 0;
20109
+ this._scrollTop = 0;
20110
+ } else {
20111
+ vp.scrollLeft = 0;
20112
+ this._scrollLeft = 0;
20113
+ }
20114
+ }
20115
+ }
20116
+ this.#applyZoomAnchor();
20117
+ }
20118
+ /** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
20119
+ #applyZoomAnchor() {
20120
+ if (!this.#zoomAnchor) return;
20121
+ const { index, lineFrac } = this.#zoomAnchor;
20122
+ this.#zoomAnchor = void 0;
20123
+ const vp = this.#viewportRef.value;
20124
+ if (!vp) return;
20125
+ const win = this.#currentWindow();
20126
+ if (win.horizontal) {
20127
+ if (this._viewportWidth <= 0) return;
20128
+ const targetCenter = (index + lineFrac) * win.colStride;
20129
+ const maxScroll = Math.max(0, win.totalWidth - this._viewportWidth);
20130
+ const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportWidth / 2));
20131
+ if (Math.abs(vp.scrollLeft - next) > .5) {
20132
+ vp.scrollLeft = next;
20133
+ this._scrollLeft = next;
20134
+ }
20135
+ return;
20136
+ }
20137
+ if (this._viewportHeight <= 0) return;
20138
+ const targetCenter = ((win.cols > 0 ? Math.floor(index / win.cols) : 0) + lineFrac) * win.rowStride;
20139
+ const maxScroll = Math.max(0, win.totalHeight - this._viewportHeight);
20140
+ const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportHeight / 2));
20141
+ if (Math.abs(vp.scrollTop - next) > .5) {
20142
+ vp.scrollTop = next;
20143
+ this._scrollTop = next;
20144
+ }
20145
+ }
20146
+ #syncItems() {
20147
+ this.#items = Array.from(this.querySelectorAll(ITEM_SELECTOR$1));
20148
+ for (const el of this.#items) {
20149
+ const name = this.#slotNameFor(el);
20150
+ if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
20151
+ }
20152
+ }
20153
+ #slotNameFor(el) {
20154
+ let name = this.#slotNames.get(el);
20155
+ if (name === void 0) {
20156
+ this.#slotSeq += 1;
20157
+ name = `gl-item-${this.#slotSeq}`;
20158
+ this.#slotNames.set(el, name);
20159
+ }
20160
+ return name;
20161
+ }
20162
+ _getVisibleItems() {
20163
+ return this.#items.filter((el) => {
20164
+ if (el.hidden) return false;
20165
+ return true;
20166
+ });
20167
+ }
20168
+ get count() {
20169
+ return this.#items.length;
20170
+ }
20171
+ /**
20172
+ * Number of cells currently rendered in the shadow DOM. With virtualization
20173
+ * this is roughly the visible rows plus overscan, regardless of how large
20174
+ * `count` is — useful for demos/diagnostics.
20175
+ */
20176
+ get renderedCount() {
20177
+ return this.renderRoot.querySelectorAll(`.cell`).length;
20178
+ }
20179
+ get selectedItems() {
20180
+ return this.selection.selected;
20181
+ }
20182
+ /** The selection as stable `data-key` / `data-value` strings. */
20183
+ get selectedKeys() {
20184
+ return this.selection.selectedKeys;
20185
+ }
20186
+ select(item) {
20187
+ this.selection.replace(item);
20188
+ }
20189
+ selectMany(items) {
20190
+ this.selection.selectMany(items);
20191
+ }
20192
+ deselect(item) {
20193
+ this.selection.deselect(item);
20194
+ }
20195
+ clearSelection() {
20196
+ this.selection.clear();
20197
+ }
20198
+ selectAll() {
20199
+ if (this.selection.canSelectAll()) this.selection.selectAll();
20200
+ }
20201
+ /**
20202
+ * Replace the selection with the items carrying these keys. Unknown keys are
20203
+ * ignored; survives the host rebuilding the item set. Requires each item to
20204
+ * have a `data-key` (or `data-value`).
20205
+ */
20206
+ selectKeys(keys) {
20207
+ this.selection.selectKeys(keys, (key) => this.getItem(key));
20208
+ }
20209
+ /** Add items by key to the selection (multi-select modes only). */
20210
+ addSelectedKeys(keys) {
20211
+ this.selection.addKeys(keys, (key) => this.getItem(key));
20212
+ }
20213
+ /** Remove items by key from the selection. */
20214
+ removeSelectedKeys(keys) {
20215
+ this.selection.removeKeys(keys, (key) => this.getItem(key));
20216
+ }
20217
+ /** Move the cursor to the item with this key and scroll it into view. */
20218
+ focusKey(key) {
20219
+ const el = this.getItem(key);
20220
+ if (!el) return;
20221
+ this.tickled.focus(el);
20222
+ this.#scrollItemIntoView(el);
20223
+ }
20224
+ /** The key of the item under the cursor, or `undefined`. */
20225
+ get tickledKey() {
20226
+ const id = this.tickled.tickledId;
20227
+ return id ? keyForElement(id) : void 0;
20228
+ }
20229
+ getItem(key) {
20230
+ return this.#items.find((el) => keyForElement(el) === key);
20231
+ }
20232
+ getItemByIndex(index) {
20233
+ return this.#items[index];
20234
+ }
20235
+ getKey(item) {
20236
+ return keyForElement(item);
20237
+ }
20238
+ visibleKeys() {
20239
+ const win = this.#currentWindow();
20240
+ return this.#items.slice(win.startIndex, win.endIndex).map((el) => keyForElement(el)).filter((k) => k !== void 0);
20241
+ }
20242
+ /** Adjust the scroll offset so `el` is fully within the viewport. */
20243
+ #scrollItemIntoView(el) {
20244
+ const index = this.#items.indexOf(el);
20245
+ const vp = this.#viewportRef.value;
20246
+ if (index < 0 || !vp) return;
20247
+ const win = this.#currentWindow();
20248
+ if (win.horizontal) {
20249
+ const left = index * win.colStride;
20250
+ const right = left + this.itemSize;
20251
+ const viewLeft = vp.scrollLeft;
20252
+ let next = viewLeft;
20253
+ if (left < viewLeft) next = left;
20254
+ else if (right > viewLeft + this._viewportWidth) next = right - this._viewportWidth;
20255
+ if (next !== viewLeft) {
20256
+ vp.scrollLeft = next;
20257
+ this._scrollLeft = next;
20258
+ }
20259
+ return;
20260
+ }
20261
+ const top = (win.cols > 0 ? Math.floor(index / win.cols) : 0) * win.rowStride;
20262
+ const bottom = top + this.#rowHeight();
20263
+ const viewTop = vp.scrollTop;
20264
+ let next = viewTop;
20265
+ if (top < viewTop) next = top;
20266
+ else if (bottom > viewTop + this._viewportHeight) next = bottom - this._viewportHeight;
20267
+ if (next !== viewTop) {
20268
+ vp.scrollTop = next;
20269
+ this._scrollTop = next;
20270
+ }
20271
+ }
20272
+ _syncItemStates() {
20273
+ for (const el of this.#items) {
20274
+ el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
20275
+ el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
20276
+ }
20277
+ this.setAttribute(`data-input-mode`, this.tickled.inputMode);
20278
+ }
20279
+ _dispatch(name, detail) {
20280
+ this.dispatchEvent(new CustomEvent(name, {
20281
+ detail,
20282
+ bubbles: true,
20283
+ composed: true
20284
+ }));
20285
+ }
20286
+ render() {
20287
+ const win = this.#currentWindow();
20288
+ const slice = this.#items.slice(win.startIndex, win.endIndex);
20289
+ const spacerStyle = win.horizontal ? `width:${win.totalWidth}px;height:100%` : `height:${win.totalHeight}px`;
20290
+ const scrollFade = win.horizontal ? `scroll-fade-x` : `scroll-fade-y`;
20291
+ return b`
20292
+ <div
20293
+ class="viewport ${scrollFade}"
20294
+ part="viewport"
20295
+ ${ref(this.#viewportRef)}
20296
+ data-input-mode=${this.tickled.inputMode}
20297
+ @scroll=${this.#onScroll}
20298
+ >
20299
+ <div class="spacer" style=${spacerStyle}>
20300
+ ${repeat(slice, (el) => this.#slotNameFor(el), (el, i) => this.#renderCell(el, win.startIndex + i, win))}
20301
+ </div>
20302
+ </div>
20303
+ `;
20304
+ }
20305
+ #renderCell(el, index, win) {
20306
+ const selected = this.selection.selected.has(el);
20307
+ const tickled = this.tickled.isTickled(el);
20308
+ const entering = this.#entering.has(el);
20309
+ const style = this.captionStyle;
20310
+ const separate = style === `separate`;
20311
+ const caption = style === `hidden` ? `` : this.#captionFor(el);
20312
+ const showCaption = caption !== ``;
20313
+ const cellHeight = win.rowStride - this.gap;
20314
+ const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
20315
+ const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
20316
+ const thumbStyle = separate ? `height:${this.itemSize}px` : ``;
20317
+ const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
20318
+ return b`
20319
+ <div
20320
+ class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}"
20321
+ part="item"
20322
+ style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
20323
+ data-index=${index}
20324
+ @click=${(e) => this.#onCellClick(e, el)}
20325
+ @dblclick=${() => this.#onCellActivate(el)}
20326
+ @pointerenter=${() => this.tickled.handlePointerEnter(el)}
20327
+ >
20328
+ <div class="thumb" part="thumb" style=${thumbStyle}>
20329
+ <slot name=${this.#slotNameFor(el)}></slot>
20330
+ </div>
20331
+ ${showCaption ? b`<div class="caption" part="caption" style=${captionCss}>${caption}</div>` : ``}
20332
+ </div>
20333
+ `;
20334
+ }
20335
+ #onScroll;
20336
+ #cancelScrollRaf() {
20337
+ if (this.#scrollRaf !== 0) {
20338
+ cancelAnimationFrame(this.#scrollRaf);
20339
+ this.#scrollRaf = 0;
20340
+ }
20341
+ }
20342
+ #onCellClick(e, item) {
20343
+ this.selection.handleClick(item, e);
20344
+ this._dispatch(`list-item-click`, { item });
20345
+ }
20346
+ #onCellActivate(item) {
20347
+ if (this.interactionMode !== `manual`) this.selection.replace(item);
20348
+ this._dispatch(`list-activate`, { item });
20349
+ }
20350
+ #currentWindow() {
20351
+ return computeGridWindow(this.#items.length, this.itemSize, this.#rowHeight(), this.gap, this._viewportWidth, this._viewportHeight, this.orientation === `row` ? this._scrollLeft : this._scrollTop, this.orientation);
20352
+ }
20353
+ };
20354
+ __decorate([property({
20355
+ type: String,
20356
+ attribute: `selection-mode`
20357
+ })], GridListElement.prototype, "selectionMode", void 0);
20358
+ __decorate([property({
20359
+ type: String,
20360
+ attribute: `interaction-mode`
20361
+ })], GridListElement.prototype, "interactionMode", void 0);
20362
+ __decorate([property({
20363
+ type: Number,
20364
+ attribute: `item-size`
20365
+ })], GridListElement.prototype, "itemSize", void 0);
20366
+ __decorate([property({ type: Number })], GridListElement.prototype, "gap", void 0);
20367
+ __decorate([property({
20368
+ type: String,
20369
+ reflect: true
20370
+ })], GridListElement.prototype, "orientation", void 0);
20371
+ __decorate([property({
20372
+ type: String,
20373
+ attribute: `caption-style`,
20374
+ reflect: true
20375
+ })], GridListElement.prototype, "captionStyle", void 0);
20376
+ __decorate([property({
20377
+ type: String,
20378
+ attribute: `caption-align`,
20379
+ reflect: true
20380
+ })], GridListElement.prototype, "captionAlign", void 0);
20381
+ __decorate([property({
20382
+ type: String,
20383
+ attribute: `caption-position`,
20384
+ reflect: true
20385
+ })], GridListElement.prototype, "captionPosition", void 0);
20386
+ __decorate([property({
20387
+ type: String,
20388
+ attribute: `caption-backdrop`,
20389
+ reflect: true
20390
+ })], GridListElement.prototype, "captionBackdrop", void 0);
20391
+ __decorate([property({
20392
+ type: Number,
20393
+ attribute: `caption-height`
20394
+ })], GridListElement.prototype, "captionHeight", void 0);
20395
+ __decorate([property({
20396
+ type: String,
20397
+ attribute: `item-wrap`
20398
+ })], GridListElement.prototype, "itemWrap", void 0);
20399
+ __decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
20400
+ __decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
20401
+ __decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
20402
+ __decorate([state()], GridListElement.prototype, "_viewportWidth", void 0);
20403
+ GridListElement = __decorate([safeCustomElement(`ixfx-grid-list`)], GridListElement);
20404
+ //#endregion
18704
20405
  //#region src/panel/panel.ts
18705
20406
  let PanelElement = class PanelElement extends i {
18706
20407
  constructor(..._args) {
@@ -19053,6 +20754,8 @@ __decorate([property({
19053
20754
  GroupedItemListerElement = __decorate([safeCustomElement(`ixfx-grouped-item-lister`)], GroupedItemListerElement);
19054
20755
  //#endregion
19055
20756
  //#region src/tree/tree-base.ts
20757
+ /** Above this many rows entering / leaving at once, skip the animation entirely. */
20758
+ const MAX_ANIMATED_ROWS = 50;
19056
20759
  let TreeBaseElement = class TreeBaseElement extends i {
19057
20760
  constructor(..._args) {
19058
20761
  super(..._args);
@@ -19076,6 +20779,12 @@ let TreeBaseElement = class TreeBaseElement extends i {
19076
20779
  });
19077
20780
  this._focusedDepth = 0;
19078
20781
  this.#config = { indent: 20 };
20782
+ this.#renderedKeys = [];
20783
+ this.#lastNodeByKey = /* @__PURE__ */ new Map();
20784
+ this.#enteringKeys = /* @__PURE__ */ new Set();
20785
+ this.#leaving = /* @__PURE__ */ new Map();
20786
+ this.#leavingAnimating = /* @__PURE__ */ new Set();
20787
+ this.#treeAnimReady = false;
19079
20788
  this.#onKeyDown = (event) => {
19080
20789
  const nodes = this.#getAllNodes();
19081
20790
  if (nodes.length === 0) return;
@@ -19137,7 +20846,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
19137
20846
  bubbles: true,
19138
20847
  composed: true
19139
20848
  }));
19140
- } else if (current.depth > 1) {
20849
+ } else if (current.depth > 0) {
19141
20850
  const parentDepth = current.depth - 1;
19142
20851
  for (let i = currentIdx - 1; i >= 0; i--) if (nodes[i].depth === parentDepth) {
19143
20852
  this.#setFocusedIndex(i);
@@ -19203,6 +20912,85 @@ let TreeBaseElement = class TreeBaseElement extends i {
19203
20912
  #anchor;
19204
20913
  #ownedRegistry;
19205
20914
  #config;
20915
+ #renderedKeys;
20916
+ #lastNodeByKey;
20917
+ #enteringKeys;
20918
+ #leaving;
20919
+ #leavingAnimating;
20920
+ #treeAnimReady;
20921
+ willUpdate(changedProps) {
20922
+ super.willUpdate(changedProps);
20923
+ this.#diffVisibleNodes();
20924
+ }
20925
+ #diffVisibleNodes() {
20926
+ const nodes = this.root ? this.#getAllNodes() : [];
20927
+ const nowKeys = new Set(nodes.map((n) => n.node.item.key));
20928
+ this.#enteringKeys.clear();
20929
+ if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
20930
+ const prevSet = new Set(this.#renderedKeys);
20931
+ for (const { node } of nodes) if (!prevSet.has(node.item.key)) this.#enteringKeys.add(node.item.key);
20932
+ if (this.#enteringKeys.size > MAX_ANIMATED_ROWS) this.#enteringKeys.clear();
20933
+ for (const key of this.#renderedKeys) if (!nowKeys.has(key) && !this.#leaving.has(key)) {
20934
+ const last = this.#lastNodeByKey.get(key);
20935
+ if (last) this.#leaving.set(key, last);
20936
+ }
20937
+ }
20938
+ for (const key of [...this.#leaving.keys()]) if (nowKeys.has(key)) {
20939
+ this.#leaving.delete(key);
20940
+ this.#leavingAnimating.delete(key);
20941
+ }
20942
+ if (this.#leaving.size > MAX_ANIMATED_ROWS) {
20943
+ this.#leaving.clear();
20944
+ this.#leavingAnimating.clear();
20945
+ }
20946
+ }
20947
+ /** True while `node` is a fading ghost row (removed / collapsed, still animating out). */
20948
+ _isLeaving(node) {
20949
+ return this.#leaving.has(node.item.key);
20950
+ }
20951
+ /** Splice the fading ghost rows back into the visible list near where they were. */
20952
+ #weaveLeaving(nodes) {
20953
+ if (this.#leaving.size === 0) return nodes;
20954
+ const result = [...nodes];
20955
+ const indexOfKey = (key) => result.findIndex((e) => e.node.item.key === key);
20956
+ for (const [key, entry] of this.#leaving) {
20957
+ const prevIdx = this.#renderedKeys.indexOf(key);
20958
+ let insertAt = result.length;
20959
+ for (let j = prevIdx + 1; j < this.#renderedKeys.length; j++) {
20960
+ const at = indexOfKey(this.#renderedKeys[j]);
20961
+ if (at !== -1) {
20962
+ insertAt = at;
20963
+ break;
20964
+ }
20965
+ }
20966
+ result.splice(insertAt, 0, entry);
20967
+ }
20968
+ return result;
20969
+ }
20970
+ #runItemAnimations() {
20971
+ if (this.#enteringKeys.size > 0) {
20972
+ for (const el of this.renderRoot.querySelectorAll(`.item`)) {
20973
+ const key = el.dataset.key;
20974
+ if (key !== void 0 && this.#enteringKeys.has(key)) animateListItemIn(el);
20975
+ }
20976
+ this.#enteringKeys.clear();
20977
+ }
20978
+ if (this.#leaving.size > 0) for (const el of this.renderRoot.querySelectorAll(`.item.leaving`)) {
20979
+ const key = el.dataset.key;
20980
+ if (key === void 0 || this.#leavingAnimating.has(key)) continue;
20981
+ this.#leavingAnimating.add(key);
20982
+ animateListItemOut(el).then(() => {
20983
+ this.#leaving.delete(key);
20984
+ this.#leavingAnimating.delete(key);
20985
+ this.requestUpdate();
20986
+ });
20987
+ }
20988
+ const visible = this.root ? this.#getAllNodes() : [];
20989
+ this.#renderedKeys = visible.map((n) => n.node.item.key);
20990
+ this.#lastNodeByKey.clear();
20991
+ for (const entry of visible) this.#lastNodeByKey.set(entry.node.item.key, entry);
20992
+ this.#treeAnimReady = true;
20993
+ }
19206
20994
  updated(changedProps) {
19207
20995
  if (changedProps.has(`root`) && this.root) {
19208
20996
  this.#abortPending();
@@ -19237,6 +21025,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
19237
21025
  if (changedProps.has(`selectedNode`) && this.selectedNode) {
19238
21026
  if (changedProps.get(`selectedNode`) !== this.selectedNode) this.tickled.setTickled(this.selectedNode.item.key);
19239
21027
  }
21028
+ this.#runItemAnimations();
19240
21029
  }
19241
21030
  connectedCallback() {
19242
21031
  super.connectedCallback();
@@ -19401,9 +21190,9 @@ let TreeBaseElement = class TreeBaseElement extends i {
19401
21190
  }
19402
21191
  }
19403
21192
  }
19404
- renderNodeContent(node, _depth) {
21193
+ renderNodeContent(node, depth) {
19405
21194
  const isBranch = node.item.isLeaf !== true;
19406
- const isExpanded = this._isExpanded(node);
21195
+ const isExpanded = this._isExpanded(node, depth);
19407
21196
  return b`
19408
21197
  <span class="tree-caret">
19409
21198
  ${isBranch ? b`<ixfx-icon class="caret-icon" name="chevron-down" .rotate=${isExpanded ? A : `-90deg`}></ixfx-icon>` : ``}
@@ -19684,9 +21473,9 @@ let TreeBaseElement = class TreeBaseElement extends i {
19684
21473
  _isFocused(node) {
19685
21474
  return this.tickled.isTickled(node.item.key);
19686
21475
  }
19687
- _isExpanded(node) {
21476
+ _isExpanded(node, depth = 0) {
19688
21477
  if (this._expandedKeys.has(node.item.key)) return true;
19689
- if (this.filterPredicate && node.children) return node.children.some((child) => this.#matchesFilter(child, 0));
21478
+ if (this.filterPredicate && node.children) return node.children.some((child) => this.#matchesFilter(child, depth + 1));
19690
21479
  return false;
19691
21480
  }
19692
21481
  #matchesFilter(node, depth) {
@@ -19767,7 +21556,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
19767
21556
  }
19768
21557
  #getAllNodes() {
19769
21558
  if (!this.root) return [];
19770
- return this.#getVisibleNodes(this.root, 0);
21559
+ return this.#getVisibleNodes(this.root, -1);
19771
21560
  }
19772
21561
  #getFocusedIndex() {
19773
21562
  const nodes = this.#getAllNodes();
@@ -19821,14 +21610,14 @@ let TreeBaseElement = class TreeBaseElement extends i {
19821
21610
  }
19822
21611
  #onDblClick;
19823
21612
  #renderNodes(nodes) {
19824
- if (nodes.length === 0 && this.#pendingNode) return b`
21613
+ if (nodes.length === 0 && this.#pendingNode && this.#leaving.size === 0) return b`
19825
21614
  <div class="tree-loading">
19826
21615
  ${this.renderLoadingIndicator()}
19827
21616
  </div>
19828
21617
  `;
19829
21618
  return b`
19830
21619
  <ul class="tree-list" role="tree">
19831
- ${nodes.map(({ node, depth }) => this._renderNode(node, depth))}
21620
+ ${this.#weaveLeaving(nodes).map(({ node, depth }) => this._renderNode(node, depth))}
19832
21621
  </ul>
19833
21622
  `;
19834
21623
  }
@@ -19836,15 +21625,17 @@ let TreeBaseElement = class TreeBaseElement extends i {
19836
21625
  const selected = this._isSelected(node);
19837
21626
  const tickled = this._isFocused(node);
19838
21627
  const isBranch = node.item.isLeaf !== true;
19839
- const isExpanded = this._isExpanded(node);
21628
+ const isExpanded = this._isExpanded(node, depth);
19840
21629
  const isPending = this.#pendingNode === node;
19841
21630
  const isFiltered = this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
21631
+ const isLeaving = this._isLeaving(node);
19842
21632
  return b`
19843
21633
  <li
19844
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``}"
21634
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``} ${isLeaving ? `leaving` : ``}"
19845
21635
  role="treeitem"
19846
21636
  aria-selected=${selected}
19847
21637
  aria-expanded=${isBranch ? isExpanded : A}
21638
+ aria-hidden=${isLeaving ? `true` : A}
19848
21639
  aria-level=${depth + 1}
19849
21640
  data-key=${node.item.key}
19850
21641
  style=${`--depth: ${depth}`}
@@ -19965,6 +21756,12 @@ let TreeBaseElement = class TreeBaseElement extends i {
19965
21756
  display: none;
19966
21757
  }
19967
21758
 
21759
+ /* Fading-out ghost row (see #runItemAnimations) — inert while it collapses. */
21760
+ .item.leaving {
21761
+ pointer-events: none;
21762
+ overflow: hidden;
21763
+ }
21764
+
19968
21765
  ::highlight(incr-search-highlight) {
19969
21766
  background: var(--search-highlight-bg, var(--accent));
19970
21767
  color: var(--search-highlight-color, var(--accent-text, #fff));
@@ -20024,11 +21821,11 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
20024
21821
  if (node.item.icon) this.#depthsWithIcons.add(depth);
20025
21822
  if (node.children) for (const child of node.children) visit(child, depth + 1);
20026
21823
  };
20027
- if (this.root) visit(this.root, 0);
21824
+ if (this.root) visit(this.root, -1);
20028
21825
  }
20029
21826
  renderNodeContent(node, depth) {
20030
21827
  const isBranch = node.item.isLeaf !== true;
20031
- const isExpanded = this._isExpanded(node);
21828
+ const isExpanded = this._isExpanded(node, depth);
20032
21829
  const icon = node.item.icon;
20033
21830
  const tooltip = node.item.tooltip;
20034
21831
  const caretAndLabel = b`
@@ -20074,16 +21871,18 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
20074
21871
  const selected = this._isSelected(node);
20075
21872
  const tickled = this._isFocused(node);
20076
21873
  const isBranch = node.item.isLeaf !== true;
20077
- const isExpanded = this._isExpanded(node);
21874
+ const isExpanded = this._isExpanded(node, depth);
20078
21875
  const isPending = this._pendingNode === node;
20079
21876
  const hasIconAtDepth = this.#depthsWithIcons.has(depth);
21877
+ const isLeaving = this._isLeaving(node);
20080
21878
  return b`
20081
21879
  <li
20082
- class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``}"
21880
+ class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``}"
20083
21881
  data-key=${node.item.key}
20084
21882
  role="treeitem"
20085
21883
  aria-selected=${selected}
20086
21884
  aria-expanded=${isBranch ? isExpanded : A}
21885
+ aria-hidden=${isLeaving ? `true` : A}
20087
21886
  aria-level=${depth + 1}
20088
21887
  style=${`--depth: ${depth}`}
20089
21888
  @click=${(e) => this._handleItemClick(node, depth, e)}
@@ -20356,6 +22155,7 @@ let VerticalListElement = class VerticalListElement extends i {
20356
22155
  this._searchQuery = ``;
20357
22156
  this.#customWrappers = /* @__PURE__ */ new Map();
20358
22157
  this.#wrappedElements = /* @__PURE__ */ new Set();
22158
+ this.#animatedItems = /* @__PURE__ */ new WeakSet();
20359
22159
  this.selection = new ListSelectionController({
20360
22160
  getOrderedItems: () => this._getVisibleItems(),
20361
22161
  isSelectable: (el) => this._isCheckable(el),
@@ -20363,11 +22163,13 @@ let VerticalListElement = class VerticalListElement extends i {
20363
22163
  selectionMode: this.selectionMode,
20364
22164
  interactionMode: this.interactionMode
20365
22165
  }),
22166
+ getKey: keyForElement,
20366
22167
  onChange: (selected, previous) => {
20367
22168
  this._syncItemStates();
20368
22169
  this._dispatch(`list-select`, {
20369
22170
  selected,
20370
- previous
22171
+ previous,
22172
+ selectedKeys: this.selection.selectedKeys
20371
22173
  });
20372
22174
  }
20373
22175
  });
@@ -20480,6 +22282,10 @@ let VerticalListElement = class VerticalListElement extends i {
20480
22282
  #search;
20481
22283
  #customWrappers;
20482
22284
  #wrappedElements;
22285
+ /** Watches for items added to the light DOM so they can be animated in. */
22286
+ #itemObserver;
22287
+ /** Items that have already played (or been skipped for) their enter animation. */
22288
+ #animatedItems;
20483
22289
  connectedCallback() {
20484
22290
  super.connectedCallback();
20485
22291
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -20488,6 +22294,9 @@ let VerticalListElement = class VerticalListElement extends i {
20488
22294
  this.addEventListener(`keydown`, this._onKeyDown);
20489
22295
  this.addEventListener(`click`, this._onClick);
20490
22296
  this.addEventListener(`dblclick`, this._onDblClick);
22297
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
22298
+ this.#itemObserver ??= new MutationObserver((records) => this.#onItemsMutated(records));
22299
+ this.#itemObserver.observe(this, { childList: true });
20491
22300
  }
20492
22301
  disconnectedCallback() {
20493
22302
  super.disconnectedCallback();
@@ -20496,9 +22305,31 @@ let VerticalListElement = class VerticalListElement extends i {
20496
22305
  this.removeEventListener(`keydown`, this._onKeyDown);
20497
22306
  this.removeEventListener(`click`, this._onClick);
20498
22307
  this.removeEventListener(`dblclick`, this._onDblClick);
22308
+ this.#itemObserver?.disconnect();
20499
22309
  this.#search?.dispose();
20500
22310
  this.#search = void 0;
20501
22311
  }
22312
+ /**
22313
+ * Animate in items appended to the light DOM (however they got there —
22314
+ * {@link addItem}, direct `appendChild`, a framework binding). A bulk populate
22315
+ * (roughly everything at once) is left alone, as are the wrapper `<li>`s the
22316
+ * checked-mode plumbing shuffles around.
22317
+ */
22318
+ #onItemsMutated(records) {
22319
+ if (!this.isConnected) return;
22320
+ const fresh = [];
22321
+ for (const record of records) for (const node of record.addedNodes) {
22322
+ if (!(node instanceof HTMLElement) || node.parentElement !== this) continue;
22323
+ if (!node.matches(ITEM_SELECTOR)) continue;
22324
+ if (this.#animatedItems.has(node) || this.#customWrappers.has(node) || this.#wrappedElements.has(node)) continue;
22325
+ this.#animatedItems.add(node);
22326
+ fresh.push(node);
22327
+ }
22328
+ if (fresh.length === 0) return;
22329
+ const total = this.querySelectorAll(ITEM_SELECTOR).length;
22330
+ if (fresh.length >= total) return;
22331
+ for (const el of fresh) animateListItemIn(el);
22332
+ }
20502
22333
  firstUpdated() {
20503
22334
  this.#search = createElementIncrSearch({
20504
22335
  container: this,
@@ -20544,8 +22375,21 @@ let VerticalListElement = class VerticalListElement extends i {
20544
22375
  this.appendChild(el);
20545
22376
  if (this.interactionMode === `checked`) this._syncCheckboxes();
20546
22377
  }
22378
+ /**
22379
+ * Collapse + fade `item` out, then detach it. The rows below slide up to
22380
+ * close the gap. Resolves once the element has actually been removed.
22381
+ * Under `prefers-reduced-motion: reduce` it's removed immediately.
22382
+ */
20547
22383
  removeItem(item) {
20548
- item.remove();
22384
+ this.selection.deselect(item);
22385
+ return animateListItemOut(item).then(() => {
22386
+ item.remove();
22387
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
22388
+ });
22389
+ }
22390
+ /** {@link removeItem} for several items at once. */
22391
+ removeItems(items) {
22392
+ return Promise.all([...items].map((item) => this.removeItem(item))).then(() => void 0);
20549
22393
  }
20550
22394
  clearItems() {
20551
22395
  this.#customWrappers.clear();
@@ -20584,6 +22428,43 @@ let VerticalListElement = class VerticalListElement extends i {
20584
22428
  selectAll() {
20585
22429
  if (this.selection.canSelectAll()) this.selection.selectAll();
20586
22430
  }
22431
+ /** The selection as stable `data-key` / `data-value` strings. */
22432
+ get selectedKeys() {
22433
+ return this.selection.selectedKeys;
22434
+ }
22435
+ /** The key of the item under the cursor, or `undefined`. */
22436
+ get tickledKey() {
22437
+ const id = this.tickled.tickledId;
22438
+ return id ? keyForElement(id) : void 0;
22439
+ }
22440
+ /** Replace the selection with the items carrying these keys (unknown keys ignored). */
22441
+ selectKeys(keys) {
22442
+ this.selection.selectKeys(keys, (key) => this.#itemForKey(key));
22443
+ }
22444
+ /** Add items by key to the selection (multi-select modes only). */
22445
+ addSelectedKeys(keys) {
22446
+ this.selection.addKeys(keys, (key) => this.#itemForKey(key));
22447
+ }
22448
+ /** Remove items by key from the selection. */
22449
+ removeSelectedKeys(keys) {
22450
+ this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
22451
+ }
22452
+ /** Move the cursor to the item with this key and scroll it into view. */
22453
+ focusKey(key) {
22454
+ const el = this.#itemForKey(key);
22455
+ if (!el) return;
22456
+ this.tickled.focus(el);
22457
+ el.scrollIntoView({
22458
+ block: `nearest`,
22459
+ inline: `nearest`
22460
+ });
22461
+ }
22462
+ #itemForKey(key) {
22463
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
22464
+ if (this.#wrappedElements.has(el)) continue;
22465
+ if (keyForElement(el) === key) return el;
22466
+ }
22467
+ }
20587
22468
  *selectedFilter(kind) {
20588
22469
  const wanted = (el) => {
20589
22470
  switch (kind) {
@@ -27228,6 +29109,136 @@ function dismiss(id) {
27228
29109
  syncRenderer();
27229
29110
  }
27230
29111
  //#endregion
29112
+ //#region src/pan-text/pan-text.ts
29113
+ const SPEED = 45;
29114
+ const END_PADDING = 4;
29115
+ const START_DELAY = 300;
29116
+ const PAN_EASING = `cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
29117
+ let PanTextElement = class PanTextElement extends i {
29118
+ constructor(..._args) {
29119
+ super(..._args);
29120
+ this.direction = `end`;
29121
+ this.disabled = false;
29122
+ this._isOverflowing = false;
29123
+ this._isHovering = false;
29124
+ this._overflow = 0;
29125
+ this._resizeObserver = null;
29126
+ this._onMouseEnter = () => {
29127
+ this._isHovering = true;
29128
+ };
29129
+ this._onMouseLeave = () => {
29130
+ this._isHovering = false;
29131
+ };
29132
+ this._measure = () => {
29133
+ if (this._isHovering) return;
29134
+ const el = this._textEl;
29135
+ if (!el) return;
29136
+ this._overflow = Math.max(0, el.scrollWidth - el.clientWidth);
29137
+ const overflowing = this._overflow > 0;
29138
+ this.classList.toggle(`is-overflowing`, overflowing);
29139
+ if (overflowing !== this._isOverflowing) this._isOverflowing = overflowing;
29140
+ };
29141
+ }
29142
+ connectedCallback() {
29143
+ super.connectedCallback();
29144
+ this._resizeObserver = new ResizeObserver(() => this._measure());
29145
+ this.addEventListener(`mouseenter`, this._onMouseEnter);
29146
+ this.addEventListener(`mouseleave`, this._onMouseLeave);
29147
+ }
29148
+ disconnectedCallback() {
29149
+ super.disconnectedCallback();
29150
+ this._resizeObserver?.disconnect();
29151
+ this._resizeObserver = null;
29152
+ this.removeEventListener(`mouseenter`, this._onMouseEnter);
29153
+ this.removeEventListener(`mouseleave`, this._onMouseLeave);
29154
+ }
29155
+ firstUpdated() {
29156
+ this._measure();
29157
+ this._resizeObserver?.observe(this);
29158
+ document.fonts?.ready.then(() => this._measure());
29159
+ }
29160
+ updated() {
29161
+ this._measure();
29162
+ }
29163
+ _textStyle() {
29164
+ if (!this._isOverflowing) return ``;
29165
+ const distance = this._overflow + END_PADDING;
29166
+ const duration = Math.max(1.5, distance / SPEED);
29167
+ const revealEnd = this.direction === `end`;
29168
+ const restX = revealEnd ? 0 : -distance;
29169
+ const hoverX = revealEnd ? -distance : 0;
29170
+ if (this._isHovering) return [
29171
+ `transform: translateX(${hoverX}px)`,
29172
+ `transition: transform ${duration}s ${PAN_EASING} ${START_DELAY}ms`,
29173
+ `overflow: visible`,
29174
+ `text-overflow: clip`
29175
+ ].join(`; `);
29176
+ return [
29177
+ `transform: translateX(${restX}px)`,
29178
+ `transition: transform 400ms ${PAN_EASING}`,
29179
+ revealEnd ? `` : `overflow: visible; text-overflow: clip`
29180
+ ].filter(Boolean).join(`; `);
29181
+ }
29182
+ render() {
29183
+ const textClasses = {
29184
+ "is-overflowing": this._isOverflowing,
29185
+ "is-hovering": this._isHovering
29186
+ };
29187
+ return b`
29188
+ <span class="text ${classMap(textClasses)}" part="text" style=${this._textStyle()}>
29189
+ <slot @slotchange=${this._measure}></slot>
29190
+ </span>
29191
+ `;
29192
+ }
29193
+ static {
29194
+ this.styles = [themeFallbacks, i$3`
29195
+ /* Renders like plain inline text: no colours, border, padding or margin
29196
+ of its own — it only adds the clipping needed to pan on hover. */
29197
+ :host {
29198
+ display: inline-block;
29199
+ overflow: hidden;
29200
+ white-space: nowrap;
29201
+ max-width: 100%;
29202
+ box-sizing: border-box;
29203
+ font: inherit;
29204
+ color: inherit;
29205
+ }
29206
+
29207
+ :host([disabled]) {
29208
+ opacity: var(--disabled-opacity, 0.5);
29209
+ pointer-events: none;
29210
+ }
29211
+
29212
+ .text {
29213
+ display: block;
29214
+ overflow: hidden;
29215
+ text-overflow: ellipsis;
29216
+ white-space: nowrap;
29217
+ will-change: transform;
29218
+ }
29219
+
29220
+ @media (prefers-reduced-motion: reduce) {
29221
+ .text {
29222
+ transition: none !important;
29223
+ transform: none !important;
29224
+ }
29225
+ }
29226
+ `];
29227
+ }
29228
+ };
29229
+ __decorate([property({
29230
+ type: String,
29231
+ attribute: `direction`
29232
+ })], PanTextElement.prototype, "direction", void 0);
29233
+ __decorate([property({
29234
+ type: Boolean,
29235
+ attribute: `disabled`
29236
+ })], PanTextElement.prototype, "disabled", void 0);
29237
+ __decorate([state()], PanTextElement.prototype, "_isOverflowing", void 0);
29238
+ __decorate([state()], PanTextElement.prototype, "_isHovering", void 0);
29239
+ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
29240
+ PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
29241
+ //#endregion
27231
29242
  //#region src/panel/panel-group.ts
27232
29243
  let PanelGroupElement = class PanelGroupElement extends i {
27233
29244
  constructor(..._args) {
@@ -36631,6 +38642,6 @@ function init() {
36631
38642
  console.log(`Init`);
36632
38643
  }
36633
38644
  //#endregion
36634
- export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, readBaseColour, registerFontIcon, registerIcon, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
38645
+ export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, readBaseColour, registerFontIcon, registerIcon, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
36635
38646
 
36636
38647
  //# sourceMappingURL=index.js.map