@ixfx/components 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/bundle/index.d.ts +1355 -526
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +3277 -384
  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 +2 -2
  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 +6 -2
  15. package/dist/crumbs.d.ts.map +1 -1
  16. package/dist/crumbs.js +13 -3
  17. package/dist/crumbs.js.map +1 -1
  18. package/dist/{data-grid-Db4QpnII.d.ts → data-grid-CtJ35v8L.d.ts} +59 -2
  19. package/dist/data-grid-CtJ35v8L.d.ts.map +1 -0
  20. package/dist/data-grid.d.ts +1 -1
  21. package/dist/data-grid.js +268 -14
  22. package/dist/data-grid.js.map +1 -1
  23. package/dist/detail-list-BS5dRDda.d.ts +175 -0
  24. package/dist/detail-list-BS5dRDda.d.ts.map +1 -0
  25. package/dist/detail-list.d.ts +3 -0
  26. package/dist/detail-list.js +1031 -0
  27. package/dist/detail-list.js.map +1 -0
  28. package/dist/grid-list-D-GbghSH.d.ts +321 -0
  29. package/dist/grid-list-D-GbghSH.d.ts.map +1 -0
  30. package/dist/grid-list.d.ts +3 -0
  31. package/dist/grid-list.js +994 -0
  32. package/dist/grid-list.js.map +1 -0
  33. package/dist/{grouped-item-lister-BWlC9S4e.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +3 -3
  34. package/dist/{grouped-item-lister-BWlC9S4e.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
  35. package/dist/grouped-item-lister.d.ts +1 -1
  36. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
  37. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  38. package/dist/icons.d.ts +2 -2
  39. package/dist/incr-search-DpbSJtgj.js +962 -0
  40. package/dist/incr-search-DpbSJtgj.js.map +1 -0
  41. package/dist/incr-search.d.ts +2 -2
  42. package/dist/incr-search.js +2 -3
  43. package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
  44. package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  45. package/dist/index-CBS4z1v-.d.ts +365 -0
  46. package/dist/index-CBS4z1v-.d.ts.map +1 -0
  47. package/dist/{index-DsjpDksO.d.ts → index-CS9yM8L-.d.ts} +2 -2
  48. package/dist/{index-DsjpDksO.d.ts.map → index-CS9yM8L-.d.ts.map} +1 -1
  49. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
  50. package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
  51. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
  52. package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
  53. package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
  54. package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
  55. package/dist/index.d.ts +188 -243
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +376 -1004
  58. package/dist/index.js.map +1 -1
  59. package/dist/interaction-Cr13azbM.js +3 -0
  60. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
  61. package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
  62. package/dist/labelled-radial-input.d.ts +2 -2
  63. package/dist/labelled-radial-input.d.ts.map +1 -1
  64. package/dist/labelled-range-input.d.ts +1 -1
  65. package/dist/labelled-slider-input.d.ts +1 -1
  66. package/dist/list-item-animation-DuiwgN3l.js +112 -0
  67. package/dist/list-item-animation-DuiwgN3l.js.map +1 -0
  68. package/dist/list-selection-controller-DTFI40li.js +264 -0
  69. package/dist/list-selection-controller-DTFI40li.js.map +1 -0
  70. package/dist/{list-selection-types-DSuRNWpx.d.ts → list-selection-types-B5ioL-dG.d.ts} +6 -1
  71. package/dist/list-selection-types-B5ioL-dG.d.ts.map +1 -0
  72. package/dist/{menu-DMzjkVnK.js → menu-Bg90Qr0B.js} +3 -3
  73. package/dist/{menu-DMzjkVnK.js.map → menu-Bg90Qr0B.js.map} +1 -1
  74. package/dist/{menu-item-D2S4i8u1.js → menu-item-Bx-Fs7jq.js} +3 -3
  75. package/dist/{menu-item-D2S4i8u1.js.map → menu-item-Bx-Fs7jq.js.map} +1 -1
  76. package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
  77. package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
  78. package/dist/menu.d.ts +2 -2
  79. package/dist/menu.js +2 -2
  80. package/dist/miller.d.ts +82 -4
  81. package/dist/miller.d.ts.map +1 -1
  82. package/dist/miller.js +329 -70
  83. package/dist/miller.js.map +1 -1
  84. package/dist/narrowed-text.js +3 -4
  85. package/dist/narrowed-text.js.map +1 -1
  86. package/dist/panel.d.ts +1 -1
  87. package/dist/plots.js +1 -1
  88. package/dist/select-horiz.js +2 -2
  89. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
  90. package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
  91. package/dist/split-layout.d.ts +1 -1
  92. package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
  93. package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
  94. package/dist/tabs.d.ts +1 -1
  95. package/dist/{tickled-controller-Ds1DMyG7.d.ts → tickled-controller-Bn3YdQsM.d.ts} +12 -1
  96. package/dist/{tickled-controller-Ds1DMyG7.d.ts.map → tickled-controller-Bn3YdQsM.d.ts.map} +1 -1
  97. package/dist/{tickled-controller-h9GmJ_bU.js → tickled-controller-C66DgJHl.js} +16 -1
  98. package/dist/tickled-controller-C66DgJHl.js.map +1 -0
  99. package/dist/{titlebar-B23MrIbI.d.ts → titlebar-Bj5pfdw_.d.ts} +2 -2
  100. package/dist/{titlebar-B23MrIbI.d.ts.map → titlebar-Bj5pfdw_.d.ts.map} +1 -1
  101. package/dist/titlebar.d.ts +1 -1
  102. package/dist/{tree-Ch4W8qXQ.js → tree-DAii2A1g.js} +190 -29
  103. package/dist/tree-DAii2A1g.js.map +1 -0
  104. package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
  105. package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
  106. package/dist/tree.d.ts +28 -4
  107. package/dist/tree.d.ts.map +1 -1
  108. package/dist/tree.js +1 -1
  109. package/dist/vertical-list-DwtCPy1b.d.ts +143 -0
  110. package/dist/vertical-list-DwtCPy1b.d.ts.map +1 -0
  111. package/dist/vertical-list.d.ts +3 -121
  112. package/dist/vertical-list.js +642 -1
  113. package/dist/vertical-list.js.map +1 -0
  114. package/dist/{xy-axis-DWizs0bt.js → xy-axis-VrX8dmvS.js} +2 -2
  115. package/dist/{xy-axis-DWizs0bt.js.map → xy-axis-VrX8dmvS.js.map} +1 -1
  116. package/docs-user/README.md +1 -0
  117. package/docs-user/data-grid.md +70 -2
  118. package/docs-user/detail-list.md +39 -1
  119. package/docs-user/grid-list.md +298 -0
  120. package/docs-user/incr-search.md +12 -2
  121. package/docs-user/index.json +7 -2
  122. package/docs-user/llms.txt +1 -0
  123. package/docs-user/tree.md +35 -1
  124. package/docs-user/vertical-list.md +58 -2
  125. package/llms.txt +1 -0
  126. package/package.json +17 -2
  127. package/dist/data-grid-Db4QpnII.d.ts.map +0 -1
  128. package/dist/element-search-Y8SRfPGM.js +0 -185
  129. package/dist/element-search-Y8SRfPGM.js.map +0 -1
  130. package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
  131. package/dist/incr-search-vCzuao-2.js +0 -137
  132. package/dist/incr-search-vCzuao-2.js.map +0 -1
  133. package/dist/index-Cu5i8zZY.d.ts +0 -160
  134. package/dist/index-Cu5i8zZY.d.ts.map +0 -1
  135. package/dist/interaction-D5XdKzbR.js +0 -2
  136. package/dist/list-selection-types-DSuRNWpx.d.ts.map +0 -1
  137. package/dist/tickled-controller-h9GmJ_bU.js.map +0 -1
  138. package/dist/tree-Ch4W8qXQ.js.map +0 -1
  139. package/dist/vertical-list-B4w_ksJk.js +0 -814
  140. package/dist/vertical-list-B4w_ksJk.js.map +0 -1
  141. package/dist/vertical-list.d.ts.map +0 -1
@@ -0,0 +1,3 @@
1
+ import "./tickled-controller-C66DgJHl.js";
2
+ import "./tickled-styles-BrAHEj1q.js";
3
+ import "./list-item-animation-DuiwgN3l.js";
@@ -33,9 +33,9 @@ declare abstract class LabelledInputBase extends LitElement {
33
33
  value: number;
34
34
  }>) => void;
35
35
  protected _syncValue(v: number): void;
36
- protected _renderEditableNumber(): import("lit").TemplateResult<1> | typeof nothing;
36
+ protected _renderEditableNumber(): typeof nothing | import("lit").TemplateResult<1>;
37
37
  updated(changedProperties: Map<string, unknown>): void;
38
38
  }
