@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,592 @@
1
+ # Tree Components — Common API Reference
2
+
3
+ Three components share a common `TreeComponent` interface: **`ixfx-tree-list`**, **`ixfx-miller-list`**, and **`ixfx-crumb-navigation`**. This document describes the shared API, plus component-specific behaviour where noted.
4
+
5
+ `ixfx-crumb-path` uses a fundamentally different string-based model and is not covered here.
6
+
7
+ ---
8
+
9
+ ## Contents
10
+
11
+ 1. [Data model](#data-model)
12
+ 2. [Properties](#properties)
13
+ 3. [Selection](#selection)
14
+ 4. [Events](#events)
15
+ 5. [Keyboard navigation](#keyboard-navigation)
16
+ 6. [Incremental search](#incremental-search)
17
+ 7. [ARIA](#aria)
18
+ 8. [CSS variables](#css-variables)
19
+ 9. [Controller authoring guide](#controller-authoring-guide)
20
+
21
+ ---
22
+
23
+ ## Data model
24
+
25
+ ### Core types
26
+
27
+ ```typescript
28
+ type TreeItem = {
29
+ key: string; // Unique identifier across the whole tree
30
+ label: string; // Display text
31
+ isLeaf?: boolean; // true = never expandable; undefined/false = branch
32
+ data?: unknown; // Arbitrary user payload
33
+ icon?: string; // Icon name (passed to <ixfx-icon>)
34
+ tooltip?: string; // Tooltip content
35
+ value?: string; // Optional machine-readable value
36
+ };
37
+
38
+ type TreeNode = {
39
+ item: TreeItem;
40
+ children?: TreeNode[]; // undefined = unloaded; [] = confirmed empty
41
+ };
42
+ ```
43
+
44
+ `TreeNode` is **immutable** — structural changes always produce a new root. Never mutate a node in place; use `TreeDataModel` convenience methods or replace `root` directly.
45
+
46
+ ---
47
+
48
+ ### Static trees
49
+
50
+ Set `root` directly when the tree data is available up-front:
51
+
52
+ ```typescript
53
+ el.root = {
54
+ item: { key: 'root', label: 'My Files' },
55
+ children: [
56
+ { item: { key: 'docs', label: 'Documents' }, children: [] },
57
+ { item: { key: 'img', label: 'Images', isLeaf: true } },
58
+ ],
59
+ };
60
+ ```
61
+
62
+ ---
63
+
64
+ ### Lazy loading via `loadChildren`
65
+
66
+ Set `loadChildren` to fetch children on demand (called when a branch is expanded):
67
+
68
+ ```typescript
69
+ el.loadChildren = async ({ node, depth }, signal) => {
70
+ const res = await fetch(`/api/children/${node.item.key}`, { signal });
71
+ return res.json(); // TreeNode[]
72
+ };
73
+ ```
74
+
75
+ The callback receives an `AbortSignal`; honour it to avoid races when the user collapses quickly. It may return `TreeNode[]`, `Promise<TreeNode[]>`, or `AsyncIterable<DataBatch<TreeNode>>` for streaming children.
76
+
77
+ ---
78
+
79
+ ### `TreeDataModel` — shared, cached data layer
80
+
81
+ `TreeDataModel` is an optional shared data layer that owns child-loading and caching. It is useful when:
82
+
83
+ - Multiple components display the same dataset (e.g. a tree-list and a miller-list side-by-side).
84
+ - You need programmatic tree mutation without tracking the root yourself.
85
+ - You need to invalidate (refresh) individual branches without a full re-render.
86
+
87
+ ```typescript
88
+ import { TreeDataModel } from '@ixfx/components';
89
+
90
+ const model = new TreeDataModel({
91
+ root: { item: { key: 'root', label: 'Root' }, children: undefined },
92
+ loadChildren: async ({ node }, signal) => {
93
+ const res = await fetch(`/api/children/${node.item.key}`, { signal });
94
+ return res.json();
95
+ },
96
+ });
97
+
98
+ treeEl.model = model;
99
+ millerEl.model = model; // Both components share the same cache
100
+ ```
101
+
102
+ #### Model API
103
+
104
+ | Member | Signature | Description |
105
+ |--------|-----------|-------------|
106
+ | `root` | `get root(): TreeNode` | Current immutable root snapshot |
107
+ | `setRoot` | `(root: TreeNode): void` | Replace the root; notifies all connected components |
108
+ | `getChildren` | `(node, depth, signal): Promise<TreeNode[]>` | Returns cached children or calls `loadChildren`; cache keyed by `node.item.key` |
109
+ | `invalidate` | `(nodeKey: string): void` | Clears cached children for `nodeKey`; connected components collapse that branch so the next expand triggers a fresh fetch |
110
+ | `addNode` | `(parentKey, node): TreeNode` | Appends a child; returns new root |
111
+ | `removeNode` | `(key): TreeNode` | Removes node and all descendants; returns new root |
112
+ | `updateItem` | `(key, patch): TreeNode` | Merges `patch` into the item at `key`; returns new root |
113
+ | `moveNode` | `(key, newParentKey): TreeNode` | Moves a node under a new parent; returns new root |
114
+ | `_subscribe` | `(cb): () => void` | Internal subscription used by components; returns unsubscribe |
115
+
116
+ #### How caching works
117
+
118
+ `getChildren()` stores results in a `Map<string, TreeNode[]>` keyed by `node.item.key`. A cache hit returns the stored array synchronously (wrapped in a resolved `Promise`). `invalidate(key)` deletes the cache entry and emits an `invalidate` event; connected components collapse the branch so the next expand triggers a fresh load.
119
+
120
+ Mutator methods (`addNode`, `removeNode`, `updateItem`, `moveNode`) apply pure transformations to produce a new immutable root, then emit a `root` event so all connected components re-render.
121
+
122
+ #### Model vs `loadChildren`
123
+
124
+ When both `model` and `loadChildren` are set, `model` takes precedence. `loadChildren` remains available for simple cases that do not need sharing or programmatic mutation.
125
+
126
+ ---
127
+
128
+ ## Properties
129
+
130
+ These properties are present on all three components (`ixfx-tree-list`, `ixfx-miller-list`, `ixfx-crumb-navigation`).
131
+
132
+ | Property / Attribute | Type | Default | Description |
133
+ |----------------------|------|---------|-------------|
134
+ | `root` | `TreeNode \| undefined` | `undefined` | Root node of the tree |
135
+ | `model` | `TreeDataModel \| undefined` | `undefined` | Shared data model; takes precedence over `loadChildren` |
136
+ | `loadChildren` | `LoadChildrenCallback \| undefined` | `undefined` | Callback for lazy child loading |
137
+ | `selectedNode` | `TreeNode \| undefined` | `undefined` | Primary (most recent) selected node |
138
+ | `selectedNodes` | `ReadonlySet<TreeNode>` | `{}` | Full selection set (read-only) |
139
+ | `interactionMode` | `TreeInteractionMode` | `'standard'` | How gestures map to selection changes (`ixfx-tree-list` / `ixfx-miller-list` only) |
140
+ | `selectionFilter` | `'none' \| 'leaf' \| 'branch'` | `'leaf'` | Which node kinds can be selected (reflected attribute) |
141
+ | `filterPredicate` | `TreeFilterPredicate \| undefined` | `undefined` | Filter; only nodes returning `true` are shown |
142
+ | `exclusivity` | `'none' \| 'depth' \| 'global'` | `'none'` | Expansion exclusivity (reflected attribute) |
143
+
144
+ ### `interactionMode` (`ixfx-tree-list` / `ixfx-miller-list`)
145
+
146
+ Controls how pointer and keyboard gestures translate to selection changes.
147
+
148
+ | Value | Behaviour |
149
+ |-------|-----------|
150
+ | `'implicit'` | Single selection follows every click; click also toggles expand/collapse |
151
+ | `'standard'` | Plain click selects + toggles expand; Ctrl/Cmd+click toggles item; Shift+click range-selects |
152
+ | `'manual'` | Component never auto-selects; use `select()` / `deselect()` / `clearSelection()` to drive state |
153
+ | `'checked'` | Checkboxes on every row; checkbox click recursively selects/deselects loaded descendants; row click only expands/collapses |
154
+ | `'vscode'` | Like `standard` for clicks; Shift+Up/Down uses snapshot-based toggle-range (VS Code file-explorer style) |
155
+
156
+ Switching `interactionMode` clears the current selection.
157
+
158
+ ### `selectionFilter`
159
+
160
+ | Value | Behaviour |
161
+ |-------|-----------|
162
+ | `'none'` | No nodes can be selected |
163
+ | `'leaf'` | Only leaf nodes (`isLeaf: true` or `children: []`) can be selected |
164
+ | `'branch'` | Only branch nodes can be selected |
165
+
166
+ ### `exclusivity`
167
+
168
+ Controls how expansion of one branch affects others.
169
+
170
+ | Value | Behaviour |
171
+ |-------|-----------|
172
+ | `'none'` | Any number of branches can be open simultaneously |
173
+ | `'depth'` | Opening a branch collapses its siblings at the same depth (accordion) |
174
+ | `'global'` | Opening any branch collapses all other open branches in the tree |
175
+
176
+ `exclusivity="depth"` replaces the old `accordion` boolean attribute. To migrate: `accordion` → `exclusivity="depth"`.
177
+
178
+ ---
179
+
180
+ ## Selection
181
+
182
+ ### Programmatic selection
183
+
184
+ ```typescript
185
+ // Select a node (respects selectionFilter; always replaces selection)
186
+ el.select(node);
187
+
188
+ // Remove a specific node from the selection
189
+ el.deselect(node);
190
+
191
+ // Clear the entire selection
192
+ el.clearSelection();
193
+ ```
194
+
195
+ ### Reading the selection
196
+
197
+ ```typescript
198
+ // Primary (most-recent) selected node
199
+ console.log(el.selectedNode?.item.label);
200
+
201
+ // Full selection set (for multi-select)
202
+ for (const node of el.selectedNodes) {
203
+ console.log(node.item.label);
204
+ }
205
+ ```
206
+
207
+ ### Listening for selection changes
208
+
209
+ ```typescript
210
+ el.addEventListener('select', ({ detail }) => {
211
+ const { selected, previous } = detail;
212
+ console.log('Selected:', [...selected].map(n => n.item.label));
213
+ console.log('Previously:', [...previous].map(n => n.item.label));
214
+ });
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Events
220
+
221
+ All events **bubble** and are **composed** (cross shadow-DOM boundaries).
222
+
223
+ | Event | Detail type | When fired |
224
+ |-------|-------------|------------|
225
+ | `select` | `TreeSelectDetail` | Selection set changes by any means (click, keyboard, programmatic) |
226
+ | `tickle` | `TreeTickleDetail` | An item enters cursor focus: pointer hover OR keyboard navigation cursor |
227
+ | `activate` | `TreeActivateDetail` | User explicitly activates an item (Enter key or double-click) |
228
+ | `item-click` | `{ node: TreeNode; depth: number }` | Raw pointer click on any item |
229
+ | `expand` | `{ node: TreeNode; depth: number }` | A branch expands |
230
+ | `collapse` | `{ node: TreeNode; depth: number }` | A branch collapses |
231
+
232
+ ### Detail types
233
+
234
+ ```typescript
235
+ type TreeSelectDetail = {
236
+ readonly selected: ReadonlySet<TreeNode>; // Current full selection
237
+ readonly previous: ReadonlySet<TreeNode>; // Selection before this change
238
+ };
239
+
240
+ type TreeTickleDetail = {
241
+ readonly node: TreeNode;
242
+ readonly depth: number;
243
+ };
244
+
245
+ type TreeActivateDetail = {
246
+ readonly node: TreeNode;
247
+ readonly depth: number;
248
+ };
249
+ ```
250
+
251
+ ### Usage example
252
+
253
+ ```typescript
254
+ el.addEventListener('select', ({ detail }) => {
255
+ // Works for both single and multi-select
256
+ const first = [...detail.selected][0];
257
+ if (first) console.log('Primary selection:', first.item.label);
258
+ });
259
+
260
+ el.addEventListener('tickle', ({ detail }) => {
261
+ // Show a preview pane as the user hovers
262
+ preview.setNode(detail.node);
263
+ });
264
+
265
+ el.addEventListener('activate', ({ detail }) => {
266
+ // Open the item (e.g. navigate to a route)
267
+ router.navigate(detail.node.item.key);
268
+ });
269
+ ```
270
+
271
+ ### Migration from old event shapes
272
+
273
+ | Old | New |
274
+ |-----|-----|
275
+ | `select` → `{ selected: TreeNode, previous: TreeNode \| undefined }` | `select` → `TreeSelectDetail` (set-based) |
276
+ | `change` (Miller) | `select` (set-based) |
277
+ | Miller `select` (Enter key) | `activate` |
278
+ | no tickle event | `tickle` |
279
+
280
+ ---
281
+
282
+ ## Keyboard navigation
283
+
284
+ The element must be focused (click it or tab to it) before keyboard navigation works.
285
+
286
+ ### `ixfx-tree-list`
287
+
288
+ | Key | Action |
289
+ |-----|--------|
290
+ | `Arrow Up` | Move cursor to previous visible item |
291
+ | `Arrow Down` | Move cursor to next visible item |
292
+ | `Arrow Right` (on collapsed branch) | Expand and move into first child |
293
+ | `Arrow Right` (on expanded branch) | Move into first child |
294
+ | `Arrow Right` (on leaf) | No-op |
295
+ | `Arrow Left` (on expanded branch) | Collapse branch |
296
+ | `Arrow Left` (on leaf or collapsed branch) | Move to parent |
297
+ | `Enter` | Select and activate focused item |
298
+ | `Escape` | Cancel in-flight lazy load |
299
+
300
+ Moving the cursor fires a `tickle` event. Pressing Enter fires `select` then `activate`.
301
+
302
+ ### `ixfx-miller-list`
303
+
304
+ | Key | Action |
305
+ |-----|--------|
306
+ | `Arrow Up / Down` | Move within the active column |
307
+ | `Arrow Right` | Expand (open next column for) highlighted branch |
308
+ | `Arrow Left` | Go back to the parent column |
309
+ | `Enter` | Activate the highlighted item (`activate` event) |
310
+ | `Escape` | Cancel pending operations |
311
+
312
+ ### `ixfx-crumb-navigation`
313
+
314
+ Keyboard navigation follows the crumb path. Arrow keys move between breadcrumb segments; clicking or Enter expands the next level.
315
+
316
+ ---
317
+
318
+ ## Incremental search
319
+
320
+ Wire up a text input to filter and highlight matching nodes using `IncrSearchTreeController`:
321
+
322
+ ```typescript
323
+ import { IncrSearchTreeController } from '@ixfx/components';
324
+
325
+ const ctrl = new IncrSearchTreeController(el, {
326
+ selector: item => item.node.item.label, // Text to search against
327
+ input: document.querySelector('#search'), // Optional: auto-wires input/keydown
328
+ });
329
+ ctrl.connect();
330
+
331
+ // Clean up when done
332
+ ctrl.disconnect();
333
+ ```
334
+
335
+ ### Options
336
+
337
+ ```typescript
338
+ type IncrSearchTreeOptions = {
339
+ /** How to extract searchable text from each node */
340
+ selector: (item: { node: TreeNode; depth: number }) => string;
341
+
342
+ /**
343
+ * CSS selector for label elements inside the shadow root.
344
+ * Default: '.tree-label' (ixfx-tree-list)
345
+ * Use '.label' for ixfx-miller-list
346
+ */
347
+ labelSelector?: string;
348
+
349
+ /** Input element to auto-wire. Esc clears the query. */
350
+ input?: HTMLInputElement;
351
+
352
+ /** fzf matching options */
353
+ fzf?: Partial<FzfOptions<{ node: TreeNode; depth: number }>>;
354
+ };
355
+ ```
356
+
357
+ ### Manual control
358
+
359
+ ```typescript
360
+ await ctrl.setQuery('search text');
361
+ await ctrl.clearQuery();
362
+ ```
363
+
364
+ ### Using with miller-list
365
+
366
+ ```typescript
367
+ const ctrl = new IncrSearchTreeController(millerEl, {
368
+ selector: item => item.node.item.label,
369
+ labelSelector: '.label', // Miller uses .label, not .tree-label
370
+ input: searchInput,
371
+ });
372
+ ctrl.connect();
373
+ ```
374
+
375
+ ### How it works
376
+
377
+ 1. `connect()` calls `el.getNodes()` to snapshot the full item list.
378
+ 2. On each query change the controller runs fzf fuzzy matching and sets `el.filterPredicate` to a function that returns `true` only for matching nodes.
379
+ 3. Matched label text is highlighted in the shadow DOM using the CSS Highlight API via `applyHighlights()`.
380
+ 4. `disconnect()` clears `filterPredicate` and all highlights.
381
+
382
+ ---
383
+
384
+ ## ARIA
385
+
386
+ Each tree-like component must implement the following. These are enforced by the shared `TreeComponent` interface and implemented in `TreeBaseElement` / `MillerBaseElement` / `CrumbNavigationElement`.
387
+
388
+ **Container element**
389
+
390
+ - [ ] `role="tree"` on the outermost interactive container
391
+ - [ ] `aria-label` or `aria-labelledby` to name the tree
392
+
393
+ **Each item element**
394
+
395
+ - [ ] `role="treeitem"`
396
+ - [ ] `aria-level` (1-based depth)
397
+ - [ ] `aria-setsize` (total siblings in the same group)
398
+ - [ ] `aria-posinset` (1-based position among siblings)
399
+ - [ ] `aria-selected="true|false"`
400
+
401
+ **Expandable items**
402
+
403
+ - [ ] `aria-expanded="true"` when open, `"false"` when closed
404
+ - [ ] Omit `aria-expanded` on leaf nodes
405
+
406
+ **Child groups**
407
+
408
+ - [ ] Wrap children in `<ul role="group">` (or equivalent `role="group"` container)
409
+
410
+ **Miller columns specifically**
411
+
412
+ Each column is a separate `role="listbox"` with `aria-label` showing the parent node label. Items use `role="option"` and `aria-selected`.
413
+
414
+ ---
415
+
416
+ ## CSS variables
417
+
418
+ ### Shared / themed
419
+
420
+ These are set by the global theme and inherited by all tree components.
421
+
422
+ | Variable | Description |
423
+ |----------|-------------|
424
+ | `--accent` | Selected item background |
425
+ | `--accent-text` | Selected item text |
426
+ | `--surface-3` | Panel background |
427
+ | `--surface-3-text` | Default item text |
428
+ | `--surface-muted-text` | Muted text (carets, secondary info) |
429
+ | `--surface-h` | Hover background |
430
+ | `--radius-s` | Small border radius |
431
+ | `--transition` | Default transition timing |
432
+ | `--space-xs` | Extra-small spacing |
433
+ | `--space-s` | Small spacing |
434
+ | `--space-m` | Medium spacing |
435
+ | `--font-family` | Font family |
436
+ | `--text-m` | Base text size |
437
+ | `--text-l` | Large text size |
438
+
439
+ ### `ixfx-tree-list` specific
440
+
441
+ | Variable | Default | Description |
442
+ |----------|---------|-------------|
443
+ | `--tree-indent` | `20px` | Indentation per depth level |
444
+ | `--tree-item-height` | `28px` | Height of each row |
445
+ | `--tree-caret-size` | `10px` | Expand/collapse caret size |
446
+
447
+ ### `ixfx-miller-list` specific
448
+
449
+ | Variable | Default | Description |
450
+ |----------|---------|-------------|
451
+ | `--miller-column-width` | `180px` | Width of each column |
452
+ | `--miller-column-max-height` | `320px` | Maximum column height before scroll |
453
+ | `--item-hover-bg` | `var(--surface-h)` | Hover background |
454
+ | `--item-selected-bg` | `var(--accent)` | Selected item background |
455
+ | `--item-selected-text` | `#fff` | Selected item text |
456
+ | `--item-open-bg` | (theme) | Background for items on the open path |
457
+ | `--icon-size` | `1em` | Icon dimensions |
458
+ | `--icon-spacing` | `0.3em` | Gap between icon and label |
459
+
460
+ ---
461
+
462
+ ## Controller authoring guide
463
+
464
+ A controller operates against the `TreeComponent` interface. This means the same controller works with any of the three components.
465
+
466
+ ### Minimal controller
467
+
468
+ ```typescript
469
+ import type { TreeComponent, TreeNode } from '@ixfx/components';
470
+
471
+ export class MyController {
472
+ #el: TreeComponent;
473
+ #unsub?: () => void;
474
+
475
+ constructor(el: TreeComponent) {
476
+ this.#el = el;
477
+ }
478
+
479
+ connect(): void {
480
+ this.disconnect();
481
+ this.#el.addEventListener('select', this.#onSelect);
482
+ this.#el.addEventListener('tickle', this.#onTickle);
483
+ }
484
+
485
+ disconnect(): void {
486
+ this.#el.removeEventListener('select', this.#onSelect);
487
+ this.#el.removeEventListener('tickle', this.#onTickle);
488
+ this.#unsub?.();
489
+ this.#unsub = undefined;
490
+ }
491
+
492
+ #onSelect = (e: CustomEvent) => {
493
+ const { selected } = e.detail;
494
+ for (const node of selected as ReadonlySet<TreeNode>) {
495
+ console.log('selected:', node.item.label);
496
+ }
497
+ };
498
+
499
+ #onTickle = (e: CustomEvent) => {
500
+ const { node, depth } = e.detail;
501
+ console.log('tickled:', node.item.label, 'at depth', depth);
502
+ };
503
+ }
504
+ ```
505
+
506
+ ### Querying all nodes
507
+
508
+ ```typescript
509
+ // Snapshot of all visible nodes with their depth
510
+ const nodes = el.getNodes(); // ReadonlyArray<{ node: TreeNode; depth: number }>
511
+ ```
512
+
513
+ ### Filtering
514
+
515
+ ```typescript
516
+ // Only show nodes matching a predicate
517
+ el.filterPredicate = (node, depth) => node.item.label.startsWith('A');
518
+
519
+ // Clear filter
520
+ el.filterPredicate = undefined;
521
+ ```
522
+
523
+ ### Selecting programmatically
524
+
525
+ ```typescript
526
+ // Honour selectionMode and selectionFilter automatically
527
+ el.select(someNode);
528
+
529
+ // Or navigate to a node by key path and select it
530
+ el.navigateTo(['root', 'documents', 'readme.md']);
531
+ ```
532
+
533
+ ### Using `TreeSearchHost` for shadow-DOM access
534
+
535
+ Controllers that need to touch the shadow DOM (e.g. for highlight injection) should type the host as `TreeSearchHost` rather than `TreeComponent`:
536
+
537
+ ```typescript
538
+ import type { TreeSearchHost } from '@ixfx/components';
539
+
540
+ export class HighlightController {
541
+ #el: TreeSearchHost;
542
+
543
+ constructor(el: TreeSearchHost) {
544
+ this.#el = el;
545
+ }
546
+
547
+ async highlight(selector: string): Promise<void> {
548
+ await this.#el.updateComplete;
549
+ const labels = this.#el.shadowRoot?.querySelectorAll(selector);
550
+ // ...
551
+ }
552
+ }
553
+ ```
554
+
555
+ ---
556
+
557
+ ## Types reference
558
+
559
+ ```typescript
560
+ type LoadChildrenCallback = (
561
+ query: { node: TreeNode; depth: number },
562
+ signal: AbortSignal
563
+ ) => TreeNode[] | Promise<TreeNode[]> | AsyncIterable<DataBatch<TreeNode>>;
564
+
565
+ type TreeFilterPredicate = (node: TreeNode, depth: number) => boolean;
566
+
567
+ type TreeExclusivity = 'none' | 'depth' | 'global';
568
+ type TreeSelectionFilter = 'none' | 'leaf' | 'branch';
569
+ type TreeInteractionMode = 'implicit' | 'standard' | 'manual' | 'checked' | 'vscode';
570
+
571
+ interface TreeComponent extends EventTarget {
572
+ root: TreeNode | undefined;
573
+ model: TreeDataModel | undefined;
574
+ loadChildren: LoadChildrenCallback | undefined;
575
+ selectedNode: TreeNode | undefined;
576
+ readonly selectedNodes: ReadonlySet<TreeNode>;
577
+ selectionFilter: TreeSelectionFilter;
578
+ filterPredicate: TreeFilterPredicate | undefined;
579
+ exclusivity: TreeExclusivity;
580
+ select(node: TreeNode): void;
581
+ deselect(node: TreeNode): void;
582
+ clearSelection(): void;
583
+ navigateTo(keys: readonly string[]): boolean;
584
+ navigateToNode(predicate: (node: TreeNode) => boolean): boolean;
585
+ getNodes(): ReadonlyArray<{ node: TreeNode; depth: number }>;
586
+ }
587
+
588
+ interface TreeSearchHost extends TreeComponent {
589
+ readonly shadowRoot: ShadowRoot | null;
590
+ readonly updateComplete: Promise<boolean>;
591
+ }
592
+ ```