@ixfx/components 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/bundle/index.d.ts +1355 -526
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +3277 -384
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/ac-text.js +1 -1
  7. package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  8. package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  9. package/dist/ac-token.d.ts +1 -1
  10. package/dist/ac-token.js +2 -2
  11. package/dist/button.d.ts +1 -1
  12. package/dist/button.d.ts.map +1 -1
  13. package/dist/colour-picker.d.ts +1 -1
  14. package/dist/crumbs.d.ts +6 -2
  15. package/dist/crumbs.d.ts.map +1 -1
  16. package/dist/crumbs.js +13 -3
  17. package/dist/crumbs.js.map +1 -1
  18. package/dist/{data-grid-Db4QpnII.d.ts → data-grid-CtJ35v8L.d.ts} +59 -2
  19. package/dist/data-grid-CtJ35v8L.d.ts.map +1 -0
  20. package/dist/data-grid.d.ts +1 -1
  21. package/dist/data-grid.js +268 -14
  22. package/dist/data-grid.js.map +1 -1
  23. package/dist/detail-list-BS5dRDda.d.ts +175 -0
  24. package/dist/detail-list-BS5dRDda.d.ts.map +1 -0
  25. package/dist/detail-list.d.ts +3 -0
  26. package/dist/detail-list.js +1031 -0
  27. package/dist/detail-list.js.map +1 -0
  28. package/dist/grid-list-D-GbghSH.d.ts +321 -0
  29. package/dist/grid-list-D-GbghSH.d.ts.map +1 -0
  30. package/dist/grid-list.d.ts +3 -0
  31. package/dist/grid-list.js +994 -0
  32. package/dist/grid-list.js.map +1 -0
  33. package/dist/{grouped-item-lister-BWlC9S4e.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +3 -3
  34. package/dist/{grouped-item-lister-BWlC9S4e.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
  35. package/dist/grouped-item-lister.d.ts +1 -1
  36. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
  37. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  38. package/dist/icons.d.ts +2 -2
  39. package/dist/incr-search-DpbSJtgj.js +962 -0
  40. package/dist/incr-search-DpbSJtgj.js.map +1 -0
  41. package/dist/incr-search.d.ts +2 -2
  42. package/dist/incr-search.js +2 -3
  43. package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
  44. package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  45. package/dist/index-CBS4z1v-.d.ts +365 -0
  46. package/dist/index-CBS4z1v-.d.ts.map +1 -0
  47. package/dist/{index-DsjpDksO.d.ts → index-CS9yM8L-.d.ts} +2 -2
  48. package/dist/{index-DsjpDksO.d.ts.map → index-CS9yM8L-.d.ts.map} +1 -1
  49. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
  50. package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
  51. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
  52. package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
  53. package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
  54. package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
  55. package/dist/index.d.ts +188 -243
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +376 -1004
  58. package/dist/index.js.map +1 -1
  59. package/dist/interaction-Cr13azbM.js +3 -0
  60. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
  61. package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
  62. package/dist/labelled-radial-input.d.ts +2 -2
  63. package/dist/labelled-radial-input.d.ts.map +1 -1
  64. package/dist/labelled-range-input.d.ts +1 -1
  65. package/dist/labelled-slider-input.d.ts +1 -1
  66. package/dist/list-item-animation-DuiwgN3l.js +112 -0
  67. package/dist/list-item-animation-DuiwgN3l.js.map +1 -0
  68. package/dist/list-selection-controller-DTFI40li.js +264 -0
  69. package/dist/list-selection-controller-DTFI40li.js.map +1 -0
  70. package/dist/{list-selection-types-DSuRNWpx.d.ts → list-selection-types-B5ioL-dG.d.ts} +6 -1
  71. package/dist/list-selection-types-B5ioL-dG.d.ts.map +1 -0
  72. package/dist/{menu-DMzjkVnK.js → menu-Bg90Qr0B.js} +3 -3
  73. package/dist/{menu-DMzjkVnK.js.map → menu-Bg90Qr0B.js.map} +1 -1
  74. package/dist/{menu-item-D2S4i8u1.js → menu-item-Bx-Fs7jq.js} +3 -3
  75. package/dist/{menu-item-D2S4i8u1.js.map → menu-item-Bx-Fs7jq.js.map} +1 -1
  76. package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
  77. package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
  78. package/dist/menu.d.ts +2 -2
  79. package/dist/menu.js +2 -2
  80. package/dist/miller.d.ts +82 -4
  81. package/dist/miller.d.ts.map +1 -1
  82. package/dist/miller.js +329 -70
  83. package/dist/miller.js.map +1 -1
  84. package/dist/narrowed-text.js +3 -4
  85. package/dist/narrowed-text.js.map +1 -1
  86. package/dist/panel.d.ts +1 -1
  87. package/dist/plots.js +1 -1
  88. package/dist/select-horiz.js +2 -2
  89. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
  90. package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
  91. package/dist/split-layout.d.ts +1 -1
  92. package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
  93. package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
  94. package/dist/tabs.d.ts +1 -1
  95. package/dist/{tickled-controller-Ds1DMyG7.d.ts → tickled-controller-Bn3YdQsM.d.ts} +12 -1
  96. package/dist/{tickled-controller-Ds1DMyG7.d.ts.map → tickled-controller-Bn3YdQsM.d.ts.map} +1 -1
  97. package/dist/{tickled-controller-h9GmJ_bU.js → tickled-controller-C66DgJHl.js} +16 -1
  98. package/dist/tickled-controller-C66DgJHl.js.map +1 -0
  99. package/dist/{titlebar-B23MrIbI.d.ts → titlebar-Bj5pfdw_.d.ts} +2 -2
  100. package/dist/{titlebar-B23MrIbI.d.ts.map → titlebar-Bj5pfdw_.d.ts.map} +1 -1
  101. package/dist/titlebar.d.ts +1 -1
  102. package/dist/{tree-Ch4W8qXQ.js → tree-DAii2A1g.js} +190 -29
  103. package/dist/tree-DAii2A1g.js.map +1 -0
  104. package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
  105. package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
  106. package/dist/tree.d.ts +28 -4
  107. package/dist/tree.d.ts.map +1 -1
  108. package/dist/tree.js +1 -1
  109. package/dist/vertical-list-DwtCPy1b.d.ts +143 -0
  110. package/dist/vertical-list-DwtCPy1b.d.ts.map +1 -0
  111. package/dist/vertical-list.d.ts +3 -121
  112. package/dist/vertical-list.js +642 -1
  113. package/dist/vertical-list.js.map +1 -0
  114. package/dist/{xy-axis-DWizs0bt.js → xy-axis-VrX8dmvS.js} +2 -2
  115. package/dist/{xy-axis-DWizs0bt.js.map → xy-axis-VrX8dmvS.js.map} +1 -1
  116. package/docs-user/README.md +1 -0
  117. package/docs-user/data-grid.md +70 -2
  118. package/docs-user/detail-list.md +39 -1
  119. package/docs-user/grid-list.md +298 -0
  120. package/docs-user/incr-search.md +12 -2
  121. package/docs-user/index.json +7 -2
  122. package/docs-user/llms.txt +1 -0
  123. package/docs-user/tree.md +35 -1
  124. package/docs-user/vertical-list.md +58 -2
  125. package/llms.txt +1 -0
  126. package/package.json +17 -2
  127. package/dist/data-grid-Db4QpnII.d.ts.map +0 -1
  128. package/dist/element-search-Y8SRfPGM.js +0 -185
  129. package/dist/element-search-Y8SRfPGM.js.map +0 -1
  130. package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
  131. package/dist/incr-search-vCzuao-2.js +0 -137
  132. package/dist/incr-search-vCzuao-2.js.map +0 -1
  133. package/dist/index-Cu5i8zZY.d.ts +0 -160
  134. package/dist/index-Cu5i8zZY.d.ts.map +0 -1
  135. package/dist/interaction-D5XdKzbR.js +0 -2
  136. package/dist/list-selection-types-DSuRNWpx.d.ts.map +0 -1
  137. package/dist/tickled-controller-h9GmJ_bU.js.map +0 -1
  138. package/dist/tree-Ch4W8qXQ.js.map +0 -1
  139. package/dist/vertical-list-B4w_ksJk.js +0 -814
  140. package/dist/vertical-list-B4w_ksJk.js.map +0 -1
  141. package/dist/vertical-list.d.ts.map +0 -1
@@ -65,6 +65,30 @@ grid.getRowKey = (row, i) => row.key ?? row.id ?? String(i);
65
65
 
66
66
  Reassign `rows` to update. Sorting is derived from `sortState`.
67
67
 
68
+ When the row set changes without a wholesale replace — rows spliced in, rows
69
+ filtered out — the new rows fade in and, for ~0.3s afterwards, every row slides
70
+ to its new position instead of snapping (so an inserted row's neighbours make
71
+ room and a removed row's gap closes). Skipped under `prefers-reduced-motion:
72
+ reduce`; timing is `--data-grid-row-move-duration` (default `200ms`).
73
+
74
+ ### Streaming appends — `appendRows()`
75
+
76
+ Reassigning `rows` re-runs `_applySort([...rows])` + `_rebuildFlat()` every
77
+ time — O(n log n) per batch, quadratic over a full load. When feeding the grid
78
+ from a stream (paged fetch, incremental scan), use `appendRows()` instead:
79
+
80
+ ```ts
81
+ for await (const batch of stream) {
82
+ grid.appendRows(batch);
83
+ }
84
+ ```
85
+
86
+ It still sets `rows` to a new array reference (reactivity preserved) but
87
+ **bypasses the `rows` setter's full rebuild** — the derived arrays are extended
88
+ over the appended slice only. When a sort is active the new rows show unsorted
89
+ for one frame, then a single coalesced re-sort runs. Scroll position, selection
90
+ and cursor are preserved across the append.
91
+
68
92
  ---
69
93
 
70
94
  ## Selection & interaction
@@ -76,7 +100,19 @@ Reassign `rows` to update. Sorting is derived from `sortState`.
76
100
 
77
101
  See `ixfx-vertical-list` README for mode table. Data-grid adds checkbox column in `checked` mode.
78
102
 
79
- Programmatic: selection is `Set<string>` of row keys (`_selectedKeys` private, read via events).
103
+ ### Programmatic selection & cursor
104
+
105
+ ```ts
106
+ grid.selectedKeys; // ReadonlySet<string> of selected row keys
107
+ grid.selectedKeys = ['r3', 'r7']; // replace (clamped to known rows, single-mode keeps the last)
108
+ grid.selectKeys(['r3', 'r7']); // alias of the setter
109
+
110
+ grid.tickleRow('r7'); // move the row cursor + scroll it into view
111
+ grid.tickledKey; // row key under the cursor, or undefined
112
+ ```
113
+
114
+ The setter fires `datagrid-select` `{ selectedKeys, previous }` only when the
115
+ selection actually changes.
80
116
 
81
117
  ---
82
118
 
@@ -194,6 +230,10 @@ Demo: `Score` is random float `0–100` with 0–3 decimals, `align:'right'`, `f
194
230
 
195
231
  Gear menu (`ixfx-menu-trigger` placement `bottom-end`, `inset-area: block-end span-inline-end`) lists `Columns` (check-items) and `Group by` (radio). Toggle visibility via `col.visible=false` or menu. Event: `datagrid-column-visibility` `{key, visible}`.
196
232
 
233
+ `column-menu` attribute: `auto` (default) renders the gear menu; `none`
234
+ suppresses it entirely. `col.visible` + `datagrid-column-visibility` still give
235
+ full external control, so `none` lets a host own the column-visibility UI.
236
+
197
237
  Other events: `datagrid-column-reorder` `{from,to,columns}`, `datagrid-column-resize` `{key,width}`, `datagrid-cell-click` `{rowKey,colKey,rowIndex,colIndex}`, `datagrid-cell-edit` `{rowKey,colKey,oldValue,newValue}`.
198
238
 
199
239
  ---
@@ -213,11 +253,31 @@ Grid is focusable (`tabindex=0`). When not editing:
213
253
 
214
254
  ---
215
255
 
256
+ ## Incremental search
257
+
258
+ A built-in type-ahead popover with **navigate** (cursor + scroll to best match),
259
+ **select** (matching rows become the selection), and **filter** (non-matching
260
+ rows dropped from the flat list) modes. No default shortcut:
261
+
262
+ ```typescript
263
+ grid.registry.invoke('list.search.filter'); // or .navigate / .select
264
+ ```
265
+
266
+ A row is matched against `rowSearchText(row)` — by default its visible cells'
267
+ values joined by a space; override with the `rowSearchText` property. `select`
268
+ mode needs `selection-mode="multiple"` to hold more than one row. Character
269
+ highlights are not drawn (a match position spans multiple cells). `filterPredicate`
270
+ (a `(row) => boolean` property) filters rows directly. `search-modes` limits the
271
+ offered modes. Full model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
272
+
273
+ ---
274
+
216
275
  ## CSS variables & parts
217
276
 
218
277
  | Variable | Default | Description |
219
278
  |---|---|---|
220
279
  | `--data-grid-row-height` | `28px` | Row height |
280
+ | `--data-grid-row-move-duration` | `200ms` | Insertion / removal animation duration |
221
281
  | `--data-grid-header-height` | `32px` | Header height |
222
282
  | `--data-grid-bg` | `var(--surface-3)` | Grid background |
223
283
  | `--data-grid-header-bg` | `var(--surface-2)` | Header background |
@@ -239,10 +299,18 @@ No `::part` exports yet (header/row/cell are shadow-internal).
239
299
  | `startRowEdit` | `(rowKey)` | Open row editors |
240
300
  | `stopEdit` | `()` | Cancel editors |
241
301
  | `getVisibleRange` | `()=>{startRow,endRow,startCol,endCol}` | Virtual window |
302
+ | `appendRows` | `(rows)` | Add rows without a full re-sort/rebuild — for streaming |
303
+ | `selectKeys` / `set selectedKeys` | `(keys)` | Replace the selection by row key |
304
+ | `tickleRow` | `(key)` | Move the row cursor and scroll it into view |
305
+
306
+ | Getter | Type | |
307
+ |---|---|---|
308
+ | `selectedKeys` | `ReadonlySet<string>` | Selected row keys |
309
+ | `tickledKey` | `string \| undefined` | Row key under the cursor |
242
310
 
243
311
  ---
244
312
 
245
313
  ## Demo
246
314
 
247
- `demo/data-grid.html` + `demo/data-grid.ts`: 200 rows, `Department` random, `Score` float, `Name`/`Department` editable (`user` mode, slow double-click; `Tab`/`Up`/`Down` navigation; `Enter` from tickled row). Controls: Selection/Interaction/Group by/Editability + `Edit first row` (manual).
315
+ `demo/data-grid.html` + `demo/data-grid.ts`: 200 rows, `Department` random, `Score` float, `Name`/`Department` editable (`user` mode, slow double-click; `Tab`/`Up`/`Down` navigation; `Enter` from tickled row). Controls: Selection/Interaction/Group by/Editability/`column-menu` + `Edit first row` (manual), a `Stream +5,000 rows` button (`appendRows`), and `selectKeys` / `tickleRow` buttons.
248
316
 
@@ -87,8 +87,17 @@ list.addItem('Image 01.png');
87
87
  const li = document.createElement('li');
88
88
  li.textContent = 'Custom item';
89
89
  list.addItem(li);
90
+
91
+ // removeItem / removeItems return a Promise that resolves once the row is gone
92
+ list.removeItems(list.selectedItems);
90
93
  ```
91
94
 
95
+ Rows added mid-list collapse + fade in; `removeItem` / `removeItems` collapse +
96
+ fade them out before detaching, so the rows around them slide to make or close
97
+ the gap. A bulk populate isn't animated, and it's all skipped under
98
+ `prefers-reduced-motion: reduce` (`--ixfx-list-item-move-duration`, default
99
+ `200ms`).
100
+
92
101
  ---
93
102
 
94
103
  ## Item types
@@ -191,6 +200,35 @@ list.clearSelection(); // Clear all selections
191
200
  list.selectAll(); // Select all visible items (if permitted)
192
201
  ```
193
202
 
203
+ ### Key-based selection
204
+
205
+ `Element` references die when the item set is rebuilt (re-fetch, re-sort,
206
+ filter). Drive selection by a stable **`data-key`** (or `data-value`) instead —
207
+ the Element API is unchanged, the key API is additive. Object items built from
208
+ `items` / a provider get their key stamped automatically from `keyProperty`.
209
+
210
+ ```typescript
211
+ list.selectKeys(['row-3', 'row-7']); // replace selection; unknown keys ignored
212
+ list.addSelectedKeys(['row-9']); // multi-select modes only
213
+ list.removeSelectedKeys(['row-3']);
214
+ list.selectedKeys; // ReadonlySet<string>
215
+
216
+ list.focusKey('row-7'); // move cursor to an item + scroll into view
217
+ list.tickledKey; // key under the cursor, or undefined
218
+
219
+ list.addEventListener('list-select', ({ detail }) => {
220
+ console.log([...detail.selectedKeys]); // carried alongside detail.selected
221
+ });
222
+ ```
223
+
224
+ Restore a selection across a data reload:
225
+
226
+ ```typescript
227
+ const keep = [...list.selectedKeys];
228
+ list.items = await fetchRows(); // Element refs are stale...
229
+ list.selectKeys(keep); // ...keys still resolve
230
+ ```
231
+
194
232
  ---
195
233
 
196
234
  ## Interaction modes
@@ -275,7 +313,7 @@ With wrapping:
275
313
 
276
314
  ## Incremental search
277
315
 
278
- Press Ctrl+F (or Cmd+F on Mac) to open the search overlay. Type to filter items in real-time using fuzzy matching.
316
+ A built-in type-ahead popover with **navigate** / **select** / **filter** modes — see [`docs/infra-incremental.md`](../../docs/infra-incremental.md). No default shortcut; open it with `list.registry.invoke('list.search.filter')` (or `.navigate` / `.select`). A filter change re-runs the column geometry automatically.
279
317
 
280
318
  Search matches against `data-search-label` (or `textContent` as fallback). Use `data-search-label` to match against a different string than what's displayed:
281
319
 
@@ -0,0 +1,298 @@
1
+ # `ixfx-grid-list`
2
+
3
+ A virtualised grid of items — thumbnails, cards, or arbitrary slotted markup —
4
+ with selection, keyboard navigation, and captions. Windowed rendering keeps the
5
+ DOM small regardless of item count; the same component does a wrapping grid, a
6
+ single vertical column, or a horizontally-scrolling strip.
7
+
8
+ ---
9
+
10
+ ## Contents
11
+
12
+ 1. [Quick start](#quick-start)
13
+ 2. [Items & keys](#items--keys)
14
+ 3. [Sizing & layout](#sizing--layout)
15
+ 4. [Captions](#captions)
16
+ 5. [Orientation](#orientation)
17
+ 6. [Selection](#selection)
18
+ 7. [Cursor](#cursor)
19
+ 8. [Events](#events)
20
+ 9. [Keyboard](#keyboard)
21
+ 10. [Virtualisation](#virtualisation)
22
+ 11. [CSS parts](#css-parts)
23
+ 12. [Choosing a list component](#choosing-a-list-component)
24
+
25
+ ---
26
+
27
+ ## Quick start
28
+
29
+ ```html
30
+ <ixfx-grid-list id="grid" selection-mode="single" item-size="120" gap="8">
31
+ <div class="tile" data-key="a" data-label="Apple">🍎</div>
32
+ <div class="tile" data-key="b" data-label="Banana">🍌</div>
33
+ <div class="tile" data-key="c" data-label="Cherry">🍒</div>
34
+ </ixfx-grid-list>
35
+
36
+ <script type="module">
37
+ import '@ixfx/components/grid-list';
38
+
39
+ const grid = document.querySelector('#grid');
40
+ grid.addEventListener('list-select', ({ detail }) => {
41
+ console.log([...detail.selectedKeys]);
42
+ });
43
+ </script>
44
+ ```
45
+
46
+ Items are **light-DOM children**. Each is projected into its own virtualised
47
+ cell via a named `<slot>`. Add or remove children at any time — a
48
+ `MutationObserver` picks the change up and re-renders.
49
+
50
+ ```typescript
51
+ import type { GridListElement } from '@ixfx/components/grid-list';
52
+
53
+ const grid = document.querySelector<GridListElement>('#grid')!;
54
+ for (const item of data) {
55
+ const el = document.createElement('div');
56
+ el.className = 'tile';
57
+ el.dataset.key = item.id;
58
+ el.dataset.label = item.name;
59
+ el.textContent = item.glyph;
60
+ grid.appendChild(el);
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Items & keys
67
+
68
+ | Item attribute | Purpose |
69
+ |---|---|
70
+ | `data-key` (or `data-value`) | Stable identity — for key-based selection, `focusKey`, and `visibleKeys()`. Recommended on every item. |
71
+ | `data-label` | Caption text (see [Captions](#captions)). Alternatively put the text in a `[data-grid-list-caption]` descendant. |
72
+ | `not-checked` | Item can't be tickled / checked (skipped by the cursor). |
73
+ | `hidden` | Item is excluded from layout and navigation. |
74
+
75
+ ---
76
+
77
+ ## Sizing & layout
78
+
79
+ | Property / attribute | Default | Description |
80
+ |---|---|---|
81
+ | `itemSize` / `item-size` | `160` | Cell width in px. Also the square's height for `separate` / `under` / `overlaid` / `hidden`. |
82
+ | `gap` | `8` | Gap between cells, px. |
83
+ | `captionHeight` / `caption-height` | `24` | Height reserved for the caption band in `separate` / `under`. |
84
+
85
+ Values are clamped and rounded. Changing `item-size` / `gap` re-anchors the
86
+ scroll on the item nearest the viewport centre (zoom stays put rather than
87
+ jumping to the top-left corner).
88
+
89
+ ---
90
+
91
+ ## Captions
92
+
93
+ `caption-style` — how the caption relates to the content:
94
+
95
+ | value | layout |
96
+ |---|---|
97
+ | `hidden` | No caption. Cell height = `item-size`. |
98
+ | `separate` | Content in a square; caption a **detached** label below it. Square and caption highlight independently (macOS Finder icon view). |
99
+ | `under` (default) | One box: content on top, caption strip **inside** it below. One selection outline. |
100
+ | `overlaid` | Caption floats over the content. |
101
+
102
+ `caption-align` — `left` \| `middle` (default) \| `right`. Text alignment; also
103
+ positions the pill in `separate`.
104
+
105
+ `overlaid` only:
106
+
107
+ - `caption-position` — `top` \| `middle` \| `bottom` (default).
108
+ - `caption-backdrop` — `scrim` (default, flat translucent panel) \| `gradient`
109
+ (fades away from the caption edge) \| `blur` (frosted) \| `none` (text +
110
+ shadow, no panel).
111
+
112
+ ```html
113
+ <ixfx-grid-list caption-style="separate" caption-align="left"></ixfx-grid-list>
114
+ <ixfx-grid-list caption-style="overlaid" caption-position="top" caption-backdrop="gradient"></ixfx-grid-list>
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Orientation
120
+
121
+ `orientation`:
122
+
123
+ | value | layout | scrolls |
124
+ |---|---|---|
125
+ | `grid` (default) | wraps across as many columns as fit | vertically |
126
+ | `row` | a single row of items | **horizontally** |
127
+ | `column` | a single column of items | vertically |
128
+
129
+ `row` is the virtualised horizontal strip; `column` is the recommended
130
+ large-list replacement for `ixfx-vertical-list`.
131
+
132
+ ---
133
+
134
+ ## Selection
135
+
136
+ `selection-mode` (`none` \| `single` \| `multiple`) and `interaction-mode`
137
+ (`standard` \| `implicit` \| `manual` \| `checked` \| `sticky` \| `vscode`) match
138
+ the other list components — see the `ixfx-vertical-list` README for the mode
139
+ table.
140
+
141
+ ### Element API
142
+
143
+ ```typescript
144
+ grid.select(el); // replace with one item
145
+ grid.selectMany([a, b]); // selection-mode="multiple" only
146
+ grid.deselect(el);
147
+ grid.clearSelection();
148
+ grid.selectAll();
149
+ grid.selectedItems; // ReadonlySet<Element>
150
+ ```
151
+
152
+ ### Key-based selection
153
+
154
+ `Element` references die when the item set is rebuilt. With a `data-key` on each
155
+ item, drive selection by key instead — additive, the Element API is unchanged.
156
+
157
+ ```typescript
158
+ grid.selectKeys(['a', 'c']); // replace; unknown keys ignored
159
+ grid.addSelectedKeys(['b']); // multi-select modes only
160
+ grid.removeSelectedKeys(['a']);
161
+ grid.selectedKeys; // ReadonlySet<string>
162
+
163
+ grid.addEventListener('list-select', ({ detail }) => {
164
+ console.log([...detail.selectedKeys]); // alongside detail.selected
165
+ });
166
+ ```
167
+
168
+ Restore across a reload:
169
+
170
+ ```typescript
171
+ const keep = [...grid.selectedKeys];
172
+ grid.replaceChildren(...(await fetchTiles()));
173
+ grid.selectKeys(keep);
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Cursor
179
+
180
+ The "tickled" cursor follows hover (mouse) or arrow keys (keyboard).
181
+
182
+ ```typescript
183
+ grid.focusKey('b'); // move the cursor to an item and scroll it into view
184
+ grid.tickledKey; // key under the cursor, or undefined
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Events
190
+
191
+ All bubble and are `composed`.
192
+
193
+ | Event | Detail | When |
194
+ |---|---|---|
195
+ | `list-select` | `{ selected, previous, selectedKeys }` | Selection changes by any means |
196
+ | `list-activate` | `{ item }` | Double-click or `Enter` on an item |
197
+ | `list-item-click` | `{ item }` | Single click / `Enter` |
198
+ | `list-tickle` | `{ item }` | Cursor moves onto an item |
199
+ | `grid-list-viewport-change` | `{ visibleKeys }` | Scroll, resize, or content change — the rendered window moved |
200
+
201
+ ---
202
+
203
+ ## Keyboard
204
+
205
+ | Key | `grid` | `row` / `column` |
206
+ |---|---|---|
207
+ | `ArrowLeft` / `ArrowRight` | ∓1 item | ∓1 item |
208
+ | `ArrowUp` / `ArrowDown` | ∓/± one row | ∓1 item |
209
+ | `Home` / `End` | first / last | first / last |
210
+ | `PageUp` / `PageDown` | ± one screen of rows | ± one screen of items |
211
+ | `Enter` | activate | activate |
212
+ | `Space` | toggle selection | toggle selection |
213
+ | `Escape` | clear selection | clear selection |
214
+ | `Ctrl` / `Cmd` + `A` | select all (multi modes) | select all (multi modes) |
215
+
216
+ ---
217
+
218
+ ## Incremental search
219
+
220
+ A built-in type-ahead popover with **navigate** / **select** / **filter** modes.
221
+ No default shortcut — open it via a command on `grid.registry`:
222
+
223
+ ```typescript
224
+ grid.registry.invoke('list.search.filter'); // or .navigate / .select
225
+ ```
226
+
227
+ Search matches `data-label` (or `[data-grid-list-caption]` text, then
228
+ `data-search-label`, then `textContent`). Filter mode genuinely removes items
229
+ from the virtualised layout. Add a page-level highlight rule to see matched
230
+ characters:
231
+
232
+ ```css
233
+ ::highlight(grid-list-search) { background: var(--accent); color: var(--accent-text); }
234
+ ```
235
+
236
+ `filterPredicate` (an `(el: Element) => boolean` property) filters items directly,
237
+ independent of search. Limit modes with `search-modes="navigate filter"`. Full
238
+ model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
239
+
240
+ ---
241
+
242
+ ## Virtualisation
243
+
244
+ Only the cells in (or near) the viewport are in the DOM.
245
+
246
+ ```typescript
247
+ grid.count; // total items
248
+ grid.renderedCount; // cells currently in the shadow DOM
249
+ grid.visibleKeys(); // data-key of items in the current window
250
+ ```
251
+
252
+ `grid-list` handles tens of thousands of children in every orientation.
253
+
254
+ ---
255
+
256
+ ## Scroll fade & insertion animation
257
+
258
+ The scrollable edge is fade-masked whenever there's more content that way — a
259
+ zero-JS `scroll-timeline` mask (`scroll-fade-y` for `grid` / `column`,
260
+ `scroll-fade-x` for `row`). See `docs/infra-theming.md` → *Scroll Fade*; tune the
261
+ reach with `--scroll-fade-width` (default `30px`).
262
+
263
+ When the child list changes **mid-list** (not a first populate or a wholesale
264
+ `replaceChildren`), the cells slide to their new position rather than snapping —
265
+ so an inserted item's neighbours make room and a removed item's gap closes — and
266
+ a freshly inserted cell also fades in. The transition is confined to a short
267
+ window after the mutation, so it never fires during scroll virtualisation, and
268
+ it's suppressed under `prefers-reduced-motion: reduce`. Duration:
269
+ `--grid-list-item-move-duration` (default `200ms`).
270
+
271
+ ---
272
+
273
+ ## CSS parts
274
+
275
+ | Part | Element |
276
+ |---|---|
277
+ | `viewport` | The scroll container |
278
+ | `item` | A cell |
279
+ | `thumb` | The square holding the slotted content |
280
+ | `caption` | The caption element (absent for `caption-style="hidden"`) |
281
+
282
+ Host CSS custom properties: `--grid-list-item-bg`, `--grid-list-radius`,
283
+ `--grid-list-gap`, `--grid-list-item-move-duration` (insertion animation,
284
+ default `200ms`), `--scroll-fade-width` (edge fade reach, default `30px`), plus
285
+ the shared `--item-bg-*` / `--accent` theme tokens.
286
+
287
+ ---
288
+
289
+ ## Choosing a list component
290
+
291
+ - **Large lists** — prefer `ixfx-grid-list` with `orientation="column"` over
292
+ `ixfx-vertical-list`. `vertical-list` renders every child; `grid-list` is
293
+ windowed.
294
+ - **Small, declarative, richly-slotted lists** — `ixfx-vertical-list` stays the
295
+ simpler choice.
296
+ - **Horizontal virtualised strip** — `ixfx-grid-list` with `orientation="row"`.
297
+ - **Tabular data with columns / sort / grouping / inline edit** —
298
+ `ixfx-data-grid`.
@@ -1,8 +1,18 @@
1
1
  # Incremental Search
2
2
 
3
3
  Utilities for incremental (type-ahead) search over data collections or DOM-backed lists.
4
- You can use the low-level `createIncrSearch` for data, `createElementIncrSearch` for
5
- DOM elements, or `IncrSearchTreeController` for tree and Miller list components.
4
+
5
+ - **`ListSearchController`** the shared brain behind the built-in search
6
+ popover in `ixfx-grid-list` / `ixfx-vertical-list` / `ixfx-detail-list` /
7
+ `ixfx-data-grid` / `ixfx-tree-list` / `ixfx-miller-list`. Three modes
8
+ (navigate / select / filter), driven by `list.search.*` commands. See
9
+ [`docs/infra-incremental.md`](../../docs/infra-incremental.md) — start there
10
+ for anything component-facing.
11
+ - **`createIncrSearch`** — the low-level fuzzy engine (fzf) both the controller
12
+ and the helpers below are built on.
13
+ - **`createElementIncrSearch`** / **`IncrSearchTreeController`** — *deprecated*
14
+ filter-only controllers driven by an external `<input>`. Kept for standalone
15
+ use; components now use `ListSearchController`.
6
16
 
7
17
  ## Plain UL list
8
18
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
- "version": "0.7.3",
4
- "generated": "2026-09-06T18:27:10.873Z",
3
+ "version": "0.7.5",
4
+ "generated": "2026-09-08T18:17:07.322Z",
5
5
  "components": [
6
6
  {
7
7
  "name": "ac-text",
@@ -63,6 +63,11 @@
63
63
  "summary": "Primitives for typical form layout with responsive label positioning and data binding.",
64
64
  "path": "docs-user/form.md"
65
65
  },
66
+ {
67
+ "name": "grid-list",
68
+ "summary": "A virtualised grid of items — thumbnails, cards, or arbitrary slotted markup —",
69
+ "path": "docs-user/grid-list.md"
70
+ },
66
71
  {
67
72
  "name": "grouped-item-lister",
68
73
  "summary": "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",
@@ -23,6 +23,7 @@ import '@ixfx/components/data-grid';
23
23
  - [dock](docs-user/dock.md): Dockable, movable panels like VS Code / Photoshop. Drag a panel by its
24
24
  - [editable-label](docs-user/editable-label.md): Inline editable text components. `ixfx-editable-number` extends `ixfx-editable-label` with drag-to-adjust and fill-bar visualization.
25
25
  - [form](docs-user/form.md): Primitives for typical form layout with responsive label positioning and data binding.
26
+ - [grid-list](docs-user/grid-list.md): A virtualised grid of items — thumbnails, cards, or arbitrary slotted markup —
26
27
  - [grouped-item-lister](docs-user/grouped-item-lister.md): 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
27
28
  - [icons](docs-user/icons.md): 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.
28
29
  - [incr-search](docs-user/incr-search.md): Utilities for incremental (type-ahead) search over data collections or DOM-backed lists.
package/docs-user/tree.md CHANGED
@@ -367,7 +367,28 @@ Keyboard navigation follows the crumb path. Arrow keys move between breadcrumb s
367
367
 
368
368
  ## Incremental search
369
369
 
370
- Wire up a text input to filter and highlight matching nodes using `IncrSearchTreeController`:
370
+ `ixfx-tree-list` and `ixfx-miller-list` have a **built-in type-ahead popover**
371
+ with three modes:
372
+
373
+ - **navigate** — expands the match's ancestors and moves the cursor to it; ↑/↓
374
+ cycle between matches; Enter activates and closes.
375
+ - **select** — the selection becomes the set of matching nodes.
376
+ - **filter** — non-matching nodes (and branches with no matching descendant) are
377
+ hidden; branches with a match auto-expand so the match is visible.
378
+
379
+ No default shortcut — open it via a command on `el.registry`:
380
+
381
+ ```typescript
382
+ el.registry.invoke('list.search.navigate'); // or .select / .filter
383
+ ```
384
+
385
+ Limit the offered modes with the `search-modes` attribute
386
+ (`search-modes="navigate filter"`). Full model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
387
+
388
+ ### Deprecated: `IncrSearchTreeController`
389
+
390
+ The external-`<input>` filter-only controller still works but is superseded by
391
+ the built-in popover.
371
392
 
372
393
  ```typescript
373
394
  import { IncrSearchTreeController } from '@ixfx/components';
@@ -463,6 +484,18 @@ Each column is a separate `role="listbox"` with `aria-label` showing the parent
463
484
 
464
485
  ---
465
486
 
487
+ ## Row animation
488
+
489
+ Rows that appear — a branch expanded, a node added to `root` / the model —
490
+ collapse + fade in, and the rows below slide down to make room. Rows that
491
+ disappear — a branch collapsed, a node removed — are kept as fading "ghost" rows
492
+ until they've collapsed out, so the gap closes smoothly. Above ~50 rows changing
493
+ at once the animation is skipped, as it is entirely under
494
+ `prefers-reduced-motion: reduce`. Duration: `--ixfx-list-item-move-duration`
495
+ (default `200ms`).
496
+
497
+ ---
498
+
466
499
  ## CSS variables
467
500
 
468
501
  ### Shared / themed
@@ -493,6 +526,7 @@ These are set by the global theme and inherited by all tree components.
493
526
  | `--tree-indent` | `20px` | Indentation per depth level |
494
527
  | `--tree-item-height` | `28px` | Height of each row |
495
528
  | `--tree-caret-size` | `10px` | Expand/collapse caret size |
529
+ | `--ixfx-list-item-move-duration` | `200ms` | Row enter / leave animation duration |
496
530
 
497
531
  ### `ixfx-miller-list` specific
498
532
 
@@ -313,13 +313,21 @@ list.addItem(myElement);
313
313
  // In checked mode, add a row that shows no checkbox and can't be checked
314
314
  list.addItem('Header', { notChecked: true });
315
315
 
316
- // Remove a specific element
316
+ // Remove a specific element (returns a Promise that resolves once it's gone)
317
317
  list.removeItem(element);
318
+ list.removeItems(list.selectedItems);
318
319
 
319
320
  // Remove all items and clear selection
320
321
  list.clearItems();
321
322
  ```
322
323
 
324
+ Items added mid-list — via `addItem`, a direct `appendChild`, or a framework
325
+ binding — collapse + fade in, and the rows below slide down to make room.
326
+ `removeItem` / `removeItems` play the reverse (collapse + fade out) and then
327
+ detach, so the gap closes smoothly. A bulk populate isn't animated, and
328
+ everything is skipped under `prefers-reduced-motion: reduce`. Tune the timing
329
+ with the `--ixfx-list-item-move-duration` custom property (default `200ms`).
330
+
323
331
  ### Reading the selection
324
332
 
325
333
  ```typescript
@@ -341,6 +349,48 @@ list.addEventListener('list-select', ({ detail }) => {
341
349
  });
342
350
  ```
343
351
 
352
+ ### Key-based selection
353
+
354
+ `Element` references die the moment you rebuild the item set (re-fetch, external
355
+ re-sort, filter). Give each item a **`data-key`** (or `data-value`) and drive
356
+ selection by that stable string instead. The Element API keeps working
357
+ unchanged — the key API is additive.
358
+
359
+ ```html
360
+ <ixfx-vertical-list selection-mode="multiple">
361
+ <li data-key="a">Apple</li>
362
+ <li data-key="b">Banana</li>
363
+ <li data-key="c">Cherry</li>
364
+ </ixfx-vertical-list>
365
+ ```
366
+
367
+ ```typescript
368
+ list.selectKeys(['a', 'c']); // replace selection; unknown keys ignored
369
+ list.addSelectedKeys(['b']); // multi-select modes only
370
+ list.removeSelectedKeys(['a']);
371
+ list.selectedKeys; // ReadonlySet<string>, e.g. Set { 'b', 'c' }
372
+
373
+ list.focusKey('b'); // move the cursor to an item + scroll it into view
374
+ list.tickledKey; // key under the cursor, or undefined
375
+
376
+ // list-select carries keys too
377
+ list.addEventListener('list-select', ({ detail }) => {
378
+ console.log([...detail.selectedKeys]);
379
+ });
380
+ ```
381
+
382
+ Restore a selection across a data reload:
383
+
384
+ ```typescript
385
+ const keep = [...list.selectedKeys];
386
+ list.setItems(await fetchRows()); // Element refs are now stale
387
+ list.selectKeys(keep); // ...but the keys still resolve
388
+ ```
389
+
390
+ The `data-key` requirement is documented, not enforced: items without a key
391
+ simply never appear in `selectedKeys` and can't be targeted by `selectKeys` /
392
+ `focusKey`.
393
+
344
394
  ### Filtering by state
345
395
 
346
396
  Use `selectedFilter()` to iterate over items based on their selection or checked state. Pass `'selected'`, `'checked'`, or `'either'` to filter accordingly:
@@ -454,7 +504,13 @@ The element must be focused (click it or tab to it) before keyboard navigation w
454
504
 
455
505
  ## Incremental search
456
506
 
457
- Press **Ctrl+F** (or **Cmd+F**) to open the search overlay. Typing filters items in real time using fuzzy matching. Matched text in plain `<li>` items is highlighted via the [CSS Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API).
507
+ A built-in type-ahead popover with three modes — **navigate** (cursor jumps to the best match, ↑/↓ cycle), **select** (matches become the selection), **filter** (non-matches hidden). See [`docs/infra-incremental.md`](../../docs/infra-incremental.md) for the full model, conversions between modes, and the `search-modes` attribute.
508
+
509
+ There is **no default shortcut**. Open it by invoking one of the commands on `list.registry`:
510
+
511
+ ```typescript
512
+ list.registry.invoke('list.search.filter'); // or .navigate / .select
513
+ ```
458
514
 
459
515
  ### Adding the page-level highlight rule
460
516
 
package/llms.txt CHANGED
@@ -23,6 +23,7 @@ import '@ixfx/components/data-grid';
23
23
  - [dock](docs-user/dock.md): Dockable, movable panels like VS Code / Photoshop. Drag a panel by its
24
24
  - [editable-label](docs-user/editable-label.md): Inline editable text components. `ixfx-editable-number` extends `ixfx-editable-label` with drag-to-adjust and fill-bar visualization.
25
25
  - [form](docs-user/form.md): Primitives for typical form layout with responsive label positioning and data binding.
26
+ - [grid-list](docs-user/grid-list.md): A virtualised grid of items — thumbnails, cards, or arbitrary slotted markup —
26
27
  - [grouped-item-lister](docs-user/grouped-item-lister.md): 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
27
28
  - [icons](docs-user/icons.md): 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.
28
29
  - [incr-search](docs-user/incr-search.md): Utilities for incremental (type-ahead) search over data collections or DOM-backed lists.