39
39
  //#endregion
40
40
  export { LabelledInputBase as t };
41
- //# sourceMappingURL=labelled-input-base-Dp_9KP0G.d.ts.map
41
+ //# sourceMappingURL=labelled-input-base-DiTme1JO.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"labelled-input-base-Dp_9KP0G.d.ts","names":[],"sources":["../src/mixins/labelled-input-base.ts"],"mappings":";;uBAIsB,0BAA0B;EAClC;EAEgB;EACA;EACA;EACA;EACA;EAC+B;EAEI;EACC;EAEjB;YAE5B;YACA;YACA,cAAc;UAEzB;UACA;oBAEU;MAEd,cAAc;MAId,UAAU,SAAS;gBAIT;EAIL;EAMA;YAMC,cAAc,SAAS;YAcvB,sBAAuB,GAAG;IAAc;;YAWxC,qBAAsB,GAAG;IAAc;;YAavC,qBAAsB,GAAG;IAAc;;YAYvC,WAAW;YAYX,uCAAqB,2BAAA;EAuBtB,QAAQ,mBAAmB"}
1
+ {"version":3,"file":"labelled-input-base-DiTme1JO.d.ts","names":[],"sources":["../src/mixins/labelled-input-base.ts"],"mappings":";;uBAIsB,0BAA0B;EAClC;EAEgB;EACA;EACA;EACA;EACA;EAC+B;EAEI;EACC;EAEjB;YAE5B;YACA;YACA,cAAc;UAEzB;UACA;oBAEU;MAEd,cAAc;MAId,UAAU,SAAS;gBAIT;EAIL;EAMA;YAMC,cAAc,SAAS;YAcvB,sBAAuB,GAAG;IAAc;;YAWxC,qBAAsB,GAAG;IAAc;;YAavC,qBAAsB,GAAG;IAAc;;YAYvC,WAAW;YAYX,gCAAqB,wBAAA;EAuBtB,QAAQ,mBAAmB"}
@@ -1,4 +1,4 @@
1
- import { t as LabelledInputBase } from "./labelled-input-base-Dp_9KP0G.js";
1
+ import { t as LabelledInputBase } from "./labelled-input-base-DiTme1JO.js";
2
2
  import { i as MarkerStyle, l as ValueMode } from "./radial-input-DU6t2hYO.js";
3
3
  import { CSSResultGroup, nothing } from "lit";
4
4
  //#region src/labelled-radial-input/labelled-radial-input.d.ts
