@ixfx/components 0.7.2 → 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 (82) hide show
  1. package/bundle/index.d.ts +805 -137
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2168 -42
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.js +1 -1
  6. package/dist/ac-token.js +1 -1
  7. package/dist/crumbs.js +3 -3
  8. package/dist/{data-grid-Bh4YrQGK.d.ts → data-grid-BddUjAti.d.ts} +33 -1
  9. package/dist/data-grid-BddUjAti.d.ts.map +1 -0
  10. package/dist/data-grid.d.ts +1 -1
  11. package/dist/data-grid.js +180 -7
  12. package/dist/data-grid.js.map +1 -1
  13. package/dist/detail-list-Br0WAnry.d.ts +167 -0
  14. package/dist/detail-list-Br0WAnry.d.ts.map +1 -0
  15. package/dist/detail-list.d.ts +3 -0
  16. package/dist/detail-list.js +1115 -0
  17. package/dist/detail-list.js.map +1 -0
  18. package/dist/grid-list-B_orPsot.d.ts +304 -0
  19. package/dist/grid-list-B_orPsot.d.ts.map +1 -0
  20. package/dist/grid-list.d.ts +3 -0
  21. package/dist/grid-list.js +933 -0
  22. package/dist/grid-list.js.map +1 -0
  23. package/dist/{grouped-item-lister-DnWB-eFT.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +2 -2
  24. package/dist/{grouped-item-lister-DnWB-eFT.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
  25. package/dist/grouped-item-lister.d.ts +1 -1
  26. package/dist/index.d.ts +267 -124
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +469 -979
  29. package/dist/index.js.map +1 -1
  30. package/dist/interaction-Cr13azbM.js +3 -0
  31. package/dist/list-item-animation-DuiwgN3l.js +112 -0
  32. package/dist/list-item-animation-DuiwgN3l.js.map +1 -0
  33. package/dist/list-selection-controller-CUo-Hhcm.js +338 -0
  34. package/dist/list-selection-controller-CUo-Hhcm.js.map +1 -0
  35. package/dist/{list-selection-types-DSuRNWpx.d.ts → list-selection-types-B5ioL-dG.d.ts} +6 -1
  36. package/dist/list-selection-types-B5ioL-dG.d.ts.map +1 -0
  37. package/dist/{menu-CXDFJxNm.js → menu-Bg90Qr0B.js} +7 -7
  38. package/dist/menu-Bg90Qr0B.js.map +1 -0
  39. package/dist/{menu-item-D2S4i8u1.js → menu-item-Bx-Fs7jq.js} +3 -3
  40. package/dist/{menu-item-D2S4i8u1.js.map → menu-item-Bx-Fs7jq.js.map} +1 -1
  41. package/dist/menu.js +2 -2
  42. package/dist/miller.d.ts +1 -1
  43. package/dist/miller.js +2 -2
  44. package/dist/narrowed-text.js +2 -2
  45. package/dist/plots.js +1 -1
  46. package/dist/select-horiz.js +2 -2
  47. package/dist/{tickled-controller-CRzdIEjJ.d.ts → tickled-controller-Bn3YdQsM.d.ts} +13 -2
  48. package/dist/{tickled-controller-CRzdIEjJ.d.ts.map → tickled-controller-Bn3YdQsM.d.ts.map} +1 -1
  49. package/dist/{tickled-controller-h9GmJ_bU.js → tickled-controller-C66DgJHl.js} +16 -1
  50. package/dist/tickled-controller-C66DgJHl.js.map +1 -0
  51. package/dist/{tree-Ch4W8qXQ.js → tree-B74zdI60.js} +116 -17
  52. package/dist/tree-B74zdI60.js.map +1 -0
  53. package/dist/tree.d.ts +6 -3
  54. package/dist/tree.d.ts.map +1 -1
  55. package/dist/tree.js +1 -1
  56. package/dist/vertical-list-CQSEVeOT.d.ts +140 -0
  57. package/dist/vertical-list-CQSEVeOT.d.ts.map +1 -0
  58. package/dist/vertical-list.d.ts +3 -121
  59. package/dist/vertical-list.js +710 -1
  60. package/dist/vertical-list.js.map +1 -0
  61. package/dist/{xy-axis-DWizs0bt.js → xy-axis-VrX8dmvS.js} +2 -2
  62. package/dist/{xy-axis-DWizs0bt.js.map → xy-axis-VrX8dmvS.js.map} +1 -1
  63. package/docs-user/README.md +2 -0
  64. package/docs-user/data-grid.md +51 -2
  65. package/docs-user/detail-list.md +80 -15
  66. package/docs-user/grid-list.md +274 -0
  67. package/docs-user/index.json +11 -2
  68. package/docs-user/infra-tickled.md +163 -0
  69. package/docs-user/llms.txt +2 -0
  70. package/docs-user/tree.md +13 -0
  71. package/docs-user/vertical-list.md +51 -1
  72. package/llms.txt +2 -0
  73. package/package.json +17 -2
  74. package/dist/data-grid-Bh4YrQGK.d.ts.map +0 -1
  75. package/dist/interaction-D5XdKzbR.js +0 -2
  76. package/dist/list-selection-types-DSuRNWpx.d.ts.map +0 -1
  77. package/dist/menu-CXDFJxNm.js.map +0 -1
  78. package/dist/tickled-controller-h9GmJ_bU.js.map +0 -1
  79. package/dist/tree-Ch4W8qXQ.js.map +0 -1
  80. package/dist/vertical-list-ClnqTkF3.js +0 -814
  81. package/dist/vertical-list-ClnqTkF3.js.map +0 -1
  82. 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";
@@ -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,338 @@
1
+ //#region src/interaction/keyed-list.ts
2
+ /**
3
+ * Helpers for key-based selection on the Element-flavoured list components
4
+ * (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
5
+ *
6
+ * A "key" is a stable string the host puts on each item element — `data-key`
7
+ * (preferred) or `data-value`. It survives the host rebuilding the item set
8
+ * (re-fetch, re-sort, filter) where raw `Element` references do not.
9
+ */
10
+ /**
11
+ * The stable key for an item element: `data-key`, then `data-value`, else
12
+ * `undefined`.
13
+ */
14
+ function keyForElement(el) {
15
+ return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
16
+ }
17
+ /**
18
+ * A lazily-built key → element index over a container. Rebuilt on first use
19
+ * and after any child-list mutation under `root`; `invalidate()` forces a
20
+ * rebuild on the next `get()`.
21
+ *
22
+ * ```ts
23
+ * #keyIndex = new KeyedElementIndex(this);
24
+ * // ...
25
+ * const el = this.#keyIndex.get(`row-42`);
26
+ * // on disconnect:
27
+ * this.#keyIndex.dispose();
28
+ * ```
29
+ */
30
+ var KeyedElementIndex = class {
31
+ #root;
32
+ #selector;
33
+ #map = /* @__PURE__ */ new Map();
34
+ #dirty = true;
35
+ #observer;
36
+ /**
37
+ * @param root Container whose descendants are indexed.
38
+ * @param itemSelector Which descendants count as items (default: any `[data-key]`).
39
+ */
40
+ constructor(root, itemSelector = `[data-key]`) {
41
+ this.#root = root;
42
+ this.#selector = itemSelector;
43
+ this.#observer = new MutationObserver(() => {
44
+ this.#dirty = true;
45
+ });
46
+ this.#observer.observe(root, {
47
+ childList: true,
48
+ subtree: true
49
+ });
50
+ }
51
+ /** The element with this key, or `undefined`. First match wins on duplicates. */
52
+ get(key) {
53
+ if (this.#dirty) this.#rebuild();
54
+ return this.#map.get(key);
55
+ }
56
+ /** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
57
+ invalidate() {
58
+ this.#dirty = true;
59
+ }
60
+ /** Stop observing and drop the cache. */
61
+ dispose() {
62
+ this.#observer.disconnect();
63
+ this.#map.clear();
64
+ }
65
+ #rebuild() {
66
+ this.#map.clear();
67
+ for (const el of this.#root.querySelectorAll(this.#selector)) {
68
+ const key = keyForElement(el);
69
+ if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
70
+ }
71
+ this.#dirty = false;
72
+ }
73
+ };
74
+ //#endregion
75
+ //#region src/interaction/list-selection-controller.ts
76
+ /** Whether the interaction mode allows building a multi-item selection. */
77
+ function isMultiMode(mode) {
78
+ return mode === `checked` || mode === `sticky` || mode === `vscode` || mode === `standard`;
79
+ }
80
+ /**
81
+ * Generic selection state machine shared by list components.
82
+ *
83
+ * Owns the selected set plus the range-selection anchor, the selection
84
+ * primitives (`replace` / `toggle` / `rangeTo` / `clear` / `selectAll` /
85
+ * `selectMany` / `deselect`) and the per-interactionMode gesture routers
86
+ * (`handleClick` / `handleSpace`).
87
+ *
88
+ * Identity is generic: both `ixfx-vertical-list` and `ixfx-detail-list` use
89
+ * `ListSelectionController<Element>`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const selection = new ListSelectionController<Element>({
94
+ * getOrderedItems: () => this.getVisibleItems(),
95
+ * isSelectable: el => !el.hasAttribute(`not-checked`),
96
+ * getModes: () => ({ selectionMode: this.selectionMode, interactionMode: this.interactionMode }),
97
+ * onChange: (selected, previous) => {
98
+ * this.syncItemStates();
99
+ * this.dispatchEvent(new CustomEvent(`list-select`, { detail: { selected, previous } }));
100
+ * },
101
+ * });
102
+ * ```
103
+ */
104
+ var ListSelectionController = class {
105
+ #config;
106
+ #selected = /* @__PURE__ */ new Set();
107
+ #anchor;
108
+ constructor(config) {
109
+ this.#config = config;
110
+ }
111
+ /** The current selection set (read-only). */
112
+ get selected() {
113
+ return this.#selected;
114
+ }
115
+ /**
116
+ * The current selection as stable string keys (read-only). Empty unless a
117
+ * `getKey` was supplied in config. Items whose `getKey` returns `undefined`
118
+ * are omitted.
119
+ */
120
+ get selectedKeys() {
121
+ const getKey = this.#config.getKey;
122
+ const out = /* @__PURE__ */ new Set();
123
+ if (getKey) for (const item of this.#selected) {
124
+ const key = getKey(item);
125
+ if (key !== void 0) out.add(key);
126
+ }
127
+ return out;
128
+ }
129
+ /** Anchor for range (shift) selection — the last plainly-selected item. */
130
+ get anchor() {
131
+ return this.#anchor;
132
+ }
133
+ /** Whether "select all" (Cmd/Ctrl+A) is currently permitted. */
134
+ canSelectAll() {
135
+ return this.canSelectMultiple() && this.#config.getModes().selectionMode !== `none`;
136
+ }
137
+ /** Whether a multi-item selection can be built in the current modes. */
138
+ canSelectMultiple() {
139
+ const { selectionMode, interactionMode } = this.#config.getModes();
140
+ return selectionMode === `multiple` || isMultiMode(interactionMode);
141
+ }
142
+ /** Replace the entire selection with a single item; updates the anchor. */
143
+ replace(item) {
144
+ if (this.#config.getModes().selectionMode === `none`) return;
145
+ if (!this.#config.isSelectable(item)) return;
146
+ this.#anchor = item;
147
+ this.#apply(/* @__PURE__ */ new Set([item]));
148
+ }
149
+ /** Toggle a single item in/out of the selection without replacing it. Anchor unchanged. */
150
+ toggle(item) {
151
+ const { selectionMode, interactionMode } = this.#config.getModes();
152
+ if (!this.#config.isSelectable(item)) return;
153
+ if (selectionMode === `none` && interactionMode !== `checked` && interactionMode !== `sticky`) return;
154
+ const next = new Set(this.#selected);
155
+ if (next.has(item)) next.delete(item);
156
+ else {
157
+ if (!this.canSelectMultiple()) next.clear();
158
+ next.add(item);
159
+ }
160
+ this.#apply(next);
161
+ }
162
+ /** Select the contiguous ordered range from the anchor to `item`. Anchor unchanged. */
163
+ rangeTo(item) {
164
+ if (this.#config.getModes().selectionMode === `none`) return;
165
+ if (this.#anchor === void 0) {
166
+ this.replace(item);
167
+ return;
168
+ }
169
+ const ordered = this.#config.getOrderedItems();
170
+ const anchorIdx = ordered.indexOf(this.#anchor);
171
+ const targetIdx = ordered.indexOf(item);
172
+ if (anchorIdx === -1 || targetIdx === -1) {
173
+ this.replace(item);
174
+ return;
175
+ }
176
+ const start = Math.min(anchorIdx, targetIdx);
177
+ const end = Math.max(anchorIdx, targetIdx);
178
+ this.#apply(new Set(ordered.slice(start, end + 1).filter((t) => this.#config.isSelectable(t))));
179
+ }
180
+ /** Clear the selection. No change event when the selection is already empty. */
181
+ clear() {
182
+ if (this.#selected.size === 0) return;
183
+ this.#apply(/* @__PURE__ */ new Set());
184
+ }
185
+ /** Select every ordered item that passes `isSelectable`. */
186
+ selectAll() {
187
+ const ordered = this.#config.getOrderedItems();
188
+ if (ordered.length === 0) return;
189
+ const next = new Set(ordered.filter((t) => this.#config.isSelectable(t)));
190
+ if (next.size === 0) return;
191
+ this.#apply(next);
192
+ }
193
+ /**
194
+ * Replace the selection with many items at once. Requires
195
+ * `selectionMode="multiple"`, throws otherwise. Updates the anchor.
196
+ */
197
+ selectMany(items) {
198
+ const { selectionMode } = this.#config.getModes();
199
+ if (selectionMode !== `multiple`) throw new Error(`selectMany requires selectionMode="multiple" (current: "${selectionMode}")`);
200
+ const next = new Set([...items].filter((t) => this.#config.isSelectable(t)));
201
+ if (next.size === 0) return;
202
+ this.#anchor = [...next].at(-1);
203
+ this.#apply(next);
204
+ }
205
+ /** Remove a single item from the selection. */
206
+ deselect(item) {
207
+ if (!this.#selected.has(item)) return;
208
+ const next = new Set(this.#selected);
209
+ next.delete(item);
210
+ this.#apply(next);
211
+ }
212
+ /**
213
+ * Whether the current modes permit a selection of more than one item.
214
+ * Stricter than `canSelectMultiple()`: `standard` / `implicit` only count
215
+ * as multi when `selectionMode="multiple"`.
216
+ */
217
+ #effectiveMulti() {
218
+ const { selectionMode, interactionMode } = this.#config.getModes();
219
+ return selectionMode === `multiple` || interactionMode === `checked` || interactionMode === `sticky` || interactionMode === `vscode`;
220
+ }
221
+ /**
222
+ * Replace the selection with the items resolved from `keys`. Keys that
223
+ * `resolve` cannot map, or that resolve to a non-selectable item, are
224
+ * ignored. `selectionMode="none"` is a no-op. When multi-select is not
225
+ * available, only the last resolvable key is kept. `onChange` fires once.
226
+ */
227
+ selectKeys(keys, resolve) {
228
+ if (this.#config.getModes().selectionMode === `none`) return;
229
+ let next = /* @__PURE__ */ new Set();
230
+ for (const key of new Set(keys)) {
231
+ const item = resolve(key);
232
+ if (item !== void 0 && this.#config.isSelectable(item)) next.add(item);
233
+ }
234
+ if (!this.#effectiveMulti() && next.size > 1) {
235
+ const last = [...next].at(-1);
236
+ next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
237
+ }
238
+ this.#anchor = [...next].at(-1);
239
+ this.#apply(next);
240
+ }
241
+ /**
242
+ * Add the items resolved from `keys` to the selection (multi-select only;
243
+ * a no-op otherwise). `onChange` fires once if anything changed.
244
+ */
245
+ addKeys(keys, resolve) {
246
+ if (!this.#effectiveMulti()) return;
247
+ const next = new Set(this.#selected);
248
+ let changed = false;
249
+ for (const key of new Set(keys)) {
250
+ const item = resolve(key);
251
+ if (item !== void 0 && !next.has(item) && this.#config.isSelectable(item)) {
252
+ next.add(item);
253
+ changed = true;
254
+ }
255
+ }
256
+ if (changed) this.#apply(next);
257
+ }
258
+ /**
259
+ * Remove the items resolved from `keys` from the selection. `onChange`
260
+ * fires once if anything changed.
261
+ */
262
+ removeKeys(keys, resolve) {
263
+ if (this.#selected.size === 0) return;
264
+ const next = new Set(this.#selected);
265
+ let changed = false;
266
+ for (const key of new Set(keys)) {
267
+ const item = resolve(key);
268
+ if (item !== void 0 && next.delete(item)) changed = true;
269
+ }
270
+ if (changed) this.#apply(next);
271
+ }
272
+ /** Route a click on `item` according to the current interaction mode. */
273
+ handleClick(item, gesture) {
274
+ const { selectionMode, interactionMode: mode } = this.#config.getModes();
275
+ if (mode === `manual`) return;
276
+ if (selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
277
+ switch (mode) {
278
+ case `implicit`:
279
+ this.replace(item);
280
+ break;
281
+ case `standard`:
282
+ case `vscode`:
283
+ if (gesture.ctrlKey || gesture.metaKey) {
284
+ if (selectionMode === `multiple` || mode === `vscode`) this.toggle(item);
285
+ } else if (gesture.shiftKey && (selectionMode === `multiple` || mode === `vscode`)) this.rangeTo(item);
286
+ else this.replace(item);
287
+ break;
288
+ case `checked`: break;
289
+ case `sticky`: this.toggle(item);
290
+ }
291
+ }
292
+ /** Route a Space keypress on `item` according to the current interaction mode. */
293
+ handleSpace(item) {
294
+ const mode = this.#config.getModes().interactionMode;
295
+ if (mode === `manual`) return;
296
+ if (mode === `implicit`) this.replace(item);
297
+ else this.toggle(item);
298
+ }
299
+ /** Drop the range-selection anchor (e.g. when the interaction mode changes). */
300
+ clearAnchor() {
301
+ this.#anchor = void 0;
302
+ }
303
+ /**
304
+ * Re-key the selection across an identity boundary — e.g. when items are
305
+ * wrapped/unwrapped and the selected Element references change. Silent:
306
+ * no `onChange` is fired.
307
+ */
308
+ remap(fn) {
309
+ if (this.#selected.size === 0) return;
310
+ this.#selected = new Set([...this.#selected].map(fn));
311
+ }
312
+ /**
313
+ * Hard reset: empty the selection and clear the anchor without firing
314
+ * `onChange` (e.g. when all items are removed).
315
+ */
316
+ reset() {
317
+ this.#selected = /* @__PURE__ */ new Set();
318
+ this.#anchor = void 0;
319
+ }
320
+ /**
321
+ * Replace the selection and anchor silently (no `onChange`). Used to
322
+ * re-key the selection when items are rebuilt from data — e.g. matching
323
+ * new elements to previously selected keys.
324
+ */
325
+ seed(items, anchor) {
326
+ this.#selected = new Set(items);
327
+ this.#anchor = anchor;
328
+ }
329
+ #apply(next) {
330
+ const previous = this.#selected;
331
+ this.#selected = next;
332
+ this.#config.onChange(next, previous);
333
+ }
334
+ };
335
+ //#endregion
336
+ export { keyForElement as i, isMultiMode as n, KeyedElementIndex as r, ListSelectionController as t };
337
+
338
+ //# sourceMappingURL=list-selection-controller-CUo-Hhcm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-selection-controller-CUo-Hhcm.js","names":["#root","#selector","#map","#observer","#dirty","#rebuild","#config","#selected","#anchor","#apply","#effectiveMulti"],"sources":["../src/interaction/keyed-list.ts","../src/interaction/list-selection-controller.ts"],"sourcesContent":["/**\n * Helpers for key-based selection on the Element-flavoured list components\n * (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).\n *\n * A \"key\" is a stable string the host puts on each item element — `data-key`\n * (preferred) or `data-value`. It survives the host rebuilding the item set\n * (re-fetch, re-sort, filter) where raw `Element` references do not.\n */\n\n/**\n * The stable key for an item element: `data-key`, then `data-value`, else\n * `undefined`.\n */\nexport function keyForElement(el: Element): string | undefined {\n return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? undefined;\n}\n\n/**\n * A lazily-built key → element index over a container. Rebuilt on first use\n * and after any child-list mutation under `root`; `invalidate()` forces a\n * rebuild on the next `get()`.\n *\n * ```ts\n * #keyIndex = new KeyedElementIndex(this);\n * // ...\n * const el = this.#keyIndex.get(`row-42`);\n * // on disconnect:\n * this.#keyIndex.dispose();\n * ```\n */\nexport class KeyedElementIndex {\n readonly #root: Element;\n readonly #selector: string;\n readonly #map = new Map<string, Element>();\n #dirty = true;\n readonly #observer: MutationObserver;\n\n /**\n * @param root Container whose descendants are indexed.\n * @param itemSelector Which descendants count as items (default: any `[data-key]`).\n */\n constructor(root: Element, itemSelector = `[data-key]`) {\n this.#root = root;\n this.#selector = itemSelector;\n this.#observer = new MutationObserver(() => {\n this.#dirty = true;\n });\n this.#observer.observe(root, { childList: true, subtree: true });\n }\n\n /** The element with this key, or `undefined`. First match wins on duplicates. */\n get(key: string): Element | undefined {\n if (this.#dirty) {\n this.#rebuild();\n }\n return this.#map.get(key);\n }\n\n /** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */\n invalidate(): void {\n this.#dirty = true;\n }\n\n /** Stop observing and drop the cache. */\n dispose(): void {\n this.#observer.disconnect();\n this.#map.clear();\n }\n\n #rebuild(): void {\n this.#map.clear();\n for (const el of this.#root.querySelectorAll(this.#selector)) {\n const key = keyForElement(el);\n if (key !== undefined && !this.#map.has(key)) {\n this.#map.set(key, el);\n }\n }\n this.#dirty = false;\n }\n}\n","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
@@ -175,8 +175,8 @@ let MenuCheckItem = class MenuCheckItem extends LitElement {
175
175
  }
176
176
 
177
177
  .check {
178
- width: 16px;
179
- margin-right: var(--space-s);
178
+ width: 1em;
179
+ margin-left: var(--space-xs);
180
180
  font-size: var(--text-m);
181
181
  color: var(--accent);
182
182
  }
@@ -696,8 +696,8 @@ let MenuRadioItem = class MenuRadioItem extends LitElement {
696
696
  }
697
697
 
698
698
  .radio {
699
- width: 16px;
700
- margin-right: var(--space-s);
699
+ width: 1em;
700
+ margin-left: var(--space-xs);
701
701
  font-size: var(--text-m);
702
702
  color: var(--accent);
703
703
  }
@@ -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-CXDFJxNm.js.map
825
+ //# sourceMappingURL=menu-Bg90Qr0B.js.map