@ixfx/components 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.d.ts +805 -137
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2168 -42
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.js +1 -1
- package/dist/ac-token.js +1 -1
- package/dist/crumbs.js +3 -3
- package/dist/{data-grid-Bh4YrQGK.d.ts → data-grid-BddUjAti.d.ts} +33 -1
- package/dist/data-grid-BddUjAti.d.ts.map +1 -0
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +180 -7
- package/dist/data-grid.js.map +1 -1
- package/dist/detail-list-Br0WAnry.d.ts +167 -0
- package/dist/detail-list-Br0WAnry.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -0
- package/dist/detail-list.js +1115 -0
- package/dist/detail-list.js.map +1 -0
- package/dist/grid-list-B_orPsot.d.ts +304 -0
- package/dist/grid-list-B_orPsot.d.ts.map +1 -0
- package/dist/grid-list.d.ts +3 -0
- package/dist/grid-list.js +933 -0
- package/dist/grid-list.js.map +1 -0
- package/dist/{grouped-item-lister-DnWB-eFT.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +2 -2
- package/dist/{grouped-item-lister-DnWB-eFT.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/index.d.ts +267 -124
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +469 -979
- package/dist/index.js.map +1 -1
- package/dist/interaction-Cr13azbM.js +3 -0
- package/dist/list-item-animation-DuiwgN3l.js +112 -0
- package/dist/list-item-animation-DuiwgN3l.js.map +1 -0
- package/dist/list-selection-controller-CUo-Hhcm.js +338 -0
- package/dist/list-selection-controller-CUo-Hhcm.js.map +1 -0
- package/dist/{list-selection-types-DSuRNWpx.d.ts → list-selection-types-B5ioL-dG.d.ts} +6 -1
- package/dist/list-selection-types-B5ioL-dG.d.ts.map +1 -0
- package/dist/{menu-CXDFJxNm.js → menu-Bg90Qr0B.js} +7 -7
- package/dist/menu-Bg90Qr0B.js.map +1 -0
- package/dist/{menu-item-D2S4i8u1.js → menu-item-Bx-Fs7jq.js} +3 -3
- package/dist/{menu-item-D2S4i8u1.js.map → menu-item-Bx-Fs7jq.js.map} +1 -1
- package/dist/menu.js +2 -2
- package/dist/miller.d.ts +1 -1
- package/dist/miller.js +2 -2
- package/dist/narrowed-text.js +2 -2
- package/dist/plots.js +1 -1
- package/dist/select-horiz.js +2 -2
- package/dist/{tickled-controller-CRzdIEjJ.d.ts → tickled-controller-Bn3YdQsM.d.ts} +13 -2
- package/dist/{tickled-controller-CRzdIEjJ.d.ts.map → tickled-controller-Bn3YdQsM.d.ts.map} +1 -1
- package/dist/{tickled-controller-h9GmJ_bU.js → tickled-controller-C66DgJHl.js} +16 -1
- package/dist/tickled-controller-C66DgJHl.js.map +1 -0
- package/dist/{tree-Ch4W8qXQ.js → tree-B74zdI60.js} +116 -17
- package/dist/tree-B74zdI60.js.map +1 -0
- package/dist/tree.d.ts +6 -3
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/vertical-list-CQSEVeOT.d.ts +140 -0
- package/dist/vertical-list-CQSEVeOT.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +3 -121
- package/dist/vertical-list.js +710 -1
- package/dist/vertical-list.js.map +1 -0
- package/dist/{xy-axis-DWizs0bt.js → xy-axis-VrX8dmvS.js} +2 -2
- package/dist/{xy-axis-DWizs0bt.js.map → xy-axis-VrX8dmvS.js.map} +1 -1
- package/docs-user/README.md +2 -0
- package/docs-user/data-grid.md +51 -2
- package/docs-user/detail-list.md +80 -15
- package/docs-user/grid-list.md +274 -0
- package/docs-user/index.json +11 -2
- package/docs-user/infra-tickled.md +163 -0
- package/docs-user/llms.txt +2 -0
- package/docs-user/tree.md +13 -0
- package/docs-user/vertical-list.md +51 -1
- package/llms.txt +2 -0
- package/package.json +17 -2
- package/dist/data-grid-Bh4YrQGK.d.ts.map +0 -1
- package/dist/interaction-D5XdKzbR.js +0 -2
- package/dist/list-selection-types-DSuRNWpx.d.ts.map +0 -1
- package/dist/menu-CXDFJxNm.js.map +0 -1
- package/dist/tickled-controller-h9GmJ_bU.js.map +0 -1
- package/dist/tree-Ch4W8qXQ.js.map +0 -1
- package/dist/vertical-list-ClnqTkF3.js +0 -814
- package/dist/vertical-list-ClnqTkF3.js.map +0 -1
- package/dist/vertical-list.d.ts.map +0 -1
package/docs-user/data-grid.md
CHANGED
|
@@ -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
|
|
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
|
---
|
|
@@ -218,6 +258,7 @@ Grid is focusable (`tabindex=0`). When not editing:
|
|
|
218
258
|
| Variable | Default | Description |
|
|
219
259
|
|---|---|---|
|
|
220
260
|
| `--data-grid-row-height` | `28px` | Row height |
|
|
261
|
+
| `--data-grid-row-move-duration` | `200ms` | Insertion / removal animation duration |
|
|
221
262
|
| `--data-grid-header-height` | `32px` | Header height |
|
|
222
263
|
| `--data-grid-bg` | `var(--surface-3)` | Grid background |
|
|
223
264
|
| `--data-grid-header-bg` | `var(--surface-2)` | Header background |
|
|
@@ -239,10 +280,18 @@ No `::part` exports yet (header/row/cell are shadow-internal).
|
|
|
239
280
|
| `startRowEdit` | `(rowKey)` | Open row editors |
|
|
240
281
|
| `stopEdit` | `()` | Cancel editors |
|
|
241
282
|
| `getVisibleRange` | `()=>{startRow,endRow,startCol,endCol}` | Virtual window |
|
|
283
|
+
| `appendRows` | `(rows)` | Add rows without a full re-sort/rebuild — for streaming |
|
|
284
|
+
| `selectKeys` / `set selectedKeys` | `(keys)` | Replace the selection by row key |
|
|
285
|
+
| `tickleRow` | `(key)` | Move the row cursor and scroll it into view |
|
|
286
|
+
|
|
287
|
+
| Getter | Type | |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `selectedKeys` | `ReadonlySet<string>` | Selected row keys |
|
|
290
|
+
| `tickledKey` | `string \| undefined` | Row key under the cursor |
|
|
242
291
|
|
|
243
292
|
---
|
|
244
293
|
|
|
245
294
|
## Demo
|
|
246
295
|
|
|
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).
|
|
296
|
+
`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
297
|
|
package/docs-user/detail-list.md
CHANGED
|
@@ -8,20 +8,35 @@ Selection semantics match `ixfx-vertical-list` — same modes, same `list-*` eve
|
|
|
8
8
|
|
|
9
9
|
## Contents
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
- [`ixfx-detail-list`](#ixfx-detail-list)
|
|
12
|
+
- [Contents](#contents)
|
|
13
|
+
- [Quick start](#quick-start)
|
|
14
|
+
- [Declarative HTML items](#declarative-html-items)
|
|
15
|
+
- [Programmatic items](#programmatic-items)
|
|
16
|
+
- [Item types](#item-types)
|
|
17
|
+
- [Plain `<li>` elements](#plain-li-elements)
|
|
18
|
+
- [Object items](#object-items)
|
|
19
|
+
- [Properties](#properties)
|
|
20
|
+
- [Selection](#selection)
|
|
21
|
+
- [Getters](#getters)
|
|
22
|
+
- [Methods](#methods)
|
|
23
|
+
- [Interaction modes](#interaction-modes)
|
|
24
|
+
- [Custom checkbox](#custom-checkbox)
|
|
25
|
+
- [Events](#events)
|
|
26
|
+
- [Keyboard navigation](#keyboard-navigation)
|
|
27
|
+
- [Wrapping](#wrapping)
|
|
28
|
+
- [Incremental search](#incremental-search)
|
|
29
|
+
- [Highlight styling](#highlight-styling)
|
|
30
|
+
- [Filter predicate](#filter-predicate)
|
|
31
|
+
- [Sorting](#sorting)
|
|
32
|
+
- [Simple sort](#simple-sort)
|
|
33
|
+
- [Sort by property](#sort-by-property)
|
|
34
|
+
- [Custom comparator](#custom-comparator)
|
|
35
|
+
- [Asynchronous data](#asynchronous-data)
|
|
36
|
+
- [Promise provider](#promise-provider)
|
|
37
|
+
- [Streaming provider](#streaming-provider)
|
|
38
|
+
- [CSS variables](#css-variables)
|
|
39
|
+
- [CSS parts](#css-parts)
|
|
25
40
|
|
|
26
41
|
---
|
|
27
42
|
|
|
@@ -72,8 +87,17 @@ list.addItem('Image 01.png');
|
|
|
72
87
|
const li = document.createElement('li');
|
|
73
88
|
li.textContent = 'Custom item';
|
|
74
89
|
list.addItem(li);
|
|
90
|
+
|
|
91
|
+
// removeItem / removeItems return a Promise that resolves once the row is gone
|
|
92
|
+
list.removeItems(list.selectedItems);
|
|
75
93
|
```
|
|
76
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
|
+
|
|
77
101
|
---
|
|
78
102
|
|
|
79
103
|
## Item types
|
|
@@ -176,6 +200,35 @@ list.clearSelection(); // Clear all selections
|
|
|
176
200
|
list.selectAll(); // Select all visible items (if permitted)
|
|
177
201
|
```
|
|
178
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
|
+
|
|
179
232
|
---
|
|
180
233
|
|
|
181
234
|
## Interaction modes
|
|
@@ -373,13 +426,25 @@ Streaming providers extend columns to the right as batches arrive.
|
|
|
373
426
|
| `--detail-list-column-width` | `220px` | Width of each column |
|
|
374
427
|
| `--detail-list-column-gap` | `8px` | Gap between columns |
|
|
375
428
|
| `--detail-list-item-height` | `28px` | Height of each item |
|
|
376
|
-
| `--detail-list-item-padding` | `0 var(--space-
|
|
429
|
+
| `--detail-list-item-padding` | `0 var(--space-xs)` | Item padding |
|
|
377
430
|
| `--detail-list-scrollbar-width` | `auto` | Scrollbar width |
|
|
378
431
|
| `--detail-list-scrollbar-gutter` | `auto` | Scrollbar gutter |
|
|
379
432
|
| `--detail-list-scrollbar-color` | `auto` | Scrollbar color |
|
|
380
433
|
|
|
381
434
|
Plus all `--item-bg-*`, `--item-text-*` variables from the theme for selection/tickle states.
|
|
382
435
|
|
|
436
|
+
To explicitly size columns to the widest visible item, call `fitColumnWidth()`. It
|
|
437
|
+
returns the applied width in pixels and does not run automatically:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
const width = await list.fitColumnWidth();
|
|
441
|
+
console.log(`Column width: ${width}px`);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The resulting pixel value is set on the element itself and overrides an inherited
|
|
445
|
+
`--detail-list-column-width` value. Lists otherwise continue to use the configured
|
|
446
|
+
CSS variable or the default `220px` width.
|
|
447
|
+
|
|
383
448
|
---
|
|
384
449
|
|
|
385
450
|
## CSS parts
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
## Virtualisation
|
|
219
|
+
|
|
220
|
+
Only the cells in (or near) the viewport are in the DOM.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
grid.count; // total items
|
|
224
|
+
grid.renderedCount; // cells currently in the shadow DOM
|
|
225
|
+
grid.visibleKeys(); // data-key of items in the current window
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`grid-list` handles tens of thousands of children in every orientation.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Scroll fade & insertion animation
|
|
233
|
+
|
|
234
|
+
The scrollable edge is fade-masked whenever there's more content that way — a
|
|
235
|
+
zero-JS `scroll-timeline` mask (`scroll-fade-y` for `grid` / `column`,
|
|
236
|
+
`scroll-fade-x` for `row`). See `docs/infra-theming.md` → *Scroll Fade*; tune the
|
|
237
|
+
reach with `--scroll-fade-width` (default `30px`).
|
|
238
|
+
|
|
239
|
+
When the child list changes **mid-list** (not a first populate or a wholesale
|
|
240
|
+
`replaceChildren`), the cells slide to their new position rather than snapping —
|
|
241
|
+
so an inserted item's neighbours make room and a removed item's gap closes — and
|
|
242
|
+
a freshly inserted cell also fades in. The transition is confined to a short
|
|
243
|
+
window after the mutation, so it never fires during scroll virtualisation, and
|
|
244
|
+
it's suppressed under `prefers-reduced-motion: reduce`. Duration:
|
|
245
|
+
`--grid-list-item-move-duration` (default `200ms`).
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## CSS parts
|
|
250
|
+
|
|
251
|
+
| Part | Element |
|
|
252
|
+
|---|---|
|
|
253
|
+
| `viewport` | The scroll container |
|
|
254
|
+
| `item` | A cell |
|
|
255
|
+
| `thumb` | The square holding the slotted content |
|
|
256
|
+
| `caption` | The caption element (absent for `caption-style="hidden"`) |
|
|
257
|
+
|
|
258
|
+
Host CSS custom properties: `--grid-list-item-bg`, `--grid-list-radius`,
|
|
259
|
+
`--grid-list-gap`, `--grid-list-item-move-duration` (insertion animation,
|
|
260
|
+
default `200ms`), `--scroll-fade-width` (edge fade reach, default `30px`), plus
|
|
261
|
+
the shared `--item-bg-*` / `--accent` theme tokens.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Choosing a list component
|
|
266
|
+
|
|
267
|
+
- **Large lists** — prefer `ixfx-grid-list` with `orientation="column"` over
|
|
268
|
+
`ixfx-vertical-list`. `vertical-list` renders every child; `grid-list` is
|
|
269
|
+
windowed.
|
|
270
|
+
- **Small, declarative, richly-slotted lists** — `ixfx-vertical-list` stays the
|
|
271
|
+
simpler choice.
|
|
272
|
+
- **Horizontal virtualised strip** — `ixfx-grid-list` with `orientation="row"`.
|
|
273
|
+
- **Tabular data with columns / sort / grouping / inline edit** —
|
|
274
|
+
`ixfx-data-grid`.
|
package/docs-user/index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixfx/components",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"generated": "2026-09-
|
|
3
|
+
"version": "0.7.4",
|
|
4
|
+
"generated": "2026-09-08T16:59:30.569Z",
|
|
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",
|
|
@@ -235,6 +240,10 @@
|
|
|
235
240
|
}
|
|
236
241
|
],
|
|
237
242
|
"overviews": [
|
|
243
|
+
{
|
|
244
|
+
"name": "infra-tickled.md",
|
|
245
|
+
"path": "docs-user/infra-tickled.md"
|
|
246
|
+
},
|
|
238
247
|
{
|
|
239
248
|
"name": "user-catalog.md",
|
|
240
249
|
"path": "docs-user/user-catalog.md"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Tickled items
|
|
2
|
+
|
|
3
|
+
An item is **tickled** when it is under active attention — pointer hovering over it, or keyboard cursor on it. Only one item is tickled at a time — a single logical cursor regardless of input mechanism.
|
|
4
|
+
|
|
5
|
+
**Tickled state is controlled via the `.tickled` CSS class** (applied by `TickledController`). The `tickledItemStyles` provides base styling that all tickled components should use.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## TickledController
|
|
10
|
+
|
|
11
|
+
`TickledController` is a Lit `ReactiveController` from `src/interaction/index.js` that manages this state for you.
|
|
12
|
+
|
|
13
|
+
### Direct-render setup (items in your template)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { TickledController, tickledStyles, tickledItemStyles } from '../interaction/index.js';
|
|
17
|
+
|
|
18
|
+
class MyList extends LitElement {
|
|
19
|
+
static override styles = [
|
|
20
|
+
tickledStyles,
|
|
21
|
+
tickledItemStyles,
|
|
22
|
+
css`
|
|
23
|
+
.item.selected { background: var(--item-bg-selected); }
|
|
24
|
+
`
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
private tickled = new TickledController(this, {
|
|
28
|
+
getItems: () => this.items.map((_, i) => ({ id: i })),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
protected override render() {
|
|
32
|
+
return html`
|
|
33
|
+
<div
|
|
34
|
+
data-input-mode=${this.tickled.inputMode}
|
|
35
|
+
@pointerleave=${() => this.tickled.clearTickled()}
|
|
36
|
+
@keydown=${this.#handleKeyDown}
|
|
37
|
+
>
|
|
38
|
+
${this.items.map((item, i) => html`
|
|
39
|
+
<div
|
|
40
|
+
class="item ${this.tickled.isTickled(i) ? 'tickled' : ''}"
|
|
41
|
+
@pointerenter=${() => this.tickled.handlePointerEnter(i)}
|
|
42
|
+
>${item}</div>
|
|
43
|
+
`)}
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#handleKeyDown(e: KeyboardEvent) {
|
|
49
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); this.tickled.handleKeyboardNavigation('next'); }
|
|
50
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); this.tickled.handleKeyboardNavigation('previous'); }
|
|
51
|
+
else if (e.key === 'Home') { e.preventDefault(); this.tickled.handleKeyboardNavigation('first'); }
|
|
52
|
+
else if (e.key === 'End') { e.preventDefault(); this.tickled.handleKeyboardNavigation('last'); }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Slotted-content setup
|
|
58
|
+
|
|
59
|
+
Lit's reactive rendering doesn't touch slotted elements, so you must sync DOM classes manually via `onStateChange`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
private tickled = new TickledController<number>(this, {
|
|
63
|
+
getItems: () => this._slottedItems().map((_, i) => ({ id: i })),
|
|
64
|
+
onStateChange: () => this._syncTickled(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Must run in willUpdate so :host gets data-input-mode before paint
|
|
68
|
+
override willUpdate(changed: Map<PropertyKey, unknown>) {
|
|
69
|
+
super.willUpdate(changed);
|
|
70
|
+
this.setAttribute('data-input-mode', this.tickled.inputMode);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _slottedItems(): HTMLElement[] {
|
|
74
|
+
const slot = this.shadowRoot?.querySelector('slot');
|
|
75
|
+
return Array.from(slot?.assignedElements() ?? []) as HTMLElement[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private _syncTickled() {
|
|
79
|
+
this._slottedItems().forEach((item, i) => {
|
|
80
|
+
const isTickled = this.tickled.isTickled(i);
|
|
81
|
+
if (isTickled && this.tickled.inputMode === 'keyboard') {
|
|
82
|
+
item.classList.add('tickled');
|
|
83
|
+
} else {
|
|
84
|
+
item.classList.remove('tickled');
|
|
85
|
+
}
|
|
86
|
+
item.tabIndex = isTickled ? 0 : -1;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For slotted content, set `data-input-mode` on `:host` (not on an inner div) so `tickledStyles` CSS variables cascade through the shadow boundary to slotted elements.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## CSS variable contract
|
|
96
|
+
|
|
97
|
+
All `--item-*` state variables are defined in `src/style.css` (the theme). `tickledItemStyles` applies them to `.item` elements — it no longer sets defaults of its own for the colour/state variables.
|
|
98
|
+
|
|
99
|
+
See [infra-theming.md](./infra-theming.md#item-states) for the full variable listing and their theme values.
|
|
100
|
+
|
|
101
|
+
The layout variables (`--item-padding`, `--item-border-radius`, `--item-icon-spacing`, `--item-bg`) are also defined in `style.css` with defaults derived from the spacing/radius tokens.
|
|
102
|
+
|
|
103
|
+
## Base item styles
|
|
104
|
+
|
|
105
|
+
`tickledItemStyles` provides these base styles:
|
|
106
|
+
|
|
107
|
+
```css
|
|
108
|
+
.item {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: var(--item-icon-spacing);
|
|
112
|
+
padding: var(--item-padding);
|
|
113
|
+
background: var(--item-bg);
|
|
114
|
+
border: var(--item-border);
|
|
115
|
+
border-radius: var(--item-border-radius);
|
|
116
|
+
transition: background var(--transition), border-color var(--transition);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.item.tickled {
|
|
120
|
+
background: var(--item-bg-tickled);
|
|
121
|
+
border-color: var(--item-border-tickled);
|
|
122
|
+
color: var(--item-text-tickled);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.item.selected {
|
|
126
|
+
background: var(--item-bg-selected);
|
|
127
|
+
border-color: var(--item-border-selected);
|
|
128
|
+
color: var(--item-text-selected);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.item.selected.tickled {
|
|
132
|
+
background: var(--item-bg-selected-tickled);
|
|
133
|
+
outline: 2px dashed var(--surface-3);
|
|
134
|
+
outline-offset: -2px;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Controller API
|
|
141
|
+
|
|
142
|
+
**Constructor:** `new TickledController<T>(host, config)`
|
|
143
|
+
|
|
144
|
+
Config:
|
|
145
|
+
- `getItems: () => readonly { id: T; enabled?: boolean }[]` — called on each render
|
|
146
|
+
- `onStateChange?: () => void` — fired after every state mutation (required for slotted content)
|
|
147
|
+
- `onTickle?: (id: T | undefined) => void` — called only when the tickled id *changes* (useful for opening submenus)
|
|
148
|
+
- `initialInputMode?: 'keyboard' | 'mouse'`
|
|
149
|
+
|
|
150
|
+
Properties: `inputMode`, `tickledId`
|
|
151
|
+
|
|
152
|
+
Methods: `isTickled(id)`, `handlePointerEnter(id)`, `handleKeyboardNavigation('next'|'previous'|'first'|'last'|T)`, `clearTickled()`, `setTickled(id)`, `setInputMode(mode)`
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Checklist
|
|
157
|
+
|
|
158
|
+
- [ ] `tickledStyles` and `tickledItemStyles` included in `static styles`
|
|
159
|
+
- [ ] Components use `tickledItemStyles` base styles, override only for component-specific needs
|
|
160
|
+
- [ ] `data-input-mode` set on the correct element (container div for direct-render; `:host` via `willUpdate` for slotted)
|
|
161
|
+
- [ ] `@pointerenter` on each item, `@pointerleave` on container
|
|
162
|
+
- [ ] Keyboard navigation calls `handleKeyboardNavigation`
|
|
163
|
+
- [ ] Slotted components: `onStateChange` + `_syncTickled()` implemented; `.tickled` class only added in keyboard mode
|