@ixfx/components 0.6.2 → 0.7.1

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 (181) hide show
  1. package/bundle/index.d.ts +1918 -835
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +22137 -16840
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +1 -1
  6. package/dist/ac-text.d.ts +1 -1
  7. package/dist/ac-text.js +3 -2
  8. package/dist/ac-text.js.map +1 -1
  9. package/dist/{ac-token-BAa6ke09.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  10. package/dist/{ac-token-BAa6ke09.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  11. package/dist/ac-token.d.ts +1 -1
  12. package/dist/ac-token.js +3 -2
  13. package/dist/ac-token.js.map +1 -1
  14. package/dist/{button-Bn0BuLpO.js → button-DsFbV4XD.js} +5 -4
  15. package/dist/button-DsFbV4XD.js.map +1 -0
  16. package/dist/button.d.ts.map +1 -1
  17. package/dist/button.js +1 -1
  18. package/dist/checkbox.js +1 -1
  19. package/dist/{colour-picker-0stHB90J.js → colour-picker-Ddsk-7E_.js} +4 -3
  20. package/dist/{colour-picker-0stHB90J.js.map → colour-picker-Ddsk-7E_.js.map} +1 -1
  21. package/dist/colour-picker.js +1 -1
  22. package/dist/crumbs.d.ts +1 -1
  23. package/dist/crumbs.js +4 -2
  24. package/dist/crumbs.js.map +1 -1
  25. package/dist/data-display.js +1 -1
  26. package/dist/{data-grid-C8qIo1-u.d.ts → data-grid-Bh4YrQGK.d.ts} +2 -2
  27. package/dist/{data-grid-C8qIo1-u.d.ts.map → data-grid-Bh4YrQGK.d.ts.map} +1 -1
  28. package/dist/data-grid.d.ts +1 -1
  29. package/dist/data-grid.js +4 -2
  30. package/dist/data-grid.js.map +1 -1
  31. package/dist/{defaults-BCiDhZbF.js → defaults-BJwDsMH4.js} +27 -20
  32. package/dist/defaults-BJwDsMH4.js.map +1 -0
  33. package/dist/editable-label-rzXkjf1H.d.ts +75 -0
  34. package/dist/editable-label-rzXkjf1H.d.ts.map +1 -0
  35. package/dist/editable-label.d.ts +2 -75
  36. package/dist/editable-label.js +2 -1
  37. package/dist/editable-label.js.map +1 -1
  38. package/dist/{fallbacks-DEE1nCTt.js → fallbacks-DNI9xt0F.js} +2 -2
  39. package/dist/{fallbacks-DEE1nCTt.js.map → fallbacks-DNI9xt0F.js.map} +1 -1
  40. package/dist/{grouped-item-lister-_pMDwIo1.d.ts → grouped-item-lister-DnWB-eFT.d.ts} +2 -2
  41. package/dist/{grouped-item-lister-_pMDwIo1.d.ts.map → grouped-item-lister-DnWB-eFT.d.ts.map} +1 -1
  42. package/dist/grouped-item-lister.d.ts +1 -1
  43. package/dist/{hex-editor-Bk6rxhfg.js → hex-editor-BiRfwTz1.js} +2 -2
  44. package/dist/{hex-editor-Bk6rxhfg.js.map → hex-editor-BiRfwTz1.js.map} +1 -1
  45. package/dist/hex.js +1 -1
  46. package/dist/{icon-DpA9x2Ve.d.ts → icon-CDGprSdE.d.ts} +1 -12
  47. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  48. package/dist/{icons-Bm2ByQ3s.js → icon-DcEEGQa_.js} +38 -12
  49. package/dist/icon-DcEEGQa_.js.map +1 -0
  50. package/dist/icons.d.ts +3 -3
  51. package/dist/icons.js +9 -3
  52. package/dist/icons.js.map +1 -0
  53. package/dist/{index-CKtLhgYj.d.ts → index-9AhaHIT2.d.ts} +3 -3
  54. package/dist/{index-CKtLhgYj.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  55. package/dist/index-CG2vIgPv.d.ts +101 -0
  56. package/dist/index-CG2vIgPv.d.ts.map +1 -0
  57. package/dist/{index-BtGFYfaK.d.ts → index-CS9yM8L-.d.ts} +14 -4
  58. package/dist/index-CS9yM8L-.d.ts.map +1 -0
  59. package/dist/index.d.ts +861 -71
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +4257 -931
  62. package/dist/index.js.map +1 -1
  63. package/dist/interaction-D5XdKzbR.js +2 -0
  64. package/dist/labelled-radial-input.js +2 -1
  65. package/dist/labelled-radial-input.js.map +1 -1
  66. package/dist/labelled-slider-input-DzigjMwP.js +808 -0
  67. package/dist/labelled-slider-input-DzigjMwP.js.map +1 -0
  68. package/dist/labelled-slider-input.d.ts +2 -0
  69. package/dist/labelled-slider-input.js +2 -0
  70. package/dist/{types-DlVNJez_.d.ts → list-selection-types-DSuRNWpx.d.ts} +10 -2
  71. package/dist/list-selection-types-DSuRNWpx.d.ts.map +1 -0
  72. package/dist/menu-CXDFJxNm.js +825 -0
  73. package/dist/menu-CXDFJxNm.js.map +1 -0
  74. package/dist/{menu-item-Bs2MYyHq.d.ts → menu-item-CZi9vy5O.d.ts} +4 -2
  75. package/dist/menu-item-CZi9vy5O.d.ts.map +1 -0
  76. package/dist/menu-item-D2S4i8u1.js +809 -0
  77. package/dist/menu-item-D2S4i8u1.js.map +1 -0
  78. package/dist/menu.d.ts +2 -2
  79. package/dist/menu.js +2 -1
  80. package/dist/miller.d.ts +1 -1
  81. package/dist/miller.js +3 -2
  82. package/dist/miller.js.map +1 -1
  83. package/dist/narrowed-text.js +4 -2
  84. package/dist/narrowed-text.js.map +1 -1
  85. package/dist/notification-pill-DyZydXoZ.d.ts.map +1 -1
  86. package/dist/notifier.js +4 -3
  87. package/dist/notifier.js.map +1 -1
  88. package/dist/panel.d.ts +13 -13
  89. package/dist/panel.d.ts.map +1 -1
  90. package/dist/panel.js +37 -253
  91. package/dist/panel.js.map +1 -1
  92. package/dist/plots.js +1 -1
  93. package/dist/polar-pad.js +1 -1
  94. package/dist/{radial-input-Dk8Wydp5.js → radial-input-Dp5qz0ly.js} +2 -2
  95. package/dist/{radial-input-Dk8Wydp5.js.map → radial-input-Dp5qz0ly.js.map} +1 -1
  96. package/dist/radial-input.js +1 -1
  97. package/dist/range-input.js +1 -1
  98. package/dist/range.js +1 -1
  99. package/dist/registry-BEtb8_wB.js +46 -0
  100. package/dist/registry-BEtb8_wB.js.map +1 -0
  101. package/dist/select-horiz.js +3 -2
  102. package/dist/select-horiz.js.map +1 -1
  103. package/dist/{slider-input-XppH3vcy.d.ts → slider-input-BfRUW2ZB.d.ts} +16 -3
  104. package/dist/slider-input-BfRUW2ZB.d.ts.map +1 -0
  105. package/dist/{slider-input-CT4gcF-o.js → slider-input-De4eoT11.js} +382 -67
  106. package/dist/slider-input-De4eoT11.js.map +1 -0
  107. package/dist/slider-input.d.ts +1 -1
  108. package/dist/slider-input.js +1 -1
  109. package/dist/snackbar.js +1 -1
  110. package/dist/split-layout-BFSbUZQv.js +969 -0
  111. package/dist/split-layout-BFSbUZQv.js.map +1 -0
  112. package/dist/split-layout-JK3xWVX2.d.ts +179 -0
  113. package/dist/split-layout-JK3xWVX2.d.ts.map +1 -0
  114. package/dist/split-layout.d.ts +2 -60
  115. package/dist/split-layout.js +1 -617
  116. package/dist/style.css +1 -1
  117. package/dist/swipe.js +1 -1
  118. package/dist/{tab-list-D_1cYhB_.d.ts → tab-list-CGyPt07Z.d.ts} +10 -2
  119. package/dist/tab-list-CGyPt07Z.d.ts.map +1 -0
  120. package/dist/tabs.d.ts +1 -1
  121. package/dist/tabs.js +128 -27
  122. package/dist/tabs.js.map +1 -1
  123. package/dist/{tickled-controller-BtevrRLV.d.ts → tickled-controller-CRzdIEjJ.d.ts} +7 -1
  124. package/dist/tickled-controller-CRzdIEjJ.d.ts.map +1 -0
  125. package/dist/{tickled-controller-DkAU-0qL.js → tickled-controller-h9GmJ_bU.js} +8 -2
  126. package/dist/tickled-controller-h9GmJ_bU.js.map +1 -0
  127. package/dist/titlebar-Bj5pfdw_.d.ts +50 -0
  128. package/dist/titlebar-Bj5pfdw_.d.ts.map +1 -0
  129. package/dist/titlebar.d.ts +2 -0
  130. package/dist/titlebar.js +324 -0
  131. package/dist/titlebar.js.map +1 -0
  132. package/dist/tooltip-BlvBUQxx.d.ts.map +1 -1
  133. package/dist/{tree-BO6BXI2b.js → tree-Ch4W8qXQ.js} +4 -3
  134. package/dist/{tree-BO6BXI2b.js.map → tree-Ch4W8qXQ.js.map} +1 -1
  135. package/dist/tree.d.ts +1 -1
  136. package/dist/tree.js +1 -1
  137. package/dist/vertical-list-ClnqTkF3.js +814 -0
  138. package/dist/vertical-list-ClnqTkF3.js.map +1 -0
  139. package/dist/vertical-list.d.ts +8 -9
  140. package/dist/vertical-list.d.ts.map +1 -1
  141. package/dist/vertical-list.js +1 -736
  142. package/dist/{xy-axis-B6yMjckW.js → xy-axis-DWizs0bt.js} +109 -3
  143. package/dist/xy-axis-DWizs0bt.js.map +1 -0
  144. package/dist/xy-pad.js +1 -1
  145. package/docs-user/README.md +5 -0
  146. package/docs-user/detail-list.md +398 -0
  147. package/docs-user/dock.md +461 -0
  148. package/docs-user/icons.md +4 -0
  149. package/docs-user/index.json +28 -3
  150. package/docs-user/labelled-slider-input.md +198 -0
  151. package/docs-user/llms.txt +6 -1
  152. package/docs-user/panel.md +20 -1
  153. package/docs-user/slider-input.md +55 -15
  154. package/docs-user/split-layout.md +136 -170
  155. package/docs-user/tabs.md +54 -0
  156. package/docs-user/titlebar.md +102 -0
  157. package/docs-user/toolbar.md +95 -22
  158. package/docs-user/twosplit-layout.md +254 -0
  159. package/docs-user/user-catalog.md +8 -1
  160. package/docs-user/user-theming.md +109 -0
  161. package/llms.txt +6 -1
  162. package/package.json +1 -2
  163. package/dist/button-Bn0BuLpO.js.map +0 -1
  164. package/dist/defaults-BCiDhZbF.js.map +0 -1
  165. package/dist/editable-label.d.ts.map +0 -1
  166. package/dist/icon-DpA9x2Ve.d.ts.map +0 -1
  167. package/dist/icons-Bm2ByQ3s.js.map +0 -1
  168. package/dist/index-BtGFYfaK.d.ts.map +0 -1
  169. package/dist/menu-DIfU70AA.js +0 -1609
  170. package/dist/menu-DIfU70AA.js.map +0 -1
  171. package/dist/menu-item-Bs2MYyHq.d.ts.map +0 -1
  172. package/dist/slider-input-CT4gcF-o.js.map +0 -1
  173. package/dist/slider-input-XppH3vcy.d.ts.map +0 -1
  174. package/dist/split-layout.d.ts.map +0 -1
  175. package/dist/split-layout.js.map +0 -1
  176. package/dist/tab-list-D_1cYhB_.d.ts.map +0 -1
  177. package/dist/tickled-controller-BtevrRLV.d.ts.map +0 -1
  178. package/dist/tickled-controller-DkAU-0qL.js.map +0 -1
  179. package/dist/types-DlVNJez_.d.ts.map +0 -1
  180. package/dist/vertical-list.js.map +0 -1
  181. package/dist/xy-axis-B6yMjckW.js.map +0 -1
@@ -0,0 +1,398 @@
1
+ # `ixfx-detail-list`
2
+
3
+ A list that scales horizontally, not vertically. Items are laid out in equal-width columns; they flow top→bottom down a column, then wrap to the top of the next column to the right. The component has a fixed vertical extent and scrolls horizontally.
4
+
5
+ Selection semantics match `ixfx-vertical-list` — same modes, same `list-*` events, same keyboard gestures — but navigation is a 2-D grid (Up/Down move within a column, Left/Right jump columns).
6
+
7
+ ---
8
+
9
+ ## Contents
10
+
11
+ 1. [Quick start](#quick-start)
12
+ 2. [Item types](#item-types)
13
+ 3. [Properties](#properties)
14
+ 4. [Selection](#selection)
15
+ 5. [Interaction modes](#interaction-modes)
16
+ 6. [Custom checkbox](#custom-checkbox)
17
+ 7. [Events](#events)
18
+ 8. [Keyboard navigation](#keyboard-navigation)
19
+ 9. [Incremental search](#incremental-search)
20
+ 10. [Filter predicate](#filter-predicate)
21
+ 11. [Sorting](#sorting)
22
+ 12. [Asynchronous data](#asynchronous-data)
23
+ 13. [CSS variables](#css-variables)
24
+ 14. [CSS parts](#css-parts)
25
+
26
+ ---
27
+
28
+ ## Quick start
29
+
30
+ ### Declarative HTML items
31
+
32
+ ```html
33
+ <ixfx-detail-list id="my-list" selection-mode="single">
34
+ <li>Alpha</li>
35
+ <li>Bravo</li>
36
+ <li>Charlie</li>
37
+ </ixfx-detail-list>
38
+
39
+ <script type="module">
40
+ import '@ixfx/components';
41
+
42
+ const list = document.querySelector('#my-list');
43
+ list.addEventListener('list-select', ({ detail }) => {
44
+ const [item] = detail.selected;
45
+ console.log('selected:', item?.textContent);
46
+ });
47
+ </script>
48
+ ```
49
+
50
+ Items flow down columns (vertically) then wrap to the next column:
51
+
52
+ ```
53
+ Alpha Golf November Uniform
54
+ Bravo Hotel Oscar Victor
55
+ Charlie India Papa Whiskey
56
+ Delta Juliett Quebec X-ray
57
+ Echo Kilo Romeo Yankee
58
+ Foxtrot Lima Sierra Zulu
59
+ ```
60
+
61
+ ### Programmatic items
62
+
63
+ ```typescript
64
+ import type { DetailListElement } from '@ixfx/components';
65
+
66
+ const list = document.querySelector<DetailListElement>('ixfx-detail-list')!;
67
+
68
+ list.addItem('Document A');
69
+ list.addItem('Image 01.png');
70
+
71
+ // Or pass an element directly
72
+ const li = document.createElement('li');
73
+ li.textContent = 'Custom item';
74
+ list.addItem(li);
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Item types
80
+
81
+ ### Plain `<li>` elements
82
+
83
+ The simplest item type. Items are positioned by CSS flexbox into columns.
84
+
85
+ ```html
86
+ <ixfx-detail-list>
87
+ <li>Item one</li>
88
+ <li>Item two</li>
89
+ </ixfx-detail-list>
90
+ ```
91
+
92
+ **Attributes supported on `<li>`:**
93
+
94
+ | Attribute | Description |
95
+ |---|---|
96
+ | `data-search-label` | Override searchable text (defaults to `textContent`) |
97
+ | `data-key` | Stable identity for selection persistence across sort/rebuild |
98
+ | `data-value` | Alternative to `data-key` |
99
+ | `data-icon` | Icon name to display (injects `<ixfx-icon>`) |
100
+ | `not-checked` | In `checked` mode, renders without a checkbox and cannot be checked |
101
+ | `hidden` | Hides the item from view and navigation |
102
+
103
+ ### Object items
104
+
105
+ Pass an array of objects and configure how to display them:
106
+
107
+ ```typescript
108
+ const files = [
109
+ { name: 'Report Q1.pdf', size: 220, kind: 'doc' },
110
+ { name: 'Photo 01.png', size: 1400, kind: 'img' },
111
+ { name: 'Notes.txt', size: 3, kind: 'doc' },
112
+ ];
113
+
114
+ list.items = files;
115
+ list.displayProperty = 'name';
116
+ list.keyProperty = 'name'; // Preserves selection across rebuilds
117
+ ```
118
+
119
+ **Object-related properties:**
120
+
121
+ | Property | Type | Description |
122
+ |---|---|---|
123
+ | `items` | `readonly unknown[]` | Array of objects to render |
124
+ | `displayProperty` | `string` | Property name to use as label |
125
+ | `keyProperty` | `string` | Property name for stable identity |
126
+ | `iconProperty` | `string` | Property name for icon name |
127
+ | `formatter` | `(item: unknown, index: number) => string \| Node` | Custom renderer |
128
+
129
+ ```typescript
130
+ list.formatter = (item, index) => {
131
+ const file = item as { name: string; size: number };
132
+ return `${file.name} (${file.size} KB)`;
133
+ };
134
+ ```
135
+
136
+ Retrieve the original data for an element:
137
+
138
+ ```typescript
139
+ const data = list.getItemData(liElement);
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Properties
145
+
146
+ | Property | Attribute | Type | Default | Description |
147
+ |---|---|---|---|---|
148
+ | `selectionMode` | `selection-mode` | `'single' \| 'multiple' \| 'none'` | `'single'` | How selection works |
149
+ | `interactionMode` | `interaction-mode` | `'implicit' \| 'standard' \| 'manual' \| 'checked' \| 'sticky' \| 'vscode'` | `'standard'` | Click/keyboard behavior |
150
+ | `sort` | `sort` | `'none' \| 'ascending' \| 'descending'` | `'none'` | Sort direction |
151
+ | `sortBy` | `sort-by` | `string` | — | Object property to sort by |
152
+ | `sortComparator` | — | `(a, b) => number` | — | Custom sort function |
153
+ | `filterPredicate` | — | `(el: Element) => boolean` | — | Filter visible items |
154
+ | `itemWrap` | `item-wrap` | `'stop' \| 'wrap'` | `'stop'` | Keyboard wrap behavior |
155
+ | `provider` | — | `IDataProvider` | — | Async data source |
156
+ | `checkboxRenderer` | — | `function \| object` | — | Custom checkbox |
157
+
158
+ ---
159
+
160
+ ## Selection
161
+
162
+ ### Getters
163
+
164
+ ```typescript
165
+ const selected = list.selectedItem; // First selected element or undefined
166
+ const allSelected = list.selectedItems; // Array of all selected elements
167
+ ```
168
+
169
+ ### Methods
170
+
171
+ ```typescript
172
+ list.select(item); // Select an item
173
+ list.deselect(item); // Deselect an item
174
+ list.selectMany([item1, item2]); // Select multiple
175
+ list.clearSelection(); // Clear all selections
176
+ list.selectAll(); // Select all visible items (if permitted)
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Interaction modes
182
+
183
+ | Mode | Description |
184
+ |---|---|
185
+ | `standard` | Click replaces selection. Ctrl+click toggles. Shift+click creates range. Cmd+A selects all. |
186
+ | `implicit` | Click always replaces selection (no modifier support). |
187
+ | `checked` | Checkbox column toggles selection; rows marked `not-checked` have no checkbox. |
188
+ | `sticky` | Every click toggles. Cmd+A selects all. |
189
+ | `vscode` | Like `standard`, but Shift+Arrow extends selection range. |
190
+ | `manual` | No auto-selection; drive state via `select()` / `deselect()` programmatically. |
191
+
192
+ ---
193
+
194
+ ## Custom checkbox
195
+
196
+ In `checked` mode, provide a custom checkbox renderer:
197
+
198
+ ```typescript
199
+ list.checkboxRenderer = (checked) => checked
200
+ ? html`<span>✓</span>`
201
+ : html``;
202
+ ```
203
+
204
+ Or use the shorthand object form:
205
+
206
+ ```typescript
207
+ list.checkboxRenderer = { checked: '✓', unchecked: '' };
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Events
213
+
214
+ | Event | Detail | Description |
215
+ |---|---|---|
216
+ | `list-select` | `{ selected: Element[] }` | Selection changed |
217
+ | `list-tickle` | `{ item: Element }` | Hover/focus cursor moved |
218
+ | `list-activate` | `{ item: Element }` | Item activated (Enter/double-click) |
219
+ | `list-item-click` | `{ item: Element }` | Item clicked |
220
+
221
+ ```typescript
222
+ list.addEventListener('list-select', (e) => {
223
+ console.log('Selected:', [...e.detail.selected].map(el => el.textContent));
224
+ });
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Keyboard navigation
230
+
231
+ The list must have focus for keyboard navigation. Click an item to focus, or set `tabindex="0"` on the element.
232
+
233
+ | Key | Action |
234
+ |---|---|
235
+ | ↑ / ↓ | Move up/down within the current column |
236
+ | ← / → | Jump to previous/next column (same row) |
237
+ | Home | Jump to first item |
238
+ | End | Jump to last item |
239
+ | Enter | Activate item (fires `list-activate`) |
240
+ | Space | Toggle selection (in modes that support it) |
241
+ | Escape | Clear selection / close search |
242
+ | Ctrl+F | Open incremental search |
243
+ | Ctrl+A | Select all (in modes that support it) |
244
+
245
+ ### Wrapping
246
+
247
+ Set `item-wrap="wrap"` to enable wraparound navigation:
248
+
249
+ ```html
250
+ <ixfx-detail-list item-wrap="wrap">...</ixfx-detail-list>
251
+ ```
252
+
253
+ With wrapping:
254
+ - Down from last row → first row of next column (or first column if at end)
255
+ - Up from first row → last row of previous column (or last column if at start)
256
+ - Right from last column → first column
257
+ - Left from first column → last column
258
+
259
+ ---
260
+
261
+ ## Incremental search
262
+
263
+ Press Ctrl+F (or Cmd+F on Mac) to open the search overlay. Type to filter items in real-time using fuzzy matching.
264
+
265
+ Search matches against `data-search-label` (or `textContent` as fallback). Use `data-search-label` to match against a different string than what's displayed:
266
+
267
+ ```html
268
+ <li data-search-label="document a alpha">Document A</li>
269
+ ```
270
+
271
+ ### Highlight styling
272
+
273
+ Matched characters are highlighted with the `detail-list-search` highlight key. Style matches with:
274
+
275
+ ```css
276
+ ::highlight(detail-list-search) {
277
+ background: yellow;
278
+ color: black;
279
+ }
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Filter predicate
285
+
286
+ Use `filterPredicate` to programmatically hide items:
287
+
288
+ ```typescript
289
+ // Show only .doc files
290
+ list.filterPredicate = (el) => el.getAttribute('data-kind') === 'doc';
291
+
292
+ // Clear filter
293
+ list.filterPredicate = undefined;
294
+ ```
295
+
296
+ Filtered items are hidden from view, keyboard navigation, and selection.
297
+
298
+ ---
299
+
300
+ ## Sorting
301
+
302
+ ### Simple sort
303
+
304
+ ```typescript
305
+ list.sort = 'ascending'; // or 'descending'
306
+ ```
307
+
308
+ ### Sort by property
309
+
310
+ ```typescript
311
+ list.sortBy = 'size'; // Sort by 'size' property
312
+ list.sort = 'ascending';
313
+ ```
314
+
315
+ ### Custom comparator
316
+
317
+ ```typescript
318
+ list.sortComparator = (a, b) => {
319
+ // Sort by kind first, then by label
320
+ const kindCompare = a.data.kind.localeCompare(b.data.kind);
321
+ if (kindCompare !== 0) return kindCompare;
322
+ return a.label.localeCompare(b.label);
323
+ };
324
+ ```
325
+
326
+ The comparator receives `{ element, data, label }` where:
327
+ - `element` — the `<li>` DOM element
328
+ - `data` — the original object from `items` array
329
+ - `label` — the display string
330
+
331
+ ---
332
+
333
+ ## Asynchronous data
334
+
335
+ Use a `provider` for async data loading:
336
+
337
+ ### Promise provider
338
+
339
+ ```typescript
340
+ list.provider = async () => {
341
+ const response = await fetch('/api/items');
342
+ return response.json();
343
+ };
344
+ ```
345
+
346
+ ### Streaming provider
347
+
348
+ ```typescript
349
+ list.provider = async function*() {
350
+ for (let i = 0; i < 100; i++) {
351
+ yield [{ id: i, name: `Item ${i}` }];
352
+ await delay(100);
353
+ }
354
+ }();
355
+ ```
356
+
357
+ The provider function can return:
358
+ - A promise that resolves to an array
359
+ - An async iterable that yields arrays (streaming)
360
+
361
+ Streaming providers extend columns to the right as batches arrive.
362
+
363
+ ---
364
+
365
+ ## CSS variables
366
+
367
+ | Variable | Default | Description |
368
+ |---|---|---|
369
+ | `--detail-list-bg` | `surface-3` | Background color |
370
+ | `--detail-list-text` | `surface-3-text` | Text color |
371
+ | `--detail-list-border` | `border` | Border color |
372
+ | `--detail-list-radius` | `radius-s` | Border radius |
373
+ | `--detail-list-column-width` | `220px` | Width of each column |
374
+ | `--detail-list-column-gap` | `8px` | Gap between columns |
375
+ | `--detail-list-item-height` | `28px` | Height of each item |
376
+ | `--detail-list-item-padding` | `0 var(--space-m)` | Item padding |
377
+ | `--detail-list-scrollbar-width` | `auto` | Scrollbar width |
378
+ | `--detail-list-scrollbar-gutter` | `auto` | Scrollbar gutter |
379
+ | `--detail-list-scrollbar-color` | `auto` | Scrollbar color |
380
+
381
+ Plus all `--item-bg-*`, `--item-text-*` variables from the theme for selection/tickle states.
382
+
383
+ ---
384
+
385
+ ## CSS parts
386
+
387
+ | Part | Description |
388
+ |---|---|
389
+ | `list` | The `<ul>` container |
390
+ | `search-overlay` | The Ctrl+F search overlay |
391
+ | `checkbox` | The checkbox element (in `checked` mode) |
392
+ | `loading` | The loading indicator |
393
+
394
+ ```css
395
+ ixfx-detail-list::part(search-overlay) {
396
+ background: rgba(0, 0, 0, 0.8);
397
+ }
398
+ ```