@@ -19,7 +19,7 @@ declare class LabelledRadialInput extends LabelledInputBase {
19
19
  private _isWide;
20
20
  private _getEffectivePrecision;
21
21
  protected _handleResize(entries: ResizeObserverEntry[]): void;
22
- protected _renderEditableNumber(): import("lit").TemplateResult<1> | typeof nothing;
22
+ protected _renderEditableNumber(): typeof nothing | import("lit").TemplateResult<1>;
23
23
  render(): import("lit").TemplateResult<1>;
24
24
  private _getTooltipValue;
25
25
  static styles: CSSResultGroup;
@@ -1 +1 @@
1
- {"version":3,"file":"labelled-radial-input.d.ts","names":[],"sources":["../src/labelled-radial-input/labelled-radial-input.ts"],"mappings":";;;;cA0Da,4BAA4B;WAC9B;EAGA;EACA;EAEmB;EACA;EAC2B,aAAa;EACvC;EACwB,WAAW;EACnC;EACA;EAEoC;;EAGpC;UAEZ;UAIT;YAeW,cAAc,SAAS;YAMvB,uCAAqB,2BAAA;EA0B/B,wBAAM;UA6BP;SAYD,QAAQ;;QA0ET;YACI;IACR,8BAA8B"}
1
+ {"version":3,"file":"labelled-radial-input.d.ts","names":[],"sources":["../src/labelled-radial-input/labelled-radial-input.ts"],"mappings":";;;;cA0Da,4BAA4B;WAC9B;EAGA;EACA;EAEmB;EACA;EAC2B,aAAa;EACvC;EACwB,WAAW;EACnC;EACA;EAEoC;;EAGpC;UAEZ;UAIT;YAeW,cAAc,SAAS;YAMvB,gCAAqB,wBAAA;EA0B/B,wBAAM;UA6BP;SAYD,QAAQ;;QA0ET;YACI;IACR,8BAA8B"}
@@ -1,2 +1,2 @@
1
- import { t as LabelledRangeInput } from "./index-BUB5SICW.js";
1
+ import { t as LabelledRangeInput } from "./index-CoqtMSd9.js";
2
2
  export { LabelledRangeInput };
@@ -1,2 +1,2 @@
1
- import { t as LabelledSliderInput } from "./index-CJ5fshD3.js";
1
+ import { t as LabelledSliderInput } from "./index-UvnoZUAo.js";
2
2
  export { LabelledSliderInput };
@@ -0,0 +1,112 @@
1
+ import { css } from "lit";
2
+ //#region src/interaction/list-item-animation.ts
3
+ /**
4
+ * Shared insertion / removal animation for the list & tree components.
5
+ *
6
+ * Two flavours, because the components split into two layout families:
7
+ *
8
+ * - **Flow layouts** (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-tree-*`) —
9
+ * rows sit in normal flow, so a new row can collapse in from zero height and a
10
+ * leaving row can collapse back to zero, and the rows around it slide to make
11
+ * or close the gap for free. Use {@link animateListItemIn} /
12
+ * {@link animateListItemOut} (Web Animations API, exact measured sizes).
13
+ *
14
+ * - **Virtualised layouts** (`ixfx-grid-list`, `ixfx-data-grid`) — rows are
15
+ * absolutely positioned (`transform` / `top`), so collapsing a row's box moves
16
+ * nothing. Those components instead fade the entering / leaving row with the
17
+ * {@link listItemFadeKeyframes} keyframes and slide the survivors by
18
+ * transitioning their position for a short window after the change.
19
+ *
20
+ * Everything here is a no-op under `prefers-reduced-motion: reduce`.
21
+ */
22
+ /** Default duration (ms) for both flavours; overridable per call / per component. */
23
+ const LIST_ITEM_MOTION_MS = 200;
24
+ function prefersReducedMotion() {
25
+ return typeof window !== `undefined` && typeof window.matchMedia === `function` && window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
26
+ }
27
+ /**
28
+ * Opacity keyframes for the virtualised components. Each one adds its own
29
+ * one-line rule binding `.<row>.ixfx-item-entering` / `.ixfx-item-leaving` to
30
+ * these, plus a `prefers-reduced-motion` guard on its own selectors.
31
+ */
32
+ const listItemFadeKeyframes = css`
33
+ @keyframes ixfx-list-item-in {
34
+ from { opacity: 0; }
35
+ to { opacity: 1; }
36
+ }
37
+ @keyframes ixfx-list-item-out {
38
+ from { opacity: 1; }
39
+ to { opacity: 0; }
40
+ }
41
+ `;
42
+ function measure(el, axis) {
43
+ const rect = el.getBoundingClientRect();
44
+ return axis === `inline` ? rect.width : rect.height;
45
+ }
46
+ function resolveDuration(el, explicit) {
47
+ if (explicit !== void 0) return explicit;
48
+ const raw = getComputedStyle(el).getPropertyValue(`--ixfx-list-item-move-duration`).trim();
49
+ if (raw) {
50
+ const n = Number.parseFloat(raw);
51
+ if (!Number.isNaN(n)) return raw.endsWith(`ms`) ? n : raw.endsWith(`s`) ? n * 1e3 : n;
52
+ }
53
+ return 200;
54
+ }
55
+ function collapsedFrame(axis) {
56
+ const prop = axis === `inline` ? `width` : `height`;
57
+ const minProp = axis === `inline` ? `minWidth` : `minHeight`;
58
+ return {
59
+ [prop]: `0px`,
60
+ [minProp]: `0px`,
61
+ opacity: 0,
62
+ overflow: `hidden`
63
+ };
64
+ }
65
+ function fullFrame(size, axis) {
66
+ const prop = axis === `inline` ? `width` : `height`;
67
+ const minProp = axis === `inline` ? `minWidth` : `minHeight`;
68
+ return {
69
+ [prop]: `${size}px`,
70
+ [minProp]: `0px`,
71
+ opacity: 1,
72
+ overflow: `hidden`
73
+ };
74
+ }
75
+ /**
76
+ * Collapse + fade `el` in from nothing to its natural size. Call it just after
77
+ * the element has been inserted (it must already be laid out at full size so it
78
+ * can be measured). No-op under reduced motion.
79
+ */
80
+ function animateListItemIn(el, options = {}) {
81
+ if (prefersReducedMotion()) return void 0;
82
+ const { axis = `block`, easing = `ease` } = options;
83
+ const duration = resolveDuration(el, options.duration);
84
+ const size = measure(el, axis);
85
+ if (size <= 0) return void 0;
86
+ return el.animate([collapsedFrame(axis), fullFrame(size, axis)], {
87
+ duration,
88
+ easing
89
+ });
90
+ }
91
+ /**
92
+ * Collapse + fade `el` out. Resolves once the animation finishes (or straight
93
+ * away under reduced motion); the caller detaches the element after. The final
94
+ * collapsed state is held (`fill: forwards`) so there's no flash back to full
95
+ * size in the gap before removal.
96
+ */
97
+ function animateListItemOut(el, options = {}) {
98
+ if (prefersReducedMotion()) return Promise.resolve();
99
+ const { axis = `block`, easing = `ease` } = options;
100
+ const duration = resolveDuration(el, options.duration);
101
+ const size = measure(el, axis);
102
+ if (size <= 0) return Promise.resolve();
103
+ return el.animate([fullFrame(size, axis), collapsedFrame(axis)], {
104
+ duration,
105
+ easing,
106
+ fill: `forwards`
107
+ }).finished.then(() => void 0, () => void 0);
108
+ }
109
+ //#endregion
110
+ export { prefersReducedMotion as a, listItemFadeKeyframes as i, animateListItemIn as n, animateListItemOut as r, LIST_ITEM_MOTION_MS as t };
111
+
112
+ //# sourceMappingURL=list-item-animation-DuiwgN3l.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-item-animation-DuiwgN3l.js","names":[],"sources":["../src/interaction/list-item-animation.ts"],"sourcesContent":["import { css } from 'lit';\n\n/**\n * Shared insertion / removal animation for the list & tree components.\n *\n * Two flavours, because the components split into two layout families:\n *\n * - **Flow layouts** (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-tree-*`) —\n * rows sit in normal flow, so a new row can collapse in from zero height and a\n * leaving row can collapse back to zero, and the rows around it slide to make\n * or close the gap for free. Use {@link animateListItemIn} /\n * {@link animateListItemOut} (Web Animations API, exact measured sizes).\n *\n * - **Virtualised layouts** (`ixfx-grid-list`, `ixfx-data-grid`) — rows are\n * absolutely positioned (`transform` / `top`), so collapsing a row's box moves\n * nothing. Those components instead fade the entering / leaving row with the\n * {@link listItemFadeKeyframes} keyframes and slide the survivors by\n * transitioning their position for a short window after the change.\n *\n * Everything here is a no-op under `prefers-reduced-motion: reduce`.\n */\n\n/** Default duration (ms) for both flavours; overridable per call / per component. */\nexport const LIST_ITEM_MOTION_MS = 200;\n\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window !== `undefined`\n && typeof window.matchMedia === `function`\n && window.matchMedia(`(prefers-reduced-motion: reduce)`).matches\n );\n}\n\n/**\n * Opacity keyframes for the virtualised components. Each one adds its own\n * one-line rule binding `.<row>.ixfx-item-entering` / `.ixfx-item-leaving` to\n * these, plus a `prefers-reduced-motion` guard on its own selectors.\n */\nexport const listItemFadeKeyframes = css`\n @keyframes ixfx-list-item-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n @keyframes ixfx-list-item-out {\n from { opacity: 1; }\n to { opacity: 0; }\n }\n`;\n\nexport interface ListItemMotionOptions {\n /**\n * Animation duration in ms. When omitted, the element's\n * `--ixfx-list-item-move-duration` custom property is used if set (accepts\n * `ms` or `s`), otherwise {@link LIST_ITEM_MOTION_MS}.\n */\n duration?: number;\n /** Which axis collapses — `block` (height, default) or `inline` (width). */\n axis?: `block` | `inline`;\n easing?: string;\n}\n\nfunction measure(el: HTMLElement, axis: `block` | `inline`): number {\n const rect = el.getBoundingClientRect();\n return axis === `inline` ? rect.width : rect.height;\n}\n\nfunction resolveDuration(el: HTMLElement, explicit?: number): number {\n if (explicit !== undefined)\n return explicit;\n const raw = getComputedStyle(el).getPropertyValue(`--ixfx-list-item-move-duration`).trim();\n if (raw) {\n const n = Number.parseFloat(raw);\n if (!Number.isNaN(n))\n return raw.endsWith(`ms`) ? n : raw.endsWith(`s`) ? n * 1000 : n;\n }\n return LIST_ITEM_MOTION_MS;\n}\n\nfunction collapsedFrame(axis: `block` | `inline`): Keyframe {\n const prop = axis === `inline` ? `width` : `height`;\n const minProp = axis === `inline` ? `minWidth` : `minHeight`;\n return { [prop]: `0px`, [minProp]: `0px`, opacity: 0, overflow: `hidden` };\n}\n\nfunction fullFrame(size: number, axis: `block` | `inline`): Keyframe {\n const prop = axis === `inline` ? `width` : `height`;\n const minProp = axis === `inline` ? `minWidth` : `minHeight`;\n return { [prop]: `${size}px`, [minProp]: `0px`, opacity: 1, overflow: `hidden` };\n}\n\n/**\n * Collapse + fade `el` in from nothing to its natural size. Call it just after\n * the element has been inserted (it must already be laid out at full size so it\n * can be measured). No-op under reduced motion.\n */\nexport function animateListItemIn(el: HTMLElement, options: ListItemMotionOptions = {}): Animation | undefined {\n if (prefersReducedMotion())\n return undefined;\n const { axis = `block`, easing = `ease` } = options;\n const duration = resolveDuration(el, options.duration);\n const size = measure(el, axis);\n if (size <= 0)\n return undefined;\n return el.animate([collapsedFrame(axis), fullFrame(size, axis)], { duration, easing });\n}\n\n/**\n * Collapse + fade `el` out. Resolves once the animation finishes (or straight\n * away under reduced motion); the caller detaches the element after. The final\n * collapsed state is held (`fill: forwards`) so there's no flash back to full\n * size in the gap before removal.\n */\nexport function animateListItemOut(el: HTMLElement, options: ListItemMotionOptions = {}): Promise<void> {\n if (prefersReducedMotion())\n return Promise.resolve();\n const { axis = `block`, easing = `ease` } = options;\n const duration = resolveDuration(el, options.duration);\n const size = measure(el, axis);\n if (size <= 0)\n return Promise.resolve();\n const anim = el.animate([fullFrame(size, axis), collapsedFrame(axis)], {\n duration,\n easing,\n fill: `forwards`,\n });\n return anim.finished.then(() => undefined, () => undefined);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,sBAAsB;AAEnC,SAAgB,uBAAgC;CAC9C,OACE,OAAO,WAAW,eACf,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kCAAkC,CAAC,CAAC;AAE7D;;;;;;AAOA,MAAa,wBAAwB,GAAG;;;;;;;;;;AAuBxC,SAAS,QAAQ,IAAiB,MAAkC;CAClE,MAAM,OAAO,GAAG,sBAAsB;CACtC,OAAO,SAAS,WAAW,KAAK,QAAQ,KAAK;AAC/C;AAEA,SAAS,gBAAgB,IAAiB,UAA2B;CACnE,IAAI,aAAa,KAAA,GACf,OAAO;CACT,MAAM,MAAM,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,gCAAgC,CAAC,CAAC,KAAK;CACzF,IAAI,KAAK;EACP,MAAM,IAAI,OAAO,WAAW,GAAG;EAC/B,IAAI,CAAC,OAAO,MAAM,CAAC,GACjB,OAAO,IAAI,SAAS,IAAI,IAAI,IAAI,IAAI,SAAS,GAAG,IAAI,IAAI,MAAO;CACnE;CACA,OAAA;AACF;AAEA,SAAS,eAAe,MAAoC;CAC1D,MAAM,OAAO,SAAS,WAAW,UAAU;CAC3C,MAAM,UAAU,SAAS,WAAW,aAAa;CACjD,OAAO;GAAG,OAAO;GAAQ,UAAU;EAAO,SAAS;EAAG,UAAU;CAAS;AAC3E;AAEA,SAAS,UAAU,MAAc,MAAoC;CACnE,MAAM,OAAO,SAAS,WAAW,UAAU;CAC3C,MAAM,UAAU,SAAS,WAAW,aAAa;CACjD,OAAO;GAAG,OAAO,GAAG,KAAK;GAAM,UAAU;EAAO,SAAS;EAAG,UAAU;CAAS;AACjF;;;;;;AAOA,SAAgB,kBAAkB,IAAiB,UAAiC,CAAC,GAA0B;CAC7G,IAAI,qBAAqB,GACvB,OAAO,KAAA;CACT,MAAM,EAAE,OAAO,SAAS,SAAS,WAAW;CAC5C,MAAM,WAAW,gBAAgB,IAAI,QAAQ,QAAQ;CACrD,MAAM,OAAO,QAAQ,IAAI,IAAI;CAC7B,IAAI,QAAQ,GACV,OAAO,KAAA;CACT,OAAO,GAAG,QAAQ,CAAC,eAAe,IAAI,GAAG,UAAU,MAAM,IAAI,CAAC,GAAG;EAAE;EAAU;CAAO,CAAC;AACvF;;;;;;;AAQA,SAAgB,mBAAmB,IAAiB,UAAiC,CAAC,GAAkB;CACtG,IAAI,qBAAqB,GACvB,OAAO,QAAQ,QAAQ;CACzB,MAAM,EAAE,OAAO,SAAS,SAAS,WAAW;CAC5C,MAAM,WAAW,gBAAgB,IAAI,QAAQ,QAAQ;CACrD,MAAM,OAAO,QAAQ,IAAI,IAAI;CAC7B,IAAI,QAAQ,GACV,OAAO,QAAQ,QAAQ;CAMzB,OALa,GAAG,QAAQ,CAAC,UAAU,MAAM,IAAI,GAAG,eAAe,IAAI,CAAC,GAAG;EACrE;EACA;EACA,MAAM;CACR,CACU,CAAC,CAAC,SAAS,WAAW,KAAA,SAAiB,KAAA,CAAS;AAC5D"}
@@ -0,0 +1,264 @@
1
+ //#region src/interaction/list-selection-controller.ts
2
+ /** Whether the interaction mode allows building a multi-item selection. */
3
+ function isMultiMode(mode) {
4
+ return mode === `checked` || mode === `sticky` || mode === `vscode` || mode === `standard`;
5
+ }
6
+ /**
7
+ * Generic selection state machine shared by list components.
8
+ *
9
+ * Owns the selected set plus the range-selection anchor, the selection
10
+ * primitives (`replace` / `toggle` / `rangeTo` / `clear` / `selectAll` /
11
+ * `selectMany` / `deselect`) and the per-interactionMode gesture routers
12
+ * (`handleClick` / `handleSpace`).
13
+ *
14
+ * Identity is generic: both `ixfx-vertical-list` and `ixfx-detail-list` use
15
+ * `ListSelectionController<Element>`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const selection = new ListSelectionController<Element>({
20
+ * getOrderedItems: () => this.getVisibleItems(),
21
+ * isSelectable: el => !el.hasAttribute(`not-checked`),
22
+ * getModes: () => ({ selectionMode: this.selectionMode, interactionMode: this.interactionMode }),
23
+ * onChange: (selected, previous) => {
24
+ * this.syncItemStates();
25
+ * this.dispatchEvent(new CustomEvent(`list-select`, { detail: { selected, previous } }));
26
+ * },
27
+ * });
28
+ * ```
29
+ */
30
+ var ListSelectionController = class {
31
+ #config;
32
+ #selected = /* @__PURE__ */ new Set();
33
+ #anchor;
34
+ constructor(config) {
35
+ this.#config = config;
36
+ }
37
+ /** The current selection set (read-only). */
38
+ get selected() {
39
+ return this.#selected;
40
+ }
41
+ /**
42
+ * The current selection as stable string keys (read-only). Empty unless a
43
+ * `getKey` was supplied in config. Items whose `getKey` returns `undefined`
44
+ * are omitted.
45
+ */
46
+ get selectedKeys() {
47
+ const getKey = this.#config.getKey;
48
+ const out = /* @__PURE__ */ new Set();
49
+ if (getKey) for (const item of this.#selected) {
50
+ const key = getKey(item);
51
+ if (key !== void 0) out.add(key);
52
+ }
53
+ return out;
54
+ }
55
+ /** Anchor for range (shift) selection — the last plainly-selected item. */
56
+ get anchor() {
57
+ return this.#anchor;
58
+ }
59
+ /** Whether "select all" (Cmd/Ctrl+A) is currently permitted. */
60
+ canSelectAll() {
61
+ return this.canSelectMultiple() && this.#config.getModes().selectionMode !== `none`;
62
+ }
63
+ /** Whether a multi-item selection can be built in the current modes. */
64
+ canSelectMultiple() {
65
+ const { selectionMode, interactionMode } = this.#config.getModes();
66
+ return selectionMode === `multiple` || isMultiMode(interactionMode);
67
+ }
68
+ /** Replace the entire selection with a single item; updates the anchor. */
69
+ replace(item) {
70
+ if (this.#config.getModes().selectionMode === `none`) return;
71
+ if (!this.#config.isSelectable(item)) return;
72
+ this.#anchor = item;
73
+ this.#apply(/* @__PURE__ */ new Set([item]));
74
+ }
75
+ /** Toggle a single item in/out of the selection without replacing it. Anchor unchanged. */
76
+ toggle(item) {
77
+ const { selectionMode, interactionMode } = this.#config.getModes();
78
+ if (!this.#config.isSelectable(item)) return;
79
+ if (selectionMode === `none` && interactionMode !== `checked` && interactionMode !== `sticky`) return;
80
+ const next = new Set(this.#selected);
81
+ if (next.has(item)) next.delete(item);
82
+ else {
83
+ if (!this.canSelectMultiple()) next.clear();
84
+ next.add(item);
85
+ }
86
+ this.#apply(next);
87
+ }
88
+ /** Select the contiguous ordered range from the anchor to `item`. Anchor unchanged. */
89
+ rangeTo(item) {
90
+ if (this.#config.getModes().selectionMode === `none`) return;
91
+ if (this.#anchor === void 0) {
92
+ this.replace(item);
93
+ return;
94
+ }
95
+ const ordered = this.#config.getOrderedItems();
96
+ const anchorIdx = ordered.indexOf(this.#anchor);
97
+ const targetIdx = ordered.indexOf(item);
98
+ if (anchorIdx === -1 || targetIdx === -1) {
99
+ this.replace(item);
100
+ return;
101
+ }
102
+ const start = Math.min(anchorIdx, targetIdx);
103
+ const end = Math.max(anchorIdx, targetIdx);
104
+ this.#apply(new Set(ordered.slice(start, end + 1).filter((t) => this.#config.isSelectable(t))));
105
+ }
106
+ /** Clear the selection. No change event when the selection is already empty. */
107
+ clear() {
108
+ if (this.#selected.size === 0) return;
109
+ this.#apply(/* @__PURE__ */ new Set());
110
+ }
111
+ /** Select every ordered item that passes `isSelectable`. */
112
+ selectAll() {
113
+ const ordered = this.#config.getOrderedItems();
114
+ if (ordered.length === 0) return;
115
+ const next = new Set(ordered.filter((t) => this.#config.isSelectable(t)));
116
+ if (next.size === 0) return;
117
+ this.#apply(next);
118
+ }
119
+ /**
120
+ * Replace the selection with many items at once. Requires
121
+ * `selectionMode="multiple"`, throws otherwise. Updates the anchor.
122
+ */
123
+ selectMany(items) {
124
+ const { selectionMode } = this.#config.getModes();
125
+ if (selectionMode !== `multiple`) throw new Error(`selectMany requires selectionMode="multiple" (current: "${selectionMode}")`);
126
+ const next = new Set([...items].filter((t) => this.#config.isSelectable(t)));
127
+ if (next.size === 0) return;
128
+ this.#anchor = [...next].at(-1);
129
+ this.#apply(next);
130
+ }
131
+ /** Remove a single item from the selection. */
132
+ deselect(item) {
133
+ if (!this.#selected.has(item)) return;
134
+ const next = new Set(this.#selected);
135
+ next.delete(item);
136
+ this.#apply(next);
137
+ }
138
+ /**
139
+ * Whether the current modes permit a selection of more than one item.
140
+ * Stricter than `canSelectMultiple()`: `standard` / `implicit` only count
141
+ * as multi when `selectionMode="multiple"`.
142
+ */
143
+ #effectiveMulti() {
144
+ const { selectionMode, interactionMode } = this.#config.getModes();
145
+ return selectionMode === `multiple` || interactionMode === `checked` || interactionMode === `sticky` || interactionMode === `vscode`;
146
+ }
147
+ /**
148
+ * Replace the selection with the items resolved from `keys`. Keys that
149
+ * `resolve` cannot map, or that resolve to a non-selectable item, are
150
+ * ignored. `selectionMode="none"` is a no-op. When multi-select is not
151
+ * available, only the last resolvable key is kept. `onChange` fires once.
152
+ */
153
+ selectKeys(keys, resolve) {
154
+ if (this.#config.getModes().selectionMode === `none`) return;
155
+ let next = /* @__PURE__ */ new Set();
156
+ for (const key of new Set(keys)) {
157
+ const item = resolve(key);
158
+ if (item !== void 0 && this.#config.isSelectable(item)) next.add(item);
159
+ }
160
+ if (!this.#effectiveMulti() && next.size > 1) {
161
+ const last = [...next].at(-1);
162
+ next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
163
+ }
164
+ this.#anchor = [...next].at(-1);
165
+ this.#apply(next);
166
+ }
167
+ /**
168
+ * Add the items resolved from `keys` to the selection (multi-select only;
169
+ * a no-op otherwise). `onChange` fires once if anything changed.
170
+ */
171
+ addKeys(keys, resolve) {
172
+ if (!this.#effectiveMulti()) return;
173
+ const next = new Set(this.#selected);
174
+ let changed = false;
175
+ for (const key of new Set(keys)) {
176
+ const item = resolve(key);
177
+ if (item !== void 0 && !next.has(item) && this.#config.isSelectable(item)) {
178
+ next.add(item);
179
+ changed = true;
180
+ }
181
+ }
182
+ if (changed) this.#apply(next);
183
+ }
184
+ /**
185
+ * Remove the items resolved from `keys` from the selection. `onChange`
186
+ * fires once if anything changed.
187
+ */
188
+ removeKeys(keys, resolve) {
189
+ if (this.#selected.size === 0) return;
190
+ const next = new Set(this.#selected);
191
+ let changed = false;
192
+ for (const key of new Set(keys)) {
193
+ const item = resolve(key);
194
+ if (item !== void 0 && next.delete(item)) changed = true;
195
+ }
196
+ if (changed) this.#apply(next);
197
+ }
198
+ /** Route a click on `item` according to the current interaction mode. */
199
+ handleClick(item, gesture) {
200
+ const { selectionMode, interactionMode: mode } = this.#config.getModes();
201
+ if (mode === `manual`) return;
202
+ if (selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
203
+ switch (mode) {
204
+ case `implicit`:
205
+ this.replace(item);
206
+ break;
207
+ case `standard`:
208
+ case `vscode`:
209
+ if (gesture.ctrlKey || gesture.metaKey) {
210
+ if (selectionMode === `multiple` || mode === `vscode`) this.toggle(item);
211
+ } else if (gesture.shiftKey && (selectionMode === `multiple` || mode === `vscode`)) this.rangeTo(item);
212
+ else this.replace(item);
213
+ break;
214
+ case `checked`: break;
215
+ case `sticky`: this.toggle(item);
216
+ }
217
+ }
218
+ /** Route a Space keypress on `item` according to the current interaction mode. */
219
+ handleSpace(item) {
220
+ const mode = this.#config.getModes().interactionMode;
221
+ if (mode === `manual`) return;
222
+ if (mode === `implicit`) this.replace(item);
223
+ else this.toggle(item);
224
+ }
225
+ /** Drop the range-selection anchor (e.g. when the interaction mode changes). */
226
+ clearAnchor() {
227
+ this.#anchor = void 0;
228
+ }
229
+ /**
230
+ * Re-key the selection across an identity boundary — e.g. when items are
231
+ * wrapped/unwrapped and the selected Element references change. Silent:
232
+ * no `onChange` is fired.
233
+ */
234
+ remap(fn) {
235
+ if (this.#selected.size === 0) return;
236
+ this.#selected = new Set([...this.#selected].map(fn));
237
+ }
238
+ /**
239
+ * Hard reset: empty the selection and clear the anchor without firing
240
+ * `onChange` (e.g. when all items are removed).
241
+ */
242
+ reset() {
243
+ this.#selected = /* @__PURE__ */ new Set();
244
+ this.#anchor = void 0;
245
+ }
246
+ /**
247
+ * Replace the selection and anchor silently (no `onChange`). Used to
248
+ * re-key the selection when items are rebuilt from data — e.g. matching
249
+ * new elements to previously selected keys.
250
+ */
251
+ seed(items, anchor) {
252
+ this.#selected = new Set(items);
253
+ this.#anchor = anchor;
254
+ }
255
+ #apply(next) {
256
+ const previous = this.#selected;
257
+ this.#selected = next;
258
+ this.#config.onChange(next, previous);
259
+ }
260
+ };
261
+ //#endregion
262
+ export { isMultiMode as n, ListSelectionController as t };
263
+
264
+ //# sourceMappingURL=list-selection-controller-DTFI40li.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-selection-controller-DTFI40li.js","names":["#config","#selected","#anchor","#apply","#effectiveMulti"],"sources":["../src/interaction/list-selection-controller.ts"],"sourcesContent":["import type { ListInteractionMode, ListSelectionMode } from './list-selection-types.js';\n\n/** Modifier state needed to route a click gesture per interaction mode. */\nexport type ListSelectionGesture = {\n readonly ctrlKey: boolean;\n readonly metaKey: boolean;\n readonly shiftKey: boolean;\n};\n\nexport type ListSelectionModes = {\n readonly selectionMode: ListSelectionMode;\n readonly interactionMode: ListInteractionMode;\n};\n\n/**\n * Configuration supplied by the host component.\n *\n * The controller owns selection *state* only — everything DOM- or\n * component-specific is expressed through these callbacks.\n */\nexport type ListSelectionControllerConfig<T> = {\n /** Visible items in navigation order — used for range selection and select-all. */\n readonly getOrderedItems: () => readonly T[];\n /** Whether an item may join the selection (e.g. reject `not-checked` rows). */\n readonly isSelectable: (item: T) => boolean;\n /** Current modes; read on every gesture so property changes apply immediately. */\n readonly getModes: () => ListSelectionModes;\n /**\n * Fired after every selection change. The host syncs DOM state\n * (`data-selected` / `data-checked`), dispatches `list-select`, and\n * requests an update.\n */\n readonly onChange: (selected: ReadonlySet<T>, previous: ReadonlySet<T>) => void;\n /**\n * Optional: derive a stable string key for an item. Enables `selectedKeys`\n * and `selectKeys()`. When omitted, `selectedKeys` is always empty.\n */\n readonly getKey?: (item: T) => string | undefined;\n};\n\n/** Whether the interaction mode allows building a multi-item selection. */\nexport function isMultiMode(mode: ListInteractionMode): boolean {\n return mode === `checked` || mode === `sticky` || mode === `vscode` || mode === `standard`;\n}\n\n/**\n * Generic selection state machine shared by list components.\n *\n * Owns the selected set plus the range-selection anchor, the selection\n * primitives (`replace` / `toggle` / `rangeTo` / `clear` / `selectAll` /\n * `selectMany` / `deselect`) and the per-interactionMode gesture routers\n * (`handleClick` / `handleSpace`).\n *\n * Identity is generic: both `ixfx-vertical-list` and `ixfx-detail-list` use\n * `ListSelectionController<Element>`.\n *\n * @example\n * ```ts\n * const selection = new ListSelectionController<Element>({\n * getOrderedItems: () => this.getVisibleItems(),\n * isSelectable: el => !el.hasAttribute(`not-checked`),\n * getModes: () => ({ selectionMode: this.selectionMode, interactionMode: this.interactionMode }),\n * onChange: (selected, previous) => {\n * this.syncItemStates();\n * this.dispatchEvent(new CustomEvent(`list-select`, { detail: { selected, previous } }));\n * },\n * });\n * ```\n */\nexport class ListSelectionController<T> {\n readonly #config: ListSelectionControllerConfig<T>;\n #selected: ReadonlySet<T> = new Set();\n #anchor: T | undefined;\n\n constructor(config: ListSelectionControllerConfig<T>) {\n this.#config = config;\n }\n\n /** The current selection set (read-only). */\n get selected(): ReadonlySet<T> {\n return this.#selected;\n }\n\n /**\n * The current selection as stable string keys (read-only). Empty unless a\n * `getKey` was supplied in config. Items whose `getKey` returns `undefined`\n * are omitted.\n */\n get selectedKeys(): ReadonlySet<string> {\n const getKey = this.#config.getKey;\n const out = new Set<string>();\n if (getKey) {\n for (const item of this.#selected) {\n const key = getKey(item);\n if (key !== undefined) {\n out.add(key);\n }\n }\n }\n return out;\n }\n\n /** Anchor for range (shift) selection — the last plainly-selected item. */\n get anchor(): T | undefined {\n return this.#anchor;\n }\n\n /** Whether \"select all\" (Cmd/Ctrl+A) is currently permitted. */\n canSelectAll(): boolean {\n return this.canSelectMultiple() && this.#config.getModes().selectionMode !== `none`;\n }\n\n /** Whether a multi-item selection can be built in the current modes. */\n canSelectMultiple(): boolean {\n const { selectionMode, interactionMode } = this.#config.getModes();\n return selectionMode === `multiple` || isMultiMode(interactionMode);\n }\n\n // --- Selection primitives ---\n\n /** Replace the entire selection with a single item; updates the anchor. */\n replace(item: T): void {\n if (this.#config.getModes().selectionMode === `none`)\n return;\n if (!this.#config.isSelectable(item))\n return;\n this.#anchor = item;\n this.#apply(new Set([item]));\n }\n\n /** Toggle a single item in/out of the selection without replacing it. Anchor unchanged. */\n toggle(item: T): void {\n const { selectionMode, interactionMode } = this.#config.getModes();\n if (!this.#config.isSelectable(item))\n return;\n // checked/vscode/sticky modes imply multiple selection capacity\n if (selectionMode === `none` && interactionMode !== `checked` && interactionMode !== `sticky`)\n return;\n\n const next = new Set(this.#selected);\n if (next.has(item)) {\n next.delete(item);\n } else {\n if (!this.canSelectMultiple())\n next.clear();\n next.add(item);\n }\n this.#apply(next);\n }\n\n /** Select the contiguous ordered range from the anchor to `item`. Anchor unchanged. */\n rangeTo(item: T): void {\n if (this.#config.getModes().selectionMode === `none`)\n return;\n if (this.#anchor === undefined) {\n this.replace(item);\n return;\n }\n const ordered = this.#config.getOrderedItems();\n const anchorIdx = ordered.indexOf(this.#anchor);\n const targetIdx = ordered.indexOf(item);\n if (anchorIdx === -1 || targetIdx === -1) {\n this.replace(item);\n return;\n }\n const start = Math.min(anchorIdx, targetIdx);\n const end = Math.max(anchorIdx, targetIdx);\n this.#apply(new Set(ordered.slice(start, end + 1).filter(t => this.#config.isSelectable(t))));\n }\n\n /** Clear the selection. No change event when the selection is already empty. */\n clear(): void {\n if (this.#selected.size === 0)\n return;\n this.#apply(new Set());\n }\n\n /** Select every ordered item that passes `isSelectable`. */\n selectAll(): void {\n const ordered = this.#config.getOrderedItems();\n if (ordered.length === 0)\n return;\n const next = new Set(ordered.filter(t => this.#config.isSelectable(t)));\n if (next.size === 0)\n return;\n this.#apply(next);\n }\n\n /**\n * Replace the selection with many items at once. Requires\n * `selectionMode=\"multiple\"`, throws otherwise. Updates the anchor.\n */\n selectMany(items: Iterable<T>): void {\n const { selectionMode } = this.#config.getModes();\n if (selectionMode !== `multiple`) {\n throw new Error(`selectMany requires selectionMode=\"multiple\" (current: \"${selectionMode}\")`);\n }\n const next = new Set([...items].filter(t => this.#config.isSelectable(t)));\n if (next.size === 0)\n return;\n this.#anchor = [...next].at(-1);\n this.#apply(next);\n }\n\n /** Remove a single item from the selection. */\n deselect(item: T): void {\n if (!this.#selected.has(item))\n return;\n const next = new Set(this.#selected);\n next.delete(item);\n this.#apply(next);\n }\n\n // --- Key-based selection ---\n\n /**\n * Whether the current modes permit a selection of more than one item.\n * Stricter than `canSelectMultiple()`: `standard` / `implicit` only count\n * as multi when `selectionMode=\"multiple\"`.\n */\n #effectiveMulti(): boolean {\n const { selectionMode, interactionMode } = this.#config.getModes();\n return selectionMode === `multiple`\n || interactionMode === `checked`\n || interactionMode === `sticky`\n || interactionMode === `vscode`;\n }\n\n /**\n * Replace the selection with the items resolved from `keys`. Keys that\n * `resolve` cannot map, or that resolve to a non-selectable item, are\n * ignored. `selectionMode=\"none\"` is a no-op. When multi-select is not\n * available, only the last resolvable key is kept. `onChange` fires once.\n */\n selectKeys(keys: Iterable<string>, resolve: (key: string) => T | undefined): void {\n if (this.#config.getModes().selectionMode === `none`)\n return;\n let next = new Set<T>();\n for (const key of new Set(keys)) {\n const item = resolve(key);\n if (item !== undefined && this.#config.isSelectable(item)) {\n next.add(item);\n }\n }\n if (!this.#effectiveMulti() && next.size > 1) {\n const last = [...next].at(-1);\n next = last === undefined ? new Set() : new Set([last]);\n }\n this.#anchor = [...next].at(-1);\n this.#apply(next);\n }\n\n /**\n * Add the items resolved from `keys` to the selection (multi-select only;\n * a no-op otherwise). `onChange` fires once if anything changed.\n */\n addKeys(keys: Iterable<string>, resolve: (key: string) => T | undefined): void {\n if (!this.#effectiveMulti())\n return;\n const next = new Set(this.#selected);\n let changed = false;\n for (const key of new Set(keys)) {\n const item = resolve(key);\n if (item !== undefined && !next.has(item) && this.#config.isSelectable(item)) {\n next.add(item);\n changed = true;\n }\n }\n if (changed) {\n this.#apply(next);\n }\n }\n\n /**\n * Remove the items resolved from `keys` from the selection. `onChange`\n * fires once if anything changed.\n */\n removeKeys(keys: Iterable<string>, resolve: (key: string) => T | undefined): void {\n if (this.#selected.size === 0)\n return;\n const next = new Set(this.#selected);\n let changed = false;\n for (const key of new Set(keys)) {\n const item = resolve(key);\n if (item !== undefined && next.delete(item)) {\n changed = true;\n }\n }\n if (changed) {\n this.#apply(next);\n }\n }\n\n // --- Gesture routers ---\n\n /** Route a click on `item` according to the current interaction mode. */\n handleClick(item: T, gesture: ListSelectionGesture): void {\n const { selectionMode, interactionMode: mode } = this.#config.getModes();\n if (mode === `manual`)\n return;\n if (selectionMode === `none` && mode !== `checked` && mode !== `sticky`)\n return;\n\n switch (mode) {\n case `implicit`:\n // Single-select only; modifier keys have no effect\n this.replace(item);\n break;\n\n case `standard`:\n case `vscode`:\n if (gesture.ctrlKey || gesture.metaKey) {\n // Ctrl/Cmd+click: toggle item; requires effective multi-select\n if (selectionMode === `multiple` || mode === `vscode`) {\n this.toggle(item);\n }\n } else if (gesture.shiftKey && (selectionMode === `multiple` || mode === `vscode`)) {\n // Shift+click: range-select from anchor\n this.rangeTo(item);\n } else {\n // Plain click: replace selection, update anchor\n this.replace(item);\n }\n break;\n\n case `checked`:\n // Body clicks do nothing — only the checkbox column toggles.\n break;\n\n case `sticky`:\n // Every tap/click toggles — multi-select without modifier keys (touchscreens).\n this.toggle(item);\n break;\n }\n }\n\n /** Route a Space keypress on `item` according to the current interaction mode. */\n handleSpace(item: T): void {\n const mode = this.#config.getModes().interactionMode;\n if (mode === `manual`)\n return;\n if (mode === `implicit`) {\n // Space selects (same as click) in implicit mode\n this.replace(item);\n } else {\n // standard / vscode / checked / sticky: Space toggles\n this.toggle(item);\n }\n }\n\n // --- Maintenance ---\n\n /** Drop the range-selection anchor (e.g. when the interaction mode changes). */\n clearAnchor(): void {\n this.#anchor = undefined;\n }\n\n /**\n * Re-key the selection across an identity boundary — e.g. when items are\n * wrapped/unwrapped and the selected Element references change. Silent:\n * no `onChange` is fired.\n */\n remap(fn: (item: T) => T): void {\n if (this.#selected.size === 0)\n return;\n this.#selected = new Set([...this.#selected].map(fn));\n }\n\n /**\n * Hard reset: empty the selection and clear the anchor without firing\n * `onChange` (e.g. when all items are removed).\n */\n reset(): void {\n this.#selected = new Set();\n this.#anchor = undefined;\n }\n\n /**\n * Replace the selection and anchor silently (no `onChange`). Used to\n * re-key the selection when items are rebuilt from data — e.g. matching\n * new elements to previously selected keys.\n */\n seed(items: Iterable<T>, anchor?: T): void {\n this.#selected = new Set(items);\n this.#anchor = anchor;\n }\n\n #apply(next: ReadonlySet<T>): void {\n const previous = this.#selected;\n this.#selected = next;\n this.#config.onChange(next, previous);\n }\n}\n"],"mappings":"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"}
@@ -24,6 +24,11 @@ type ListInteractionMode = `implicit` | `standard` | `manual` | `checked` | `sti
24
24
  type ListSelectDetail = {
25
25
  readonly selected: ReadonlySet<Element>;
26
26
  readonly previous: ReadonlySet<Element>;
27
+ /**
28
+ * The selection as stable key strings (`data-key` / `data-value`). Empty
29
+ * when items carry no key. Additive to `selected`.
30
+ */
31
+ readonly selectedKeys: ReadonlySet<string>;
27
32
  };
28
33
  type ListTickleDetail = {
29
34
  readonly item: Element;
@@ -46,4 +51,4 @@ interface ListElementEventMap {
46
51
  }
47
52
  //#endregion
48
53
  export { ListItemClickDetail as a, ListSelectEvent as c, ListTickleEvent as d, ListInteractionMode as i, ListSelectionMode as l, ListActivateEvent as n, ListItemClickEvent as o, ListElementEventMap as r, ListSelectDetail as s, ListActivateDetail as t, ListTickleDetail as u };
49
- //# sourceMappingURL=list-selection-types-DSuRNWpx.d.ts.map
54
+ //# sourceMappingURL=list-selection-types-B5ioL-dG.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-selection-types-B5ioL-dG.d.ts","names":[],"sources":["../src/interaction/list-selection-types.ts"],"mappings":";;;;;;;;;KASY;;;;;;;;;;;;;KAcA;KAEA;WACD,UAAU,YAAY;WACtB,UAAU,YAAY;;;;;WAKtB,cAAc;;KAGb;WACD,MAAM;;KAGL;WACD,MAAM;;KAGL;WACD,MAAM;;KAGL,kBAAkB,YAAY;KAC9B,kBAAkB,YAAY;KAC9B,oBAAoB,YAAY;KAChC,qBAAqB,YAAY;UAE5B;EACf,eAAe;EACf,eAAe;EACf,iBAAiB;EACjB,mBAAmB"}
@@ -3,8 +3,8 @@ import { t as tickledItemStyles } from "./tickled-styles-BrAHEj1q.js";
3
3
  import { t as themeFallbacks } from "./fallbacks-DNI9xt0F.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
5
  import "./icon-DcEEGQa_.js";
6
- import "./interaction-D5XdKzbR.js";
7
- import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-D2S4i8u1.js";
6
+ import "./interaction-Cr13azbM.js";
7
+ import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-Bx-Fs7jq.js";
8
8
  import { LitElement, css, html, nothing } from "lit";
9
9
  import { property, query, state } from "lit/decorators.js";
10
10
  //#region src/menu/menu-bar.ts
@@ -822,4 +822,4 @@ var menu_exports = /* @__PURE__ */ __exportAll({
822
822
  //#endregion
823
823
  export { menuFromTreeToElements as a, MenuSubTrigger as c, MenuBar as d, menuFromTree as i, MenuCommandController as l, MenuRadioGroup as n, MenuHeader as o, MenuRadioItem as r, MenuSeparator as s, menu_exports as t, MenuCheckItem as u };
824
824
 
825
- //# sourceMappingURL=menu-DMzjkVnK.js.map
825
+ //# sourceMappingURL=menu-Bg90Qr0B.js.map