@ixfx/components 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/bundle/index.d.ts +1086 -603
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +16527 -14258
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +14 -4
  6. package/dist/ac-text.d.ts.map +1 -1
  7. package/dist/ac-text.js +11 -6
  8. package/dist/ac-text.js.map +1 -1
  9. package/dist/{button-DMt5XWUK.js → button-C3fLMKUv.js} +4 -4
  10. package/dist/{button-DMt5XWUK.js.map → button-C3fLMKUv.js.map} +1 -1
  11. package/dist/button.d.ts.map +1 -1
  12. package/dist/button.js +1 -1
  13. package/dist/checkbox.d.ts +2 -2
  14. package/dist/checkbox.d.ts.map +1 -1
  15. package/dist/checkbox.js +14 -8
  16. package/dist/checkbox.js.map +1 -1
  17. package/dist/{colour-C3MQIjFJ-BLXg8W3U.js → colour-C3MQIjFJ-vRCdr8W-.js} +146 -552
  18. package/dist/colour-C3MQIjFJ-vRCdr8W-.js.map +1 -0
  19. package/dist/{colour-picker-K6zu82lm.js → colour-picker-0sgAha1T.js} +14 -9
  20. package/dist/colour-picker-0sgAha1T.js.map +1 -0
  21. package/dist/colour-picker.d.ts +1 -1
  22. package/dist/colour-picker.js +1 -1
  23. package/dist/crumbs.d.ts +1 -1
  24. package/dist/crumbs.d.ts.map +1 -1
  25. package/dist/crumbs.js +11 -6
  26. package/dist/crumbs.js.map +1 -1
  27. package/dist/data-display.d.ts +9 -4
  28. package/dist/data-display.d.ts.map +1 -1
  29. package/dist/data-display.js +270 -43
  30. package/dist/data-display.js.map +1 -1
  31. package/dist/data-provider-DgI909FP.js +23 -0
  32. package/dist/data-provider-DgI909FP.js.map +1 -0
  33. package/dist/{decorate-D7rC1gLP.js → decorate-DdjvVHS-.js} +2 -2
  34. package/dist/{decorate-D7rC1gLP.js.map → decorate-DdjvVHS-.js.map} +1 -1
  35. package/dist/{defaults-C7elhuyJ.js → defaults-C3b9OWJD.js} +2 -2
  36. package/dist/{defaults-C7elhuyJ.js.map → defaults-C3b9OWJD.js.map} +1 -1
  37. package/dist/{dist-TAGYwaju.js → dist-CA13FvMn.js} +7 -66
  38. package/dist/dist-CA13FvMn.js.map +1 -0
  39. package/dist/editable-label.d.ts.map +1 -1
  40. package/dist/editable-label.js +3 -2
  41. package/dist/editable-label.js.map +1 -1
  42. package/dist/fallbacks-pNMOMx38.js +168 -0
  43. package/dist/fallbacks-pNMOMx38.js.map +1 -0
  44. package/dist/{hex-editor-DlfoA65y.d.ts → hex-editor-CMIxtNgT.d.ts} +1 -1
  45. package/dist/hex-editor-CMIxtNgT.d.ts.map +1 -0
  46. package/dist/{hex-editor-CsN_ySkH.js → hex-editor-WZrjRtvL.js} +3 -3
  47. package/dist/{hex-editor-CsN_ySkH.js.map → hex-editor-WZrjRtvL.js.map} +1 -1
  48. package/dist/hex.d.ts +1 -1
  49. package/dist/hex.js +1 -1
  50. package/dist/{highlight-TPbSHruI.js → highlight-B-eQrhhb.js} +1 -1
  51. package/dist/{highlight-TPbSHruI.js.map → highlight-B-eQrhhb.js.map} +1 -1
  52. package/dist/{icon-CeTDJhC7.d.ts → icon-Bd5BiU4b.d.ts} +2 -2
  53. package/dist/icon-Bd5BiU4b.d.ts.map +1 -0
  54. package/dist/{icon-9NWTXtE1.js → icon-Ndo40kNO.js} +6 -5
  55. package/dist/icon-Ndo40kNO.js.map +1 -0
  56. package/dist/icons.d.ts +2 -2
  57. package/dist/icons.js +3 -3
  58. package/dist/{incr-search-DSKKsk6w.js → incr-search-CeERfijK.js} +2 -2
  59. package/dist/{incr-search-DSKKsk6w.js.map → incr-search-CeERfijK.js.map} +1 -1
  60. package/dist/incr-search.d.ts +1 -1
  61. package/dist/incr-search.js +2 -2
  62. package/dist/{index-hXFn14tM.d.ts → index-BMjri2-S.d.ts} +2 -2
  63. package/dist/{index-hXFn14tM.d.ts.map → index-BMjri2-S.d.ts.map} +1 -1
  64. package/dist/{index-DSlPeWLK.d.ts → index-CWpNcQKE.d.ts} +5 -5
  65. package/dist/index-CWpNcQKE.d.ts.map +1 -0
  66. package/dist/{index--hrxPBkQ.d.ts → index-CowKi2mo.d.ts} +2 -1
  67. package/dist/index-CowKi2mo.d.ts.map +1 -0
  68. package/dist/{index-B1hNR7Z9.d.ts → index-D87kRcEs.d.ts} +1 -1
  69. package/dist/{index-B1hNR7Z9.d.ts.map → index-D87kRcEs.d.ts.map} +1 -1
  70. package/dist/{index-CnL9krsH.d.ts → index-DgXpCg_G.d.ts} +32 -26
  71. package/dist/index-DgXpCg_G.d.ts.map +1 -0
  72. package/dist/{index-DrQjA8Gs.d.ts → index-s_IhSnwc.d.ts} +2 -2
  73. package/dist/index-s_IhSnwc.d.ts.map +1 -0
  74. package/dist/index.d.ts +432 -121
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2370 -1707
  77. package/dist/index.js.map +1 -1
  78. package/dist/interaction-D6efrBLo.js +1 -0
  79. package/dist/{keyboard-De_vOHLg.js → keyboard-CimiEskD.js} +1 -1
  80. package/dist/{keyboard-De_vOHLg.js.map → keyboard-CimiEskD.js.map} +1 -1
  81. package/dist/{labelled-input-base-CqPL5rKm.js → labelled-input-base-9K_w3oLj.js} +2 -2
  82. package/dist/{labelled-input-base-CqPL5rKm.js.map → labelled-input-base-9K_w3oLj.js.map} +1 -1
  83. package/dist/{labelled-input-base-D-Y7agky.d.ts → labelled-input-base-ORb1rGq6.d.ts} +1 -1
  84. package/dist/labelled-input-base-ORb1rGq6.d.ts.map +1 -0
  85. package/dist/labelled-radial-input.d.ts +2 -2
  86. package/dist/labelled-radial-input.d.ts.map +1 -1
  87. package/dist/labelled-radial-input.js +7 -6
  88. package/dist/labelled-radial-input.js.map +1 -1
  89. package/dist/labelled-range-input.d.ts +1 -1
  90. package/dist/labelled-range-input.js +2 -2
  91. package/dist/led.d.ts +1 -1
  92. package/dist/led.d.ts.map +1 -1
  93. package/dist/led.js +2 -2
  94. package/dist/led.js.map +1 -1
  95. package/dist/{menu-item-CKRfP0pN.js → menu-item-BrIuORYD.js} +20 -10
  96. package/dist/menu-item-BrIuORYD.js.map +1 -0
  97. package/dist/{menu-item-ClQF2DKN.d.ts → menu-item-COx-Azgj.d.ts} +1 -1
  98. package/dist/menu-item-COx-Azgj.d.ts.map +1 -0
  99. package/dist/{menu-DJJuCm-5.js → menu-vjoiHZ13.js} +30 -17
  100. package/dist/menu-vjoiHZ13.js.map +1 -0
  101. package/dist/menu.d.ts +2 -2
  102. package/dist/menu.js +2 -2
  103. package/dist/miller.d.ts +2 -2
  104. package/dist/miller.d.ts.map +1 -1
  105. package/dist/miller.js +18 -11
  106. package/dist/miller.js.map +1 -1
  107. package/dist/narrowed-text.d.ts.map +1 -1
  108. package/dist/narrowed-text.js +7 -5
  109. package/dist/narrowed-text.js.map +1 -1
  110. package/dist/panel.d.ts +1 -1
  111. package/dist/panel.d.ts.map +1 -1
  112. package/dist/panel.js +5 -4
  113. package/dist/panel.js.map +1 -1
  114. package/dist/plots.d.ts +2 -2
  115. package/dist/plots.js +2 -2
  116. package/dist/polar-pad.d.ts +1 -1
  117. package/dist/polar-pad.d.ts.map +1 -1
  118. package/dist/polar-pad.js +10 -4
  119. package/dist/polar-pad.js.map +1 -1
  120. package/dist/{popup-ImtS9KnS.js → popup-DiWMxsWj.js} +1 -1
  121. package/dist/{popup-ImtS9KnS.js.map → popup-DiWMxsWj.js.map} +1 -1
  122. package/dist/prominence-DlFShic3.js +104 -0
  123. package/dist/prominence-DlFShic3.js.map +1 -0
  124. package/dist/{radial-input-LUSBEycd.d.ts → radial-input-CpQPs1Mp.d.ts} +1 -1
  125. package/dist/radial-input-CpQPs1Mp.d.ts.map +1 -0
  126. package/dist/{radial-input-DZA6WaFu.js → radial-input-DZ7eQCcM.js} +11 -6
  127. package/dist/radial-input-DZ7eQCcM.js.map +1 -0
  128. package/dist/radial-input.d.ts +1 -1
  129. package/dist/radial-input.js +1 -1
  130. package/dist/range-input.d.ts +1 -1
  131. package/dist/range-input.d.ts.map +1 -1
  132. package/dist/range-input.js +9 -4
  133. package/dist/range-input.js.map +1 -1
  134. package/dist/range.d.ts +1 -1
  135. package/dist/range.d.ts.map +1 -1
  136. package/dist/range.js +5 -3
  137. package/dist/range.js.map +1 -1
  138. package/dist/{registry-Q2gYQCHM.js → registry-CkW7q09Q.js} +1 -1
  139. package/dist/{registry-Q2gYQCHM.js.map → registry-CkW7q09Q.js.map} +1 -1
  140. package/dist/selecthorizontal.d.ts +1 -1
  141. package/dist/selecthorizontal.d.ts.map +1 -1
  142. package/dist/selecthorizontal.js +4 -3
  143. package/dist/selecthorizontal.js.map +1 -1
  144. package/dist/snap-container.d.ts.map +1 -1
  145. package/dist/snap-container.js +1 -1
  146. package/dist/split-layout.d.ts +1 -1
  147. package/dist/split-layout.d.ts.map +1 -1
  148. package/dist/split-layout.js +6 -5
  149. package/dist/split-layout.js.map +1 -1
  150. package/dist/style.css +14 -4
  151. package/dist/swipe.d.ts.map +1 -1
  152. package/dist/swipe.js +9 -4
  153. package/dist/swipe.js.map +1 -1
  154. package/dist/{tab-list-BnqISNvO.d.ts → tab-list-DLzb-m-I.d.ts} +2 -2
  155. package/dist/tab-list-DLzb-m-I.d.ts.map +1 -0
  156. package/dist/tabs.d.ts +1 -1
  157. package/dist/tabs.js +10 -4
  158. package/dist/tabs.js.map +1 -1
  159. package/dist/{tickled-controller-Cq1Va0rR.d.ts → tickled-controller-BjYCzGrU.d.ts} +2 -2
  160. package/dist/tickled-controller-BjYCzGrU.d.ts.map +1 -0
  161. package/dist/{tickled-styles-Bg3QbcrD.js → tickled-styles-fNDdqf6l.js} +1 -17
  162. package/dist/{tickled-styles-Bg3QbcrD.js.map → tickled-styles-fNDdqf6l.js.map} +1 -1
  163. package/dist/{timeline-Cv75Th2U.js → timeline-2OFtN4Ta.js} +1437 -36
  164. package/dist/timeline-2OFtN4Ta.js.map +1 -0
  165. package/dist/timeline.d.ts +1 -1
  166. package/dist/timeline.js +1 -1
  167. package/dist/{tooltip-DMxmBNky.js → tooltip-CJt3WLmN.js} +1 -1
  168. package/dist/{tooltip-DMxmBNky.js.map → tooltip-CJt3WLmN.js.map} +1 -1
  169. package/dist/{tooltip-CwGfb4lp.d.ts → tooltip-RGVIwHrZ.d.ts} +1 -1
  170. package/dist/{tooltip-CwGfb4lp.d.ts.map → tooltip-RGVIwHrZ.d.ts.map} +1 -1
  171. package/dist/{tree-CvvKbO-f.js → tree-CJWpMUvD.js} +113 -103
  172. package/dist/tree-CJWpMUvD.js.map +1 -0
  173. package/dist/{tree-component-D8Bg48tt.d.ts → tree-component-BcO26Xvt.d.ts} +65 -3
  174. package/dist/tree-component-BcO26Xvt.d.ts.map +1 -0
  175. package/dist/tree.d.ts +15 -15
  176. package/dist/tree.d.ts.map +1 -1
  177. package/dist/tree.js +1 -1
  178. package/dist/{types-BKaqnpwx.d.ts → types-DSfbtHs3.d.ts} +1 -1
  179. package/dist/types-DSfbtHs3.d.ts.map +1 -0
  180. package/dist/{xy-axis-DL0tfJDr.js → xy-axis-CK64haaK.js} +1831 -359
  181. package/dist/xy-axis-CK64haaK.js.map +1 -0
  182. package/dist/{xy-axis-D9uwpGTT.d.ts → xy-axis-McdUpcYr.d.ts} +102 -3
  183. package/dist/xy-axis-McdUpcYr.d.ts.map +1 -0
  184. package/dist/xy-pad.d.ts +1 -1
  185. package/dist/xy-pad.d.ts.map +1 -1
  186. package/dist/xy-pad.js +11 -6
  187. package/dist/xy-pad.js.map +1 -1
  188. package/docs-user/README.md +45 -0
  189. package/docs-user/ac-text.md +67 -0
  190. package/docs-user/ac-token.md +187 -0
  191. package/docs-user/button.md +355 -0
  192. package/docs-user/checkbox.md +62 -0
  193. package/docs-user/colour-picker.md +237 -0
  194. package/docs-user/crumbs.md +66 -0
  195. package/docs-user/data-display.md +98 -0
  196. package/docs-user/editable-label.md +244 -0
  197. package/docs-user/grouped-item-lister.md +424 -0
  198. package/docs-user/icons.md +191 -0
  199. package/docs-user/incr-search.md +94 -0
  200. package/docs-user/labelled-radial-input.md +204 -0
  201. package/docs-user/labelled-range-input.md +194 -0
  202. package/docs-user/led.md +40 -0
  203. package/docs-user/menu.md +319 -0
  204. package/docs-user/miller.md +453 -0
  205. package/docs-user/narrowed-text.md +72 -0
  206. package/docs-user/panel.md +250 -0
  207. package/docs-user/plots.md +361 -0
  208. package/docs-user/polar-pad.md +228 -0
  209. package/docs-user/radial-input.md +559 -0
  210. package/docs-user/range-input.md +264 -0
  211. package/docs-user/range.md +12 -0
  212. package/docs-user/selecthorizontal.md +32 -0
  213. package/docs-user/snap-container.md +132 -0
  214. package/docs-user/split-layout.md +232 -0
  215. package/docs-user/swipe.md +44 -0
  216. package/docs-user/tabs.md +217 -0
  217. package/docs-user/timeline.md +116 -0
  218. package/docs-user/transitory-label.md +120 -0
  219. package/docs-user/tree.md +592 -0
  220. package/docs-user/user-catalog.md +165 -0
  221. package/docs-user/user-theming.md +379 -0
  222. package/docs-user/util.md +122 -0
  223. package/docs-user/vertical-list.md +351 -0
  224. package/docs-user/xy-pad.md +94 -0
  225. package/package.json +19 -18
  226. package/dist/colour-C3MQIjFJ-BLXg8W3U.js.map +0 -1
  227. package/dist/colour-picker-K6zu82lm.js.map +0 -1
  228. package/dist/dist-BX0OVIm7.js +0 -1290
  229. package/dist/dist-BX0OVIm7.js.map +0 -1
  230. package/dist/dist-TAGYwaju.js.map +0 -1
  231. package/dist/hex-editor-DlfoA65y.d.ts.map +0 -1
  232. package/dist/icon-9NWTXtE1.js.map +0 -1
  233. package/dist/icon-CeTDJhC7.d.ts.map +0 -1
  234. package/dist/index--hrxPBkQ.d.ts.map +0 -1
  235. package/dist/index-CnL9krsH.d.ts.map +0 -1
  236. package/dist/index-DSlPeWLK.d.ts.map +0 -1
  237. package/dist/index-DrQjA8Gs.d.ts.map +0 -1
  238. package/dist/interaction-DG7X1rhI.js +0 -1
  239. package/dist/labelled-input-base-D-Y7agky.d.ts.map +0 -1
  240. package/dist/menu-DJJuCm-5.js.map +0 -1
  241. package/dist/menu-item-CKRfP0pN.js.map +0 -1
  242. package/dist/menu-item-ClQF2DKN.d.ts.map +0 -1
  243. package/dist/prominence--ic9uWBl.js +0 -98
  244. package/dist/prominence--ic9uWBl.js.map +0 -1
  245. package/dist/radial-input-DZA6WaFu.js.map +0 -1
  246. package/dist/radial-input-LUSBEycd.d.ts.map +0 -1
  247. package/dist/tab-list-BnqISNvO.d.ts.map +0 -1
  248. package/dist/tickled-controller-Cq1Va0rR.d.ts.map +0 -1
  249. package/dist/timeline-Cv75Th2U.js.map +0 -1
  250. package/dist/tree-CvvKbO-f.js.map +0 -1
  251. package/dist/tree-component-D8Bg48tt.d.ts.map +0 -1
  252. package/dist/types-BKaqnpwx.d.ts.map +0 -1
  253. package/dist/xy-axis-D9uwpGTT.d.ts.map +0 -1
  254. package/dist/xy-axis-DL0tfJDr.js.map +0 -1
  255. /package/bundle/{chunk-pbuEa-1d.js → chunk-D7D4PA-g.js} +0 -0
  256. /package/dist/{chunk-pbuEa-1d.js → chunk-D7D4PA-g.js} +0 -0
@@ -0,0 +1,424 @@
1
+ # `ixfx-grouped-item-lister`
2
+
3
+ A meta-component that renders a typed item list using any sub-component you supply. Items can be displayed in a single sub-component (_ungrouped_ mode) or split into labelled groups, each with its own sub-component (_grouped_ mode), wrapped in `ixfx-panel` sections.
4
+
5
+ The component uses **Light DOM** so its children are directly accessible to external CSS and can be observed by the page's event listeners without crossing a shadow boundary.
6
+
7
+ ---
8
+
9
+ ## Contents
10
+
11
+ 1. [Quick start](#quick-start)
12
+ 2. [Properties](#properties)
13
+ 3. [Providing data](#providing-data)
14
+ 4. [Built-in adapters](#built-in-adapters)
15
+ - [`createVerticalListAdapter`](#createverticallistadapter)
16
+ - [`createTreeListAdapter`](#createtreelistadapter)
17
+ 5. [Selection](#selection)
18
+ 6. [Dispatching commands](#dispatching-commands)
19
+ 7. [Events](#events)
20
+ 8. [Writing a custom adapter](#writing-a-custom-adapter)
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ```typescript
27
+ import {
28
+ createVerticalListAdapter,
29
+ } from '@ixfx/components';
30
+ import type { GroupedItemListerElement } from '@ixfx/components';
31
+
32
+ type FileItem = { id: string; name: string; ext: string };
33
+
34
+ const lister = document.querySelector<GroupedItemListerElement<FileItem>>('ixfx-grouped-item-lister')!;
35
+
36
+ // 1. Factory — called once per group (or once for the ungrouped view)
37
+ lister.factory = () =>
38
+ createVerticalListAdapter<FileItem>(item => {
39
+ const li = document.createElement('li');
40
+ li.textContent = item.name;
41
+ return li;
42
+ });
43
+
44
+ // 2. Group key function
45
+ lister.groupBy = item => item.ext;
46
+
47
+ // 3. Data source — sync, Promise, or AsyncIterable
48
+ lister.provider = () => myFiles;
49
+
50
+ // 4. Listen for selection changes
51
+ lister.addEventListener('grouped-select', ({ detail }) => {
52
+ const { selected, groupKey } = detail;
53
+ console.log(groupKey, [...selected].map(f => f.name));
54
+ });
55
+ ```
56
+
57
+ ```html
58
+ <ixfx-grouped-item-lister grouped></ixfx-grouped-item-lister>
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Properties
64
+
65
+ ### HTML attributes
66
+
67
+ | Attribute | Type | Default | Description |
68
+ |---|---|---|---|
69
+ | `grouped` | `boolean` | `false` | When set, items are split into one sub-component per group key. Without it, all items go into a single sub-component. |
70
+ | `cross-group-selection` | `boolean` | `false` | When `grouped` is on, controls whether selecting an item in one group automatically clears all other groups. Has no effect when `grouped` is off. |
71
+
72
+ ### JavaScript-only properties
73
+
74
+ These carry typed or callable values and cannot be set as HTML attributes.
75
+
76
+ | Property | Type | Description |
77
+ |---|---|---|
78
+ | `factory` | `SubComponentFactory<T>` | Called once per group (or once for the ungrouped view) to create a sub-component. **Must be set before items arrive.** |
79
+ | `groupBy` | `(item: T) => string` | Derives the group key for each item. Required in grouped mode. |
80
+ | `provider` | `IDataProvider<void, T>` | Data source for items. Setting this property triggers an immediate request. |
81
+
82
+ #### `SubComponentFactory<T>`
83
+
84
+ ```typescript
85
+ type SubComponentFactory<T> = (groupKey: string | undefined) => HTMLElement & IGroupedSubComponent<T>;
86
+ ```
87
+
88
+ `groupKey` is the group's key string in grouped mode, or `undefined` in ungrouped mode.
89
+
90
+ ---
91
+
92
+ ## Providing data
93
+
94
+ `provider` accepts any `IDataProvider<void, T>` — a function returning one of:
95
+
96
+ ```typescript
97
+ // Sync — items available immediately
98
+ lister.provider = () => myItemsArray;
99
+
100
+ // Async one-shot
101
+ lister.provider = async () => {
102
+ const res = await fetch('/api/items');
103
+ return res.json() as T[];
104
+ };
105
+
106
+ // Streaming — items delivered in batches over time
107
+ lister.provider = async function* () {
108
+ for await (const page of paginatedFetch('/api/items')) {
109
+ yield { items: page };
110
+ }
111
+ };
112
+ ```
113
+
114
+ Replacing `provider` cancels any in-flight request and starts a fresh one.
115
+
116
+ ---
117
+
118
+ ## Built-in adapters
119
+
120
+ Two factory helpers ship with the package, one for each existing list component.
121
+
122
+ ---
123
+
124
+ ### `createVerticalListAdapter`
125
+
126
+ Wraps `ixfx-vertical-list`. Items are typed as `T`; the adapter converts each item to a DOM element via your `renderItem` function, maintains a `WeakMap<Element, T>` for reverse-lookup, and bridges `list-*` events to the interface names (`select`, `activate`, etc.).
127
+
128
+ ```typescript
129
+ import { createVerticalListAdapter } from '@ixfx/components';
130
+
131
+ lister.factory = () =>
132
+ createVerticalListAdapter<MyItem>(
133
+ // Convert your typed item to a DOM element (typically an <li>)
134
+ item => {
135
+ const li = document.createElement('li');
136
+ li.textContent = item.label;
137
+ li.dataset.key = item.id;
138
+ li.dataset.searchLabel = item.label; // enables Ctrl+F search
139
+ return li;
140
+ },
141
+ {
142
+ selectionMode: 'single', // 'none' | 'single' | 'multiple'
143
+ interactionMode: 'standard', // forwarded to ixfx-vertical-list
144
+ onDispatch: (commandId, args, list) => {
145
+ if (commandId === 'sort-alpha' && list) {
146
+ const rows = [...list.querySelectorAll<HTMLElement>('li')];
147
+ rows.sort((a, b) => (a.textContent ?? '').localeCompare(b.textContent ?? ''));
148
+ for (const row of rows) list.appendChild(row);
149
+ }
150
+ },
151
+ },
152
+ );
153
+ ```
154
+
155
+ #### Options
156
+
157
+ | Option | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'single'` | Forwarded to the inner `ixfx-vertical-list` |
160
+ | `interactionMode` | `ListInteractionMode` | `'standard'` | Forwarded to the inner `ixfx-vertical-list` |
161
+ | `onDispatch` | `(commandId, args, list?) => void` | — | Called when `lister.dispatch()` is invoked. `list` is the inner `VerticalListElement` |
162
+
163
+ The inner `ixfx-vertical-list` inherits its full feature set: keyboard navigation, incremental search (Ctrl+F), and all selection modes including checkboxes.
164
+
165
+ ---
166
+
167
+ ### `createTreeListAdapter`
168
+
169
+ Wraps `ixfx-tree-list`. Builds a `TreeNode` hierarchy from your flat or nested items, stores each original `T` in `TreeItem.data` for selection reverse-mapping, and bridges tree events to the interface.
170
+
171
+ ```typescript
172
+ import { createTreeListAdapter } from '@ixfx/components';
173
+
174
+ lister.factory = () =>
175
+ createTreeListAdapter<CategoryItem>(
176
+ // Convert your item to TreeItem display metadata
177
+ item => ({
178
+ key: item.id,
179
+ label: item.name,
180
+ isLeaf: item.children.length === 0,
181
+ icon: item.children.length ? 'folder' : 'file',
182
+ }),
183
+ {
184
+ // Return the item's direct children to build the hierarchy
185
+ getChildren: item => item.children,
186
+ selectionFilter: 'all',
187
+ onDispatch: (commandId, _args, tree) => {
188
+ if (commandId === 'sort-alpha' && tree?.root?.children) {
189
+ tree.root = {
190
+ ...tree.root,
191
+ children: [...tree.root.children].sort((a, b) =>
192
+ a.item.label.localeCompare(b.item.label),
193
+ ),
194
+ };
195
+ }
196
+ },
197
+ },
198
+ );
199
+ ```
200
+
201
+ #### Flat item lists
202
+
203
+ Omit `getChildren` when items have no hierarchy — all items become root-level leaf nodes:
204
+
205
+ ```typescript
206
+ lister.factory = () =>
207
+ createTreeListAdapter<FileItem>(
208
+ item => ({ key: item.id, label: item.name, isLeaf: true }),
209
+ );
210
+ ```
211
+
212
+ #### Options
213
+
214
+ | Option | Type | Default | Description |
215
+ |---|---|---|---|
216
+ | `getChildren` | `(item: T) => readonly T[]` | — | Returns the item's direct children. Omit for flat lists. |
217
+ | `interactionMode` | `TreeInteractionMode` | `'standard'` | Forwarded to the inner `ixfx-tree-list` |
218
+ | `selectionFilter` | `TreeSelectionFilter` | `'all'` | Which node types are selectable (`'leaf'`, `'branch'`, `'all'`, `'none'`) |
219
+ | `exclusivity` | `TreeExclusivity` | `'none'` | Accordion behaviour when expanding nodes |
220
+ | `onDispatch` | `(commandId, args, tree?) => void` | — | Called when `lister.dispatch()` is invoked. `tree` is the inner `TreeBaseElement` |
221
+
222
+ ---
223
+
224
+ ## Selection
225
+
226
+ ### Cross-group selection
227
+
228
+ By default (`cross-group-selection` is off), clicking an item in group B automatically clears selections in all other groups. Set the attribute to allow simultaneous selections across groups:
229
+
230
+ ```html
231
+ <ixfx-grouped-item-lister grouped cross-group-selection></ixfx-grouped-item-lister>
232
+ ```
233
+
234
+ ```typescript
235
+ lister.crossGroupSelection = true;
236
+ ```
237
+
238
+ This attribute has no effect when `grouped` is off.
239
+
240
+ ### Reading the current selection
241
+
242
+ ```typescript
243
+ // Full union of all selected items across every group
244
+ const all: ReadonlySet<T> = lister.selectedItems;
245
+
246
+ // Or via the grouped-select event (see Events)
247
+ lister.addEventListener('grouped-select', ({ detail }) => {
248
+ const { selected, groupKey } = detail;
249
+ });
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Dispatching commands
255
+
256
+ `lister.dispatch(commandId, args?)` forwards a command to every live sub-component by calling their `dispatch` method. Use this to apply global operations — sort order, view mode, filter string — without knowing which groups exist or how many sub-components are active.
257
+
258
+ ```typescript
259
+ // Sort all groups alphabetically
260
+ lister.dispatch('sort-alpha');
261
+
262
+ // Pass arguments
263
+ lister.dispatch('filter', { query: 'image' });
264
+ ```
265
+
266
+ Sub-components receive the command via their `dispatch` method. Both built-in adapters expose an `onDispatch` option for handling commands.
267
+
268
+ ---
269
+
270
+ ## Events
271
+
272
+ ### `grouped-select`
273
+
274
+ Fired by `ixfx-grouped-item-lister` itself after any sub-component's selection changes (and after cross-group enforcement has been applied). The detail carries the **full cross-group union** — it is the single place to observe the overall selection state regardless of mode.
275
+
276
+ ```typescript
277
+ lister.addEventListener('grouped-select', ({ detail }) => {
278
+ const { selected, groupKey } = detail;
279
+ // selected: ReadonlySet<T> — union across all groups
280
+ // groupKey: string | undefined — key of the group that changed;
281
+ // undefined in ungrouped mode
282
+ });
283
+ ```
284
+
285
+ ### Bubbled sub-component events
286
+
287
+ Because the component uses Light DOM, events fired by sub-components bubble up through `ixfx-grouped-item-lister` unchanged. Register listeners on the lister element to handle them globally:
288
+
289
+ ```typescript
290
+ lister.addEventListener('activate', event => {
291
+ const { item } = (event as CustomEvent<{ item: T }>).detail;
292
+ console.log('activated:', item);
293
+ });
294
+
295
+ lister.addEventListener('item-click', event => { /* ... */ });
296
+ lister.addEventListener('tickle', event => { /* ... */ });
297
+ ```
298
+
299
+ `event.target` is the sub-component element that fired the event. In grouped mode, querying `event.target.closest('ixfx-panel')` gives the enclosing panel.
300
+
301
+ ### Full event reference
302
+
303
+ | Event | Source | Detail | When fired |
304
+ |---|---|---|---|
305
+ | `grouped-select` | `ixfx-grouped-item-lister` | `{ selected: ReadonlySet<T>, groupKey: string \| undefined }` | After any sub-component fires `select` |
306
+ | `select` | sub-component | `{ selected: ReadonlySet<T>, previous: ReadonlySet<T> }` | Selection changes in a single group |
307
+ | `activate` | sub-component | `{ item: T }` | User double-clicks or presses Enter on an item |
308
+ | `item-click` | sub-component | `{ item: T }` | Raw click on an item |
309
+ | `tickle` | sub-component | `{ item: T }` | Pointer hovers over or keyboard cursor moves to an item |
310
+
311
+ ---
312
+
313
+ ## Writing a custom adapter
314
+
315
+ To use a different rendering component (your own Lit element, a React portal, a canvas renderer, etc.), implement `IGroupedSubComponent<T>` and return an `HTMLElement` that satisfies it from `factory`.
316
+
317
+ ### Interface
318
+
319
+ ```typescript
320
+ interface IGroupedSubComponent<T> extends HTMLElement {
321
+ /** Replace the full item list. Called whenever items or grouping changes. */
322
+ setItems(items: readonly T[]): void;
323
+
324
+ /**
325
+ * Clear the selection. Called by the lister when enforcing cross-group
326
+ * selection. The implementation MAY fire a 'select' event — the lister
327
+ * guards against re-entrant processing.
328
+ */
329
+ clearSelection(): void;
330
+
331
+ readonly selectedItems: ReadonlySet<T>;
332
+ selectionMode: 'none' | 'single' | 'multiple';
333
+
334
+ /** Receives commands from lister.dispatch(). */
335
+ dispatch(commandId: string, args?: Record<string, unknown>): void;
336
+ }
337
+ ```
338
+
339
+ ### Required events
340
+
341
+ Sub-components must fire these events with `bubbles: true, composed: true`:
342
+
343
+ | Event | Detail | When |
344
+ |---|---|---|
345
+ | `select` | `{ selected: ReadonlySet<T>, previous: ReadonlySet<T> }` | Selection changes |
346
+ | `activate` | `{ item: T }` | Double-click or Enter |
347
+ | `item-click` | `{ item: T }` | Raw click on item |
348
+ | `tickle` | `{ item: T }` | Hover or keyboard cursor |
349
+
350
+ ### Minimal example
351
+
352
+ ```typescript
353
+ class MyItemList extends HTMLElement implements IGroupedSubComponent<MyItem> {
354
+ #items: readonly MyItem[] = [];
355
+ #selected = new Set<MyItem>();
356
+
357
+ selectionMode: 'none' | 'single' | 'multiple' = 'single';
358
+
359
+ get selectedItems(): ReadonlySet<MyItem> { return this.#selected; }
360
+
361
+ setItems(items: readonly MyItem[]): void {
362
+ this.#items = items;
363
+ this.#render();
364
+ }
365
+
366
+ clearSelection(): void {
367
+ if (this.#selected.size === 0) return;
368
+ const previous = new Set(this.#selected);
369
+ this.#selected.clear();
370
+ this.#syncStyles();
371
+ this.dispatchEvent(new CustomEvent('select', {
372
+ detail: { selected: new Set(), previous },
373
+ bubbles: true, composed: true,
374
+ }));
375
+ }
376
+
377
+ dispatch(commandId: string, _args?: Record<string, unknown>): void {
378
+ // handle commands
379
+ }
380
+
381
+ #render(): void {
382
+ this.innerHTML = '';
383
+ for (const item of this.#items) {
384
+ const row = document.createElement('div');
385
+ row.textContent = item.label;
386
+ row.addEventListener('click', () => this.#handleClick(item));
387
+ row.addEventListener('dblclick', () =>
388
+ this.dispatchEvent(new CustomEvent('activate', {
389
+ detail: { item }, bubbles: true, composed: true,
390
+ })),
391
+ );
392
+ row.addEventListener('pointerover', () =>
393
+ this.dispatchEvent(new CustomEvent('tickle', {
394
+ detail: { item }, bubbles: true, composed: true,
395
+ })),
396
+ );
397
+ this.appendChild(row);
398
+ }
399
+ }
400
+
401
+ #handleClick(item: MyItem): void {
402
+ this.dispatchEvent(new CustomEvent('item-click', {
403
+ detail: { item }, bubbles: true, composed: true,
404
+ }));
405
+ const previous = new Set(this.#selected);
406
+ if (this.selectionMode === 'single') this.#selected.clear();
407
+ this.#selected.add(item);
408
+ this.#syncStyles();
409
+ this.dispatchEvent(new CustomEvent('select', {
410
+ detail: { selected: new Set(this.#selected), previous },
411
+ bubbles: true, composed: true,
412
+ }));
413
+ }
414
+
415
+ #syncStyles(): void { /* update [aria-selected] etc. */ }
416
+ }
417
+
418
+ customElements.define('my-item-list', MyItemList);
419
+
420
+ // Use it
421
+ lister.factory = () => document.createElement('my-item-list') as MyItemList;
422
+ ```
423
+
424
+ > **Constructor constraint**: Custom elements cannot call `this.appendChild()` in their constructor. If your sub-component needs child elements, create them lazily on the first `setItems()` call or in `connectedCallback()`.
@@ -0,0 +1,191 @@
1
+ # Icon Registry
2
+
3
+ A centralized SVG icon system for `@ixfx/components`. Icons are stored by name, can be overridden globally, and all components that use a given icon re-render automatically when it changes.
4
+
5
+ ## Built-in icons
6
+
7
+ | Name | Used by | Fill |
8
+ |---|---|---|
9
+ | `chevron-down` | `<ixfx-split-button>`, `<ixfx-panel>` | `currentColor` |
10
+ | `check` | Available for use | `currentColor` |
11
+ | `close` | `<ixfx-panel>`, `<ixfx-split-button>` | `currentColor` |
12
+
13
+ ## Overriding a built-in icon
14
+
15
+ Call `registerIcon` before or after the library loads — components re-render automatically.
16
+
17
+ ```ts
18
+ import { registerIcon } from '@ixfx/components';
19
+
20
+ registerIcon('chevron-down', `<svg viewBox="0 0 24 24" fill="currentColor">
21
+ <path d="M7 10l5 5 5-5H7z"/>
22
+ </svg>`);
23
+ ```
24
+
25
+ The split-button's toggle arrow updates immediately on the next render cycle.
26
+
27
+ ## Adding a custom icon
28
+
29
+ ```ts
30
+ import { registerIcon } from '@ixfx/components';
31
+
32
+ registerIcon('my-logo', `<svg viewBox="0 0 24 24" fill="currentColor">
33
+ <path d="..."/>
34
+ </svg>`);
35
+ ```
36
+
37
+ Then use it anywhere in your app:
38
+
39
+ ```html
40
+ <ixfx-icon name="my-logo"></ixfx-icon>
41
+ ```
42
+
43
+ ## `<ixfx-icon>` element
44
+
45
+ Renders an icon from the registry by name. Re-renders automatically if the icon is replaced via `registerIcon`.
46
+
47
+ ```html
48
+ <!-- Basic usage -->
49
+ <ixfx-icon name="chevron-down"></ixfx-icon>
50
+
51
+ <!-- Fixed pixel size -->
52
+ <ixfx-icon name="close" size="32"></ixfx-icon>
53
+
54
+ <!-- Accessible label (removes aria-hidden, adds aria-label) -->
55
+ <ixfx-icon name="close" label="Close dialog"></ixfx-icon>
56
+
57
+ <!-- Rotate the icon (e.g. for expand/collapse chevrons) -->
58
+ <ixfx-icon name="chevron-down" rotate="-90deg"></ixfx-icon>
59
+
60
+ <!-- Prominence mode for idle/tickled/disabled states -->
61
+ <ixfx-icon name="settings" prominence></ixfx-icon>
62
+ ```
63
+
64
+ ### Attributes
65
+
66
+ | Attribute | Type | Default | Description |
67
+ |---|---|---|---|
68
+ | `name` | `string` | `''` | Registry key of the icon to render |
69
+ | `size` | `number` | — | Explicit `width`/`height` in px. Omit to size via CSS (`1em × 1em` default) |
70
+ | `label` | `string` | `''` | Accessible label. When set, adds `aria-label` and removes `aria-hidden` |
71
+ | `rotate` | `string` | `''` | CSS angle (e.g. `"90deg"`, `"-90deg"`) to rotate the icon. Animatable with `--transition` |
72
+ | `prominence` | `boolean` | `false` | When set, enables prominence styling (opacity transitions on hover/disabled) |
73
+
74
+ ### Sizing via CSS
75
+
76
+ By default `<ixfx-icon>` is `1em × 1em`, inheriting the surrounding font size. Override with CSS:
77
+
78
+ ```css
79
+ ixfx-icon {
80
+ width: 24px;
81
+ height: 24px;
82
+ }
83
+ ```
84
+
85
+ Or inline:
86
+
87
+ ```html
88
+ <ixfx-icon name="check" style="width:20px;height:20px"></ixfx-icon>
89
+ ```
90
+
91
+ ### Rotation
92
+
93
+ Use the `rotate` attribute to rotate an icon by a CSS angle. The rotation is applied as a CSS transform and is animatable:
94
+
95
+ ```html
96
+ <!-- Collapsed state: chevron points right -->
97
+ <ixfx-icon name="chevron-down" rotate="-90deg"></ixfx-icon>
98
+
99
+ <!-- Expanded state: chevron points down (no rotation) -->
100
+ <ixfx-icon name="chevron-down"></ixfx-icon>
101
+ ```
102
+
103
+ The rotation uses a CSS variable internally, so it transitions smoothly when the attribute changes (requires `--transition` to be set).
104
+
105
+ ### Prominence mode
106
+
107
+ The `prominence` attribute enables automatic opacity styling for idle, hover/tickled, and disabled states. When present, the icon uses CSS custom properties:
108
+
109
+ ```css
110
+ ixfx-icon {
111
+ opacity: var(--prominence-idle-opacity, 1); /* default */
112
+ }
113
+
114
+ ixfx-icon:hover {
115
+ opacity: var(--prominence-tickled-opacity, 1);
116
+ }
117
+
118
+ ixfx-icon[disabled] {
119
+ opacity: var(--prominence-disabled-opacity, 0.5);
120
+ }
121
+ ```
122
+
123
+ Without `prominence`, the icon uses `--opacity` (defaults to 1).
124
+
125
+ ## CSS custom properties
126
+
127
+ | Property | Default | Description |
128
+ |---|---|---|
129
+ | `--opacity` | `1` | Icon opacity when `prominence` is NOT set |
130
+ | `--prominence-idle-opacity` | `0.6` | Opacity when `prominence` is set and idle |
131
+ | `--prominence-tickled-opacity` | `1` | Opacity on hover (when `prominence` is set) |
132
+ | `--prominence-disabled-opacity` | `0.5` | Opacity when disabled (when `prominence` is set) |
133
+ | `--transition` | `0.15s ease-out` | Transition duration for opacity and rotation |
134
+
135
+ ## API
136
+
137
+ ```ts
138
+ import { registerIcon, getIcon, hasIcon, svgToDataUri, iconBus } from '@ixfx/components';
139
+
140
+ // Add or replace an icon. Notifies all subscribers.
141
+ registerIcon(name: string, svg: string): void
142
+
143
+ // Retrieve an SVG string by name. Returns undefined if not found.
144
+ getIcon(name: string): string | undefined
145
+
146
+ // Check whether an icon is registered.
147
+ hasIcon(name: string): boolean
148
+
149
+ // Convert an SVG string to a CSS data-URI (e.g. for mask-image).
150
+ // svgToDataUri('<svg .../>') → 'url("data:image/svg+xml,...")'
151
+ svgToDataUri(svg: string): string
152
+
153
+ // EventTarget that emits 'ixfx-icon-change' CustomEvents when any icon changes.
154
+ // event.detail.name contains the changed icon name.
155
+ iconBus: EventTarget
156
+ ```
157
+
158
+ ## SVG format conventions for custom icons
159
+
160
+ - `viewBox="0 0 24 24"` — consistent with built-ins
161
+ - No `width` or `height` attributes on `<svg>` — let CSS control sizing
162
+ - `fill="currentColor"` for inline icons — inherits CSS `color`
163
+ - `fill="black"` for CSS mask icons — color comes from `background-color`
164
+ - `aria-hidden="true"` on the `<svg>` itself — the `<ixfx-icon>` wrapper handles accessibility
165
+
166
+ ## Subscribing to icon changes
167
+
168
+ Use `iconBus` if you need to react to registry changes outside of a component:
169
+
170
+ ```ts
171
+ import { iconBus } from '@ixfx/components';
172
+ import type { IconChangedEvent } from '@ixfx/components';
173
+
174
+ iconBus.addEventListener('ixfx-icon-change', (e) => {
175
+ const { name } = (e as IconChangedEvent).detail;
176
+ console.log(`Icon "${name}" was updated`);
177
+ });
178
+ ```
179
+
180
+ ## Importing raw SVG strings
181
+
182
+ The built-in SVG strings are exported as constants if you need them directly:
183
+
184
+ ```ts
185
+ import { ICON_CHEVRON_DOWN, ICON_CARET_RIGHT, ICON_CHECK, ICON_CLOSE } from '@ixfx/components';
186
+ ```
187
+
188
+ ## Notes
189
+
190
+ - The registry is a module singleton. If multiple copies of `@ixfx/components` are loaded in the same page (e.g. version mismatch in a monorepo), each copy has its own registry and bus — overrides in one will not propagate to the other.
191
+ - `registerIcon` uses `unsafeHTML` internally via `<ixfx-icon>`. Never pass untrusted user input as an SVG string.