@ixfx/components 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/bundle/index.d.ts +917 -44
  2. package/bundle/index.js +702 -696
  3. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  4. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  5. package/dist/crumbs.d.ts +2 -115
  6. package/dist/crumbs.js +1 -1179
  7. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
  9. package/dist/data-grid.d.ts +3 -3
  10. package/dist/data-grid.js +1 -1
  11. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
  13. package/dist/detail-list.d.ts +4 -4
  14. package/dist/detail-list.js +1 -1
  15. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-JPgQSmLW.d.ts} +27 -4
  16. package/dist/flat-list.d.ts +2 -2
  17. package/dist/flat-list.js +123 -4
  18. package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
  19. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
  20. package/dist/grid-list.d.ts +3 -3
  21. package/dist/grid-list.js +1 -1
  22. package/dist/incr-search.d.ts +1 -1
  23. package/dist/index-9ttcdwlL.d.ts +71 -0
  24. package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
  25. package/dist/index-CYRc6Dah.d.ts +475 -0
  26. package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
  27. package/dist/index.d.ts +334 -23
  28. package/dist/index.js +2247 -867
  29. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  30. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  31. package/dist/miller-BFRfn0ve.js +2237 -0
  32. package/dist/miller.d.ts +4 -437
  33. package/dist/miller.js +1 -1815
  34. package/dist/pending-BGidHppL.js +91 -0
  35. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  36. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  37. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  38. package/dist/split-layout.d.ts +1 -1
  39. package/dist/split-layout.js +1 -1
  40. package/dist/sunburst-tree.d.ts +2 -2
  41. package/dist/sunburst-tree.js +1 -1
  42. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_VxOHlo.js} +298 -79
  43. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
  44. package/dist/tree-CftHwtEb.js +146 -0
  45. package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
  46. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  47. package/dist/tree.d.ts +3 -3
  48. package/dist/tree.js +1 -1
  49. package/dist/types-LU1u0CB8.d.ts +87 -0
  50. package/dist/vertical-list-1V1Vg2O5.js +1993 -0
  51. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
  52. package/dist/vertical-list.d.ts +3 -3
  53. package/dist/vertical-list.js +1 -1789
  54. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  55. package/docs-user/README.md +2 -0
  56. package/docs-user/crumbs.md +27 -0
  57. package/docs-user/data-grid.md +66 -1
  58. package/docs-user/detail-list.md +54 -0
  59. package/docs-user/dock.md +54 -12
  60. package/docs-user/flat-list.md +27 -0
  61. package/docs-user/grid-list.md +64 -11
  62. package/docs-user/hyperbolic-tree.md +31 -4
  63. package/docs-user/icicle-tree.md +36 -0
  64. package/docs-user/index.json +12 -2
  65. package/docs-user/llms.txt +2 -0
  66. package/docs-user/miller.md +52 -21
  67. package/docs-user/multicol-list.md +50 -8
  68. package/docs-user/split-layout.md +45 -17
  69. package/docs-user/sunburst-tree.md +32 -2
  70. package/docs-user/tree.md +45 -6
  71. package/docs-user/treemap.md +32 -0
  72. package/docs-user/twosplit-layout.md +22 -0
  73. package/docs-user/user-catalog.md +1 -1
  74. package/docs-user/vertical-list.md +55 -7
  75. package/llms.txt +2 -0
  76. package/package.json +1 -1
@@ -20,10 +20,11 @@ single vertical column, or a horizontally-scrolling strip.
20
20
  9. [Native drag-out](#native-drag-out)
21
21
  10. [Keyboard](#keyboard)
22
22
  11. [Grouping](#grouping)
23
- 12. [Virtualisation](#virtualisation)
24
- 13. [Performance](#performance)
25
- 14. [CSS parts](#css-parts)
26
- 15. [Choosing a list component](#choosing-a-list-component)
23
+ 12. [View state](#view-state)
24
+ 13. [Virtualisation](#virtualisation)
25
+ 14. [Performance](#performance)
26
+ 15. [CSS parts](#css-parts)
27
+ 16. [Choosing a list component](#choosing-a-list-component)
27
28
 
28
29
  ---
29
30
 
@@ -214,7 +215,7 @@ grid.tickledKey; // key under the cursor, or undefined
214
215
  | Property / attribute | Default | Description |
215
216
  |---|---|---|
216
217
  | `disableHoverTickle` / `disable-hover-tickle` | `false` | When set, pointer hover no longer moves the cursor, fires `list-tickle`, or changes where arrow-key navigation resumes from, and the hover highlight is dropped so nothing *looks* interactive. Keyboard navigation, selection, and `focusKey()` are unaffected. |
217
- | `disableScrollHoverTickle` / `disable-scroll-hover-tickle` | `false` | When set, a scroll gesture moving cells beneath a stationary pointer no longer moves the cursor: virtualised `pointerenter` events from scrolling are ignored, so `list-tickle` and the highlight hold still. Hover resumes on the next real pointer movement, or after the pointer leaves and re-enters the list. Click selection, keyboard navigation, and `focusKey()` are unaffected. |
218
+ | `scrollHoverTickle` / `scroll-hover-tickle` | `false` | Let hover follow cells that scroll beneath a stationary pointer. Off by default: during a scroll gesture, virtualised `pointerenter` events are ignored so `list-tickle` and the highlight hold still. Hover resumes on the next real pointer movement, or after the pointer leaves and re-enters the list. Click selection, keyboard navigation, and `focusKey()` are unaffected. |
218
219
 
219
220
  ```html
220
221
  <ixfx-grid-list disable-hover-tickle></ixfx-grid-list>
@@ -229,8 +230,8 @@ Because cells are virtualised, wheel / trackpad scrolling inserts new cells
229
230
  under a stationary pointer, and each fresh cell fires `pointerenter` — the
230
231
  cursor and any cursor-driven rendering (e.g. a large preview panel) churn for
231
232
  as long as the gesture lasts, even though the pointer never moved.
232
- `disable-scroll-hover-tickle` ignores those synthetic enters while the gesture
233
- is active. While suppressed, the host carries a `data-scroll-hover-suppress`
233
+ By default those synthetic enters are ignored while the gesture is active
234
+ (set `scroll-hover-tickle` to let hover follow the content instead). While suppressed, the host carries a `data-scroll-hover-suppress`
234
235
  attribute (the CSS `:hover` highlight is gated on it), which is removed on the
235
236
  next real pointer movement or when the pointer leaves the list.
236
237
 
@@ -402,11 +403,11 @@ Parts: `part="icon"` and `part="badge"` on the wrappers this adds.
402
403
 
403
404
  | Method | Effect |
404
405
  |---|---|
405
- | `setItems(items)` | Replace the collection. Selection / cursor keep any key still present. |
406
+ | `setItems(items)` | Replace the collection. Selection / cursor keep any key still present; vanished keys are deselected silently and held pending (see [View state](#view-state)). |
406
407
  | `appendItems(items)` | Add to the end. Scroll, selection and cursor untouched; no reflow animation. |
407
408
  | `updateItem(key, item)` | Replace one item. A visible cell re-renders in place; an offscreen one updates when scrolled in. |
408
- | `removeItems(keys)` | Remove items. Removed keys drop out of selection / cursor. |
409
- | `clearItems()` | Drop everything. Resets selection / cursor and emits one empty `list-select`. |
409
+ | `removeItems(keys)` | Remove items. Removed keys drop out of selection / cursor silently and are held pending. |
410
+ | `clearItems()` | Drop everything. Resets selection / cursor and emits one empty `list-select`; the keys are held pending, so re-adding them restores the selection. |
410
411
 
411
412
  Selection, cursor, keyboard nav, search, filtering, `count`, `renderedCount`,
412
413
  `renderedKeys()` and `visibleKeys()` behave identically to element mode — all
@@ -442,7 +443,7 @@ grid.collapseAllGroups();
442
443
 
443
444
  - Groups appear in first-encounter order. Filters (`filterPredicate`, `itemFilterPredicate`, incremental filter) run first, so an emptied group loses its header and counts follow the filter.
444
445
  - **Collapsed groups** leave the effective order: not navigable, not rendered, not in select-all, range, marquee or drag-out. Their selection state is kept. A collapsed key is not auto-expanded by `focusKey()`.
445
- - Changing `groupBy` (from any path) clears the collapse state.
446
+ - Changing `groupBy` (from any path) clears the collapse state. Saved collapse state from `setViewState()` that hasn't matched a group yet is kept.
446
447
  - **Layout:** in `grid` / `column` a header spans the full width and its items restart at column 0 beneath it. In `row` orientation the header is an inline cell of `group-header-width`.
447
448
  - **Keyboard:** Left/Right step through items, crossing groups. Up/Down keep the column, hop across headers and clamp on a short last row. Headers are never focused; collapsing is by click or the API.
448
449
  - Events: `list-group-toggle` `{ groupKey, collapsed }`, `list-group-change` `{ groupBy }`.
@@ -450,6 +451,58 @@ grid.collapseAllGroups();
450
451
 
451
452
  Differences from data-grid: group state is reset on every `groupBy` change, the empty key is labelled `(none)`, headers are skipped by arrow keys, and the collapse API and events are exposed.
452
453
 
454
+ ---
455
+
456
+ ## View state
457
+
458
+ Snapshot and restore how the grid is showing its data, separately from the
459
+ data. The contract is shared with the other list components; see
460
+ [`docs/infra-view-state.md`](../../docs/infra-view-state.md).
461
+
462
+ ```ts
463
+ import { bindViewState } from '@ixfx/components';
464
+
465
+ bindViewState(grid, {
466
+ load: () => JSON.parse(localStorage.getItem(`photos-grid`) ?? `null`),
467
+ save: s => localStorage.setItem(`photos-grid`, JSON.stringify(s)),
468
+ });
469
+ grid.items = await fetchPhotos(); // pending state applies as items arrive
470
+ ```
471
+
472
+ `kind`: `grid-list`. Type: `GridListViewState`.
473
+
474
+ | Field | Shape | Meaning |
475
+ |---|---|---|
476
+ | `selected` | `string[]` | Selected item keys. |
477
+ | `expanded` | `Record<string, boolean>` | Group key → open. Groups default open, so only collapsed groups (`false`) are recorded. |
478
+ | `focus` | `string` | Keyboard-cursor key. Recorded only while the cursor was last moved by keyboard. |
479
+ | `scroll` | `{ top, left?, anchor? }` | `anchor` is the first item of the first visible line plus the px offset into that line, along the main axis (`left` in `row` orientation, `top` otherwise). |
480
+
481
+ Members: `getViewState(options?)`, `setViewState(state)`,
482
+ `clearPendingViewState()`, and the debounced `view-state-change` event.
483
+
484
+ Grid-specific behaviour:
485
+
486
+ - Works the same in element mode (light-DOM children with `data-key`) and data
487
+ mode (`items` + `itemAdapter`). Keyless children have auto-assigned
488
+ positional keys, which never appear in view state.
489
+ - A key counts as present when its item is in the effective order: an item
490
+ that is filtered out, or inside a collapsed group, keeps its saved selection
491
+ pending until it shows. Restoring `expanded` and `selected` together re-opens
492
+ groups first, so a saved selection inside a saved-open group resolves.
493
+ - A collapsed group whose items all leave (data reset, filter) keeps its
494
+ collapsed state pending and is collapsed again when it comes back.
495
+ Restoring collapse state does not fire `list-group-toggle`.
496
+ - Items that leave the data are deselected **without** a `list-select`; their
497
+ keys go pending and are re-selected (with a `list-select`) if they return.
498
+ - A restored cursor puts the list in keyboard input mode, so it is shown and
499
+ recorded like a keyboard-moved cursor.
500
+ - Scroll restore waits until the anchor item is present and the viewport is
501
+ measured, then until the content is long enough. It is cancelled by the
502
+ user's own wheel, touch, pointer or navigation key. The anchor is re-laid out
503
+ with the current geometry, so a different column count still lands on the
504
+ same item's line.
505
+
453
506
  ## Virtualisation
454
507
 
455
508
  Only the cells in (or near) the viewport are in the DOM — in both element and
@@ -17,10 +17,11 @@ It implements the shared `TreeComponent` interface (see [`src/tree/README.md`](.
17
17
  7. [Targeting a specific node or edge](#targeting-a-specific-node-or-edge)
18
18
  8. [Custom edge rendering](#custom-edge-rendering)
19
19
  9. [Focus, selection, navigation](#focus-selection-navigation)
20
- 10. [Events](#events)
21
- 11. [Keyboard / pointer interaction](#keyboard--pointer-interaction)
22
- 12. [CSS variables and parts](#css-variables-and-parts)
23
- 13. [Types reference](#types-reference)
20
+ 10. [View state](#view-state)
21
+ 11. [Events](#events)
22
+ 12. [Keyboard / pointer interaction](#keyboard--pointer-interaction)
23
+ 13. [CSS variables and parts](#css-variables-and-parts)
24
+ 14. [Types reference](#types-reference)
24
25
 
25
26
  ---
26
27
 
@@ -56,6 +57,7 @@ el.root = {
56
57
  | `root` | `TreeNode \| undefined` | `undefined` | Root node of the tree |
57
58
  | `model` | `TreeDataModel \| undefined` | `undefined` | Shared data model; takes precedence over `loadChildren` |
58
59
  | `loadChildren` | `LoadChildrenCallback \| undefined` | `undefined` | Callback for lazy child loading |
60
+ | `restoreLoads` | `boolean` | `true` | Whether restoring a saved `center` may load its unloaded ancestors. See [View state](#view-state) |
59
61
  | `interactionMode` (`interaction-mode`) | `'implicit' \| 'standard' \| 'manual'` | `'standard'` | Gesture mapping — see [`src/tree/README.md`](../tree/README.md#interactionmode-ixfx-tree-list--ixfx-miller-list) (`'checked'` is unsupported here) |
60
62
  | `selectionFilter` (`selection-filter`) | `'none' \| 'leaf' \| 'branch' \| 'all'` | `'all'` | Which node kinds can be selected |
61
63
  | `exclusivity` | `TreeExclusivity` | `'none'` | Accepted for interface compatibility, ignored |
@@ -252,6 +254,31 @@ el.navigateToNode(n => n.item.key === 'readme.md');
252
254
  const nodes = el.getNodes(); // ReadonlyArray<{ node: TreeNode; depth: number }>
253
255
  ```
254
256
 
257
+ ## View state
258
+
259
+ Implements the shared view-state API (`getViewState()` / `setViewState()` / `clearPendingViewState()` / `view-state-change`) — see [`docs/infra-view-state.md`](../../docs/infra-view-state.md). `kind` is `hyperbolictree-list`.
260
+
261
+ ```typescript
262
+ type HyperbolicTreeListViewState = ViewStateBase & {
263
+ center?: string[]; // key path from the root's children to the centred node; [] = root
264
+ zoom?: number; // visual magnification (the `zoom` property)
265
+ };
266
+ ```
267
+
268
+ | Field | Meaning |
269
+ |---|---|
270
+ | `selected` | Selected item keys. |
271
+ | `focus` | Keyboard cursor key — only recorded when the cursor was moved by keyboard or search navigation. |
272
+ | `center` | The centred node, as a key path. A path rather than a lone key because keys may repeat across branches here (e.g. `readme.md` in many folders). |
273
+ | `zoom` | Magnification. Not keyed: it doesn't depend on the data. |
274
+
275
+ - **What isn't saved:** a free pan (dragging empty space) and the disk's rotation are raw transforms in the centred node's local frame — they break as soon as the data changes, so they are left out. A restored view sits exactly on `center`, unpanned.
276
+ - **Data changes keep state.** A new `root`, a model `root` / `invalidate` event or a lazy load re-applies state by key. If the centred node goes away the view falls back to the root silently and `center` stays pending; selected nodes that go away are deselected silently and their keys stay pending.
277
+ - **Lazy data.** With `restoreLoads` (default `true`), a saved `center` whose ancestors aren't loaded is reached by loading each one in turn via `loadChildren` / `model`.
278
+ - **Events.** A restored selection fires `select`; a restored centre fires `focus` (no `expand`/`collapse`) and snaps rather than animating.
279
+ - An explicit `focusOn()` (click, key, navigation) drops a pending `center`; any explicit selection change drops pending `selected`.
280
+ - With repeated keys, a saved selection resolves to the first node (depth-first) with that key.
281
+
255
282
  ## Events
256
283
 
257
284
  All events **bubble** and are **composed**.
@@ -0,0 +1,36 @@
1
+ # Icicle Tree
2
+
3
+ `<ixfx-icicletree-list>` — an icicle (partition) view over tree data. Each depth is a band; each node a rectangle sized by its `valueAccessor` share of its parent. Clicking a branch zooms so it becomes the visual root, with its ancestors in a breadcrumb strip.
4
+
5
+ It implements the shared `TreeComponent` interface (see [`src/tree/README.md`](../tree/README.md)). The [sunburst](../sunburst-tree/) is its polar counterpart.
6
+
7
+ ---
8
+
9
+ ## View state
10
+
11
+ Implements the shared view-state API (`getViewState()` / `setViewState()` / `clearPendingViewState()` / `view-state-change`) — see [`docs/infra-view-state.md`](../../docs/infra-view-state.md). `kind` is `icicletree-list`.
12
+
13
+ | Field | Type | Meaning |
14
+ |---|---|---|
15
+ | `selected` | `string[]` | Selected `item.key`s |
16
+ | `zoom` | `string[]` | Key path from the root's children to the zoom node; `[]` = full tree |
17
+ | `focus` | `string` | Keyboard-cursor `item.key` (recorded only when the cursor was moved by keyboard) |
18
+ | `scroll` | `{ top, left? }` | The host's scroll offset (pixels; rects aren't a linear list, so there is no anchor) |
19
+
20
+ There is no `expanded` (expansion *is* zoom).
21
+
22
+ ```typescript
23
+ const stop = bindViewState(el, {
24
+ load: () => JSON.parse(localStorage.getItem(`org`) ?? `null`),
25
+ save: s => localStorage.setItem(`org`, JSON.stringify(s)),
26
+ });
27
+ el.root = await fetchOrg(); // zoom + selection apply as their nodes arrive
28
+ ```
29
+
30
+ - State for nodes not loaded yet stays pending and applies when they appear — whether the tree is reassigned, grown in place (then `requestUpdate()`), or lazily loaded. **Reassigning `root` keeps zoom, selection and cursor** where keys still exist; state for keys that have gone moves to pending (silently) and returns with them.
31
+ - `zoom` is a path so a lazy tree can be opened down to it: while pending, the nodes along it are loaded one at a time via `loadChildren` / `model` (set `restoreLoads = false` to only apply it as the host loads them). A zoom node that moved elsewhere in the tree is still found by its key.
32
+ - Restored selection fires `select`; a restored zoom fires `zoom` (and `expand`). Any explicit selection change drops pending selection; any explicit zoom (`zoomTo()`, `zoomOut()`, clicks, crumbs, `navigateTo()`…) drops a pending zoom.
33
+
34
+ | Property | Type | Default | Description |
35
+ |---|---|---|---|
36
+ | `restoreLoads` | `boolean` | `true` | Load nodes along a pending saved zoom path |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
- "version": "0.15.5",
4
- "generated": "2026-10-23T07:10:01.881Z",
3
+ "version": "0.16.0",
4
+ "generated": "2026-10-23T13:15:45.063Z",
5
5
  "components": [
6
6
  {
7
7
  "name": "ac-text",
@@ -98,6 +98,11 @@
98
98
  "summary": "`<ixfx-hyperbolictree-list>` — a hyperbolic (Poincaré disk) browser over tree data. The focused node sits at the disk's center; its neighbourhood fans outward, shrinking toward the edge with tree-grap",
99
99
  "path": "docs-user/hyperbolic-tree.md"
100
100
  },
101
+ {
102
+ "name": "icicle-tree",
103
+ "summary": "`<ixfx-icicletree-list>` — an icicle (partition) view over tree data. Each depth is a band; each node a rectangle sized by its `valueAccessor` share of its parent. Clicking a branch zooms so it become",
104
+ "path": "docs-user/icicle-tree.md"
105
+ },
101
106
  {
102
107
  "name": "icons",
103
108
  "summary": "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.",
@@ -273,6 +278,11 @@
273
278
  "summary": "Three components share a common `TreeComponent` interface: **`ixfx-tree-list`**, **`ixfx-miller-list`**, and **`ixfx-crumb-navigation`**. This document describes the shared API, plus component-specifi",
274
279
  "path": "docs-user/tree.md"
275
280
  },
281
+ {
282
+ "name": "treemap",
283
+ "summary": "`<ixfx-treemap-list>` — a nested-area (treemap) view over tree data. The zoom root fills the viewport; each node's rectangle is subdivided among its children by `valueAccessor` share. Clicking a branc",
284
+ "path": "docs-user/treemap.md"
285
+ },
276
286
  {
277
287
  "name": "twosplit-layout",
278
288
  "summary": "A Lit web component that lays out two elements within a container with a split bar to adjust their proportions.",
@@ -30,6 +30,7 @@ import '@ixfx/components/data-grid';
30
30
  - [grid-list](docs-user/grid-list.md): A virtualised grid of items — thumbnails, cards, or arbitrary slotted markup —
31
31
  - [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
32
32
  - [hyperbolic-tree](docs-user/hyperbolic-tree.md): `<ixfx-hyperbolictree-list>` — a hyperbolic (Poincaré disk) browser over tree data. The focused node sits at the disk's center; its neighbourhood fans outward, shrinking toward the edge with tree-grap
33
+ - [icicle-tree](docs-user/icicle-tree.md): `<ixfx-icicletree-list>` — an icicle (partition) view over tree data. Each depth is a band; each node a rectangle sized by its `valueAccessor` share of its parent. Clicking a branch zooms so it become
33
34
  - [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.
34
35
  - [incr-search](docs-user/incr-search.md): Utilities for incremental (type-ahead) search over data collections or DOM-backed lists.
35
36
  - [labelled-radial-input](docs-user/labelled-radial-input.md): A composite radial input that pairs an editable label (`ixfx-editable-number`) with a rotary dial (`ixfx-radial-input`). The two controls are always in sync — turning the dial updates the number, and
@@ -65,6 +66,7 @@ import '@ixfx/components/data-grid';
65
66
  - [toolbar](docs-user/toolbar.md): Two components for building toolbars:
66
67
  - [transitory-label](docs-user/transitory-label.md): An inline text label that animates between values instead of changing instantly. Displays as `inline-block` and inherits text styling from its parent.
67
68
  - [tree](docs-user/tree.md): Three components share a common `TreeComponent` interface: **`ixfx-tree-list`**, **`ixfx-miller-list`**, and **`ixfx-crumb-navigation`**. This document describes the shared API, plus component-specifi
69
+ - [treemap](docs-user/treemap.md): `<ixfx-treemap-list>` — a nested-area (treemap) view over tree data. The zoom root fills the viewport; each node's rectangle is subdivided among its children by `valueAccessor` share. Clicking a branc
68
70
  - [twosplit-layout](docs-user/twosplit-layout.md): A Lit web component that lays out two elements within a container with a split bar to adjust their proportions.
69
71
  - [util](docs-user/util.md): A tooltip component that renders a value with optional icon and formatted number display. Uses native render root (no shadow DOM).
70
72
  - [vertical-list](docs-user/vertical-list.md): A scrollable vertical list of items with selection, keyboard navigation, and incremental search. Items can be plain `<li>` elements declared in HTML or rich custom elements added programmatically. Lar
@@ -16,7 +16,8 @@ A Miller column component displays hierarchical data as multiple vertical column
16
16
 
17
17
  | Property | Type | Default | Description |
18
18
  |----------|------|---------|-------------|
19
- | `root` | `MillerNode \| undefined` | — | The root node of the tree. Assigning a new root resets all columns and selection state. |
19
+ | `root` | `MillerNode \| undefined` | — | The root node of the tree. Assigning a new root re-applies the open path, selection and cursor by key (see [View state](#view-state)); a rebased view is reset. |
20
+ | `restoreLoads` | `boolean` | `true` | Whether restoring view state may call `loadChildren` / `model` to open a saved path through unloaded branches. |
20
21
  | `selectionMode` | `MillerSelectionMode` | `'implicit'` | Controls how items are selected. See [Selection Modes](#selection-modes) below. |
21
22
  | `selectedKeys` | `ReadonlySet<string>` (getter) / `Iterable<string>` (setter) | — | The set of currently selected item keys. Assigning replaces the entire selection and triggers a re-render. |
22
23
  | `loadChildren` | `MillerLoadChildrenCallback \| undefined` | — | Callback to load children for lazy nodes. If undefined, clicking a lazy node opens an empty column. |
@@ -57,18 +58,18 @@ Inherited from theme:
57
58
  navigateTo(keys: readonly string[], options?: { reveal?: 'edge' | 'center-if-hidden'; silent?: boolean }): boolean
58
59
 
59
60
  // Re-sync after children arrays were patched in place (used by MillerTreeController.sync()):
60
- // closes columns whose selected node was removed, then re-renders.
61
+ // the open path and selection re-resolve by key; removed nodes close their
62
+ // columns / are deselected, and come back if their key reappears.
61
63
  invalidateStructure(): void
62
64
 
63
65
  // Navigate to the first loaded node (breadth-first) matching a predicate.
64
66
  // Returns false if no match is found in the currently loaded tree.
65
67
  navigateToNode(predicate: (node: MillerNode) => boolean): boolean
66
68
 
67
- // Get current configuration (column widths, etc.)
68
- getConfig(): MillerConfig
69
-
70
- // Set configuration. Requires a full MillerConfig object.
71
- setConfig(config: MillerConfig): void
69
+ // View state — see "View state" below
70
+ getViewState(options?): MillerListViewState
71
+ setViewState(state: Partial<MillerListViewState>): void
72
+ clearPendingViewState(): void
72
73
  ```
73
74
 
74
75
  ### Events
@@ -190,10 +191,6 @@ type MillerNode = {
190
191
  item: MillerItem;
191
192
  children?: MillerNode[]; // undefined = unloaded, [] = confirmed empty
192
193
  };
193
-
194
- type MillerConfig = {
195
- columnWidths: Record<number, number>; // depth -> px
196
- };
197
194
  ```
198
195
 
199
196
  ### Per-item icon colour, text colour and badges
@@ -338,24 +335,58 @@ el.navigateToNode(node => node.item.label === 'System32');
338
335
 
339
336
  ### Column Resizing
340
337
 
341
- Columns can be resized by dragging the handle on the right edge of each column. The widths are persisted in the config:
338
+ Columns can be resized by dragging the handle on the right edge of each
339
+ column. Widths are part of the view state (`columnWidths`, keyed by depth):
342
340
 
343
341
  ```javascript
344
- // Serialize current state (e.g. to localStorage)
345
- const config = el.getConfig();
346
- localStorage.setItem('millerConfig', JSON.stringify(config));
347
-
348
- // Restore saved state
349
- el.setConfig(JSON.parse(localStorage.getItem('millerConfig')));
342
+ el.setViewState({ columnWidths: { 0: 150, 1: 200, 2: 180 } });
350
343
  ```
351
344
 
352
- ### Configuration
345
+ ### View state
346
+
347
+ `getViewState()` / `setViewState()` snapshot and restore how the columns are
348
+ showing the data, as plain JSON keyed by `item.key`. The component never
349
+ stores it; `bindViewState()` wires it to storage. See
350
+ [docs/infra-view-state.md](../../docs/infra-view-state.md).
353
351
 
354
352
  ```javascript
355
- // Set custom column widths (depth index → width in px)
356
- el.setConfig({ columnWidths: { 0: 150, 1: 200, 2: 180 } });
353
+ import { bindViewState } from '@ixfx/components';
354
+
355
+ bindViewState(el, {
356
+ load: () => JSON.parse(localStorage.getItem('miller') ?? 'null'),
357
+ save: s => localStorage.setItem('miller', JSON.stringify(s)),
358
+ });
359
+ el.root = await fetchRoot(); // saved state applies as nodes arrive
357
360
  ```
358
361
 
362
+ `kind: 'miller-list'`. Fields:
363
+
364
+ | Field | Type | Meaning |
365
+ |---|---|---|
366
+ | `path` | `string[]` | Key of the open branch at each depth, column 0 first, relative to the view root. |
367
+ | `selected` | `string[]` | `selectedKeys` (the checked set in `checked` mode). |
368
+ | `focus` | `string` | Keyboard cursor key, only when last moved by keyboard. |
369
+ | `columnWidths` | `Record<string, number>` | User-resized widths in px, keyed by depth. |
370
+ | `columnScroll` | `Record<string, ScrollState>` | Scroll of each scrolled column, keyed by the key of the node whose children it lists (the view root's key for column 0). |
371
+
372
+ `path` is an ordered array rather than the shared `expanded` record because a
373
+ Miller view has exactly one open branch per column.
374
+
375
+ - State for keys not loaded yet stays **pending** and applies as nodes appear:
376
+ a new `root`, `invalidateStructure()` after children were patched in place,
377
+ or a lazy load. A saved path `a > b > c` opens as each node arrives.
378
+ - With `restoreLoads` (default `true`) a saved path through unloaded branches
379
+ is opened via `loadChildren` / `model`, one load at a time, so it cascades.
380
+ - New `root` data keeps the open path, selection and cursor for keys that
381
+ still exist; keys that left go pending (deselected silently) and come back
382
+ if they reappear. Restored selection fires `select` with
383
+ `origin: 'programmatic'`.
384
+ - Any explicit selection change drops pending selection; any explicit
385
+ navigation (click, arrow keys, `navigateTo`, rebasing) drops the pending
386
+ path. The view root itself is not part of the state.
387
+ - `view-state-change` fires (debounced) on selection, navigation, keyboard
388
+ cursor moves, column resize and column scroll.
389
+
359
390
  ---
360
391
 
361
392
  ## MillerTreeController
@@ -22,14 +22,14 @@ provider returns that column's choices; items may carry a `count` for display.
22
22
  | `selectionMode` | `ListSelectionMode` | `'multiple'` | `'none'`, `'single'` or `'multiple'` selection within a column. |
23
23
  | `minColumns` | `number` | `1` | Floor for column removal. |
24
24
  | `maxColumns` | `number` | `5` | Ceiling for column addition; `0` = unlimited. |
25
- | `columns` | `readonly MulticolColumnState[]` (getter) | — | Current per-column config: `id`, `categoryKey`, `sort`, `grouped`, `menu` (default `true`), `width`. Persist this to restore a layout. |
26
25
 
27
26
  ### Methods
28
27
 
29
28
  ```typescript
30
- // Restore a saved layout. Matching ids re-use live columns; unknown categories are skipped.
31
- // A first assignment suppresses the default auto-seeded column.
32
- setColumns(states: readonly MulticolColumnState[]): void
29
+ // View state: column layout, per-column selection and scroll (see below)
30
+ getViewState(options?: GetViewStateOptions<MulticolListViewState>): MulticolListViewState
31
+ setViewState(state: Partial<MulticolListViewState>): void
32
+ clearPendingViewState(): void
33
33
 
34
34
  // Add a column for a category key, optionally at a position.
35
35
  addColumn(categoryKey: string, position?: number): void
@@ -49,15 +49,16 @@ clearSelection(): void
49
49
  autoSizeColumns(options?: { minWidth?: number; maxWidth?: number }): Promise<void>
50
50
  ```
51
51
 
52
- Without `setColumns`, the element auto-seeds one column (the first category) once
53
- `categories` is assigned.
52
+ Unless a layout is restored with `setViewState({ columns })`, the element auto-seeds
53
+ one column (the first category) once `categories` is assigned.
54
54
 
55
55
  ### Events
56
56
 
57
57
  | Event | Detail | Description |
58
58
  |-------|--------|-------------|
59
59
  | `multicol-select` | `{ columnId, columns }` | A column's selection changed. `columns` is a full snapshot (see `getSelection`). |
60
- | `multicol-columns-change` | `{ columns, reason }` | Column layout changed. `reason` is one of `'add' \| 'remove' \| 'reorder' \| 'category' \| 'sort' \| 'grouping' \| 'resize'`. Resize fires **once on pointer-up** with the final width — never during the drag. |
60
+ | `multicol-columns-change` | `{ columns, reason }` | Column layout changed. `columns` is the live layout (`MulticolColumnState[]`, as in view state). `reason` is one of `'add' \| 'remove' \| 'reorder' \| 'category' \| 'sort' \| 'grouping' \| 'resize'` (a view-state restore reports `'add'`). Resize fires **once on pointer-up** with the final width — never during the drag. |
61
+ | `view-state-change` | `{ state, reason }` | Debounced view-state snapshot; see [View state](#view-state). |
61
62
 
62
63
  ---
63
64
 
@@ -78,6 +79,47 @@ at the top, selected by default. Choosing values clears All; choosing All clears
78
79
 
79
80
  ---
80
81
 
82
+ ## View state
83
+
84
+ Implements the shared view-state contract (`docs/infra-view-state.md`); `kind` is
85
+ `multicol-list`. Persist it with `bindViewState()`:
86
+
87
+ ```typescript
88
+ import { bindViewState } from '@ixfx/components';
89
+
90
+ bindViewState(el, {
91
+ load: () => JSON.parse(localStorage.getItem(`photos-facets`) ?? `null`),
92
+ save: s => localStorage.setItem(`photos-facets`, JSON.stringify(s)),
93
+ });
94
+ ```
95
+
96
+ | Field | Type | Meaning |
97
+ |---|---|---|
98
+ | `columns` | `MulticolColumnState[]` | Layout in display order: `{ id, categoryKey, sort, grouped, menu?, width? }`. |
99
+ | `selectedByColumn` | `Record<columnId, string[]>` | Selected item keys per column. A column that's absent (or `[]`) is at 'All'. |
100
+ | `scrollByColumn` | `Record<columnId, ScrollState>` | Each column list's scroll, anchored to the first visible item key. |
101
+
102
+ The shared `selected` / `scroll` / `focus` / `expanded` fields are not used.
103
+
104
+ - **`columns`** re-uses live columns by `id` (re-pointing the category if it differs,
105
+ which resets that column to 'All' and applies the category defaults, then the
106
+ saved `sort` / `grouped` / `width`), creates the others, reorders to match and
107
+ removes columns not listed. A non-empty layout suppresses auto-seeding. A column
108
+ whose category isn't in `categories` stays **pending** and is inserted at its
109
+ saved position once `categories` includes it.
110
+ - **Selection** keys a column's data doesn't contain stay pending and are selected
111
+ when items with those keys arrive (firing the normal `multicol-select` and
112
+ cascade). Keys that leave a column's data are deselected and go pending. Any
113
+ explicit selection change (click, the All row, `clearSelection()`) drops all
114
+ pending selection. Selection saved for a column id that doesn't exist yet is
115
+ handed over when that column is restored.
116
+ - **Scroll** waits until the anchor item has rendered; the user scrolling the
117
+ column cancels it.
118
+ - `getViewState()` includes pending state by default (`{ includePending: false }`
119
+ for live state only).
120
+
121
+ ---
122
+
81
123
  ## Header interactions
82
124
 
83
125
  - **Category label** — opens a menu of all categories; the current one is disabled.
@@ -126,7 +168,7 @@ el.dataProvider = (query: MulticolQuery, signal) => {
126
168
  };
127
169
 
128
170
  el.addEventListener(`multicol-select`, (event) => {
129
- saveLayout(event.detail.columns);
171
+ showResults(event.detail.columns);
130
172
  });
131
173
  ```
132
174
 
@@ -12,7 +12,7 @@ A Lit web component that lays out any number of panels along one axis with dragg
12
12
  |-----------|------|---------|-------------|
13
13
  | `direction` | `row` \| `column` | `row` | Split direction. `row` creates side-by-side panels, `column` creates stacked panels. |
14
14
  | `sizes` | string | `''` | Positional weight shorthand, e.g. `"1 2 1"`, applied to children lacking an explicit `size`. |
15
- | `storageKey` | string | `''` | Enables `localStorage` persistence. |
15
+ | `forget-absent` | boolean | `false` | Don't keep the saved size of children that leave (see [View state](#view-state)). |
16
16
  | `gutterSize` | string | — | Convenience mirror of `--split-gutter-size`. |
17
17
  | `snap` | string | — | Convenience mirror of `--split-snap`. |
18
18
 
@@ -116,7 +116,8 @@ Nested `<ixfx-split-layout>` elements create sub-groups:
116
116
  | `ixfx-split-collapse` | `{ id: string; index: number }` | no | Panel collapsed (drag or API). |
117
117
  | `ixfx-split-expand` | `{ id: string; index: number }` | no | Panel expanded. |
118
118
  | `ixfx-split-close` | `{ id: string; index: number }` | **yes** | Before a panel is removed; `preventDefault()` vetoes. |
119
- | `ixfx-split-change` | `{ layout: LayoutSnapshot }` | no | After any size/structure change (persistence hook). |
119
+ | `ixfx-split-change` | `{ layout: LayoutSnapshot }` | no | After any size/structure change. For persistence use [view state](#view-state). |
120
+ | `view-state-change` | `{ state: SplitLayoutViewState; reason }` | no | Debounced; see [View state](#view-state). |
120
121
 
121
122
  ---
122
123
 
@@ -194,25 +195,52 @@ split.beginGutterDrag(0, event.pointerId, event.clientX, event.clientY);
194
195
  handle that starts this drag alongside perpendicular drags on other nested
195
196
  layouts, resizing several splits with one gesture.
196
197
 
197
- ### Layout Serialization
198
+ ---
198
199
 
199
- ```javascript
200
- // Get current layout
201
- const layout = split.getLayout();
202
- // { version: 1, root: { kind: 'group', id: 'g-0', children: [...] } }
200
+ ## View state
203
201
 
204
- // Restore a layout
205
- split.setLayout(layout);
206
- ```
202
+ `getViewState()` / `setViewState()` snapshot and restore the size and
203
+ collapse of each direct child that has an `id`, as plain JSON (`kind:
204
+ 'split-layout'`). The component never persists anything itself — see
205
+ [docs/infra-view-state.md](../../docs/infra-view-state.md) and
206
+ `bindViewState()`.
207
207
 
208
- ---
208
+ ```ts
209
+ import { bindViewState } from '@ixfx/components';
209
210
 
210
- ## Persistence
211
-
212
- Set `storage-key` to enable automatic localStorage persistence:
211
+ bindViewState(split, {
212
+ load: () => JSON.parse(localStorage.getItem(`editor-split`) ?? `null`),
213
+ save: s => localStorage.setItem(`editor-split`, JSON.stringify(s)),
214
+ });
213
215
 
214
- ```html
215
- <ixfx-split-layout storage-key="my-app-layout" ...>
216
+ split.getViewState();
217
+ // { v: 1, kind: 'split-layout',
218
+ // layout: { editor: { weight: 0.7 }, terminal: { weight: 0.3, collapsed: true } } }
216
219
  ```
217
220
 
218
- The layout is saved automatically after each change and restored on page load.
221
+ | Field | Type | Meaning |
222
+ |---|---|---|
223
+ | `layout` | `Record<id, { weight: number; collapsed?: boolean }>` | `weight` is the child's share (0..1) of the weighted flow; for a collapsed child, the share it expands back to. `collapsed` absent ⇒ expanded. |
224
+
225
+ - **Keyed by `id`.** Children without an `id` (and nested layouts without
226
+ one) are not saved — generated ids are positional. A nested
227
+ `ixfx-split-layout` with an `id` is saved as one entry (its share in this
228
+ layout); its own children are in *its* view state.
229
+ - **Pending.** An entry whose child isn't slotted yet is kept and applied when
230
+ a child with that id appears; a child that leaves goes back to pending, so
231
+ removing and re-adding it restores its size. `getViewState()` includes
232
+ pending entries (pass `includePending: false` to leave them out);
233
+ `clearPendingViewState()` drops them.
234
+ - **Partial.** Children without an entry keep their relative size and share
235
+ what the restored children leave.
236
+ - **Events.** `view-state-change` fires (debounced) after a gutter drag ends,
237
+ collapse / expand, close, `setSizes()`, and when pending entries are applied
238
+ (`reason: 'restore'`).
239
+ - `forget-absent` (boolean attribute) turns off the pending behaviour for
240
+ children that leave — for hosts that drive child sizes themselves (used
241
+ by `ixfx-dock-host`).
242
+
243
+ A rebuild caused by children being added / removed keeps the live (e.g.
244
+ dragged) sizes of children that stayed; a changed `size` / `weight` /
245
+ `sizes` / `fixed` / `collapsed` attribute on a direct child or the layout
246
+ makes the DOM the source of truth again.
@@ -17,7 +17,8 @@ It implements the shared `TreeComponent` interface (see [`src/tree/README.md`](.
17
17
  7. [Zoom, selection, navigation](#zoom-selection-navigation)
18
18
  8. [Events](#events)
19
19
  9. [Keyboard / pointer interaction](#keyboard--pointer-interaction)
20
- 10. [CSS variables and parts](#css-variables-and-parts)
20
+ 10. [View state](#view-state)
21
+ 11. [CSS variables and parts](#css-variables-and-parts)
21
22
 
22
23
  ---
23
24
 
@@ -26,7 +27,7 @@ It implements the shared `TreeComponent` interface (see [`src/tree/README.md`](.
26
27
  - **Expand/collapse is reinterpreted as zoom.** `expand` fires on zoom-in, `collapse` on zoom-out.
27
28
  - **`interactionMode` drops `'checked'`**; **`exclusivity` is accepted but ignored**.
28
29
  - **`filterPredicate` dims** non-matching arcs instead of hiding them.
29
- - **Reassigning `root` keeps zoom and selection** wherever the same key paths still exist (the icicle and treemap reset instead). See [Progressive data](#progressive-data).
30
+ - **Reassigning `root` keeps zoom and selection** wherever the same key paths still exist; state whose path has gone stays pending and comes back with it. See [Progressive data](#progressive-data) and [View state](#view-state).
30
31
  - Drag-and-drop is out of scope.
31
32
 
32
33
  ## Data model
@@ -171,6 +172,35 @@ All events **bubble** and are **composed**. `depth` counts from the root's child
171
172
  | `Enter` | Select, activate and zoom in |
172
173
  | `Escape` / `Backspace` | Zoom up one level |
173
174
 
175
+ ## View state
176
+
177
+ Implements the shared view-state API (`getViewState()` / `setViewState()` / `clearPendingViewState()` / `view-state-change`) — see [`docs/infra-view-state.md`](../../docs/infra-view-state.md). `kind` is `sunbursttree-list`.
178
+
179
+ | Field | Type | Meaning |
180
+ |---|---|---|
181
+ | `selected` | `string[]` | Selected `item.key`s |
182
+ | `zoom` | `string[]` | Key path from the root's children to the centre node; `[]` = full tree |
183
+ | `focus` | `string` | Keyboard-cursor `item.key` (recorded only when the cursor was moved by keyboard) |
184
+
185
+ There is no `expanded` (expansion *is* zoom) and no `scroll`.
186
+
187
+ ```typescript
188
+ const stop = bindViewState(el, {
189
+ load: () => JSON.parse(localStorage.getItem(`disk`) ?? `null`),
190
+ save: s => localStorage.setItem(`disk`, JSON.stringify(s)),
191
+ });
192
+ el.root = await scan(); // zoom + selection apply as their nodes arrive
193
+ ```
194
+
195
+ - State for nodes not loaded yet stays pending and applies when they appear — whether the tree is reassigned, grown in place, or lazily loaded. A data change that removes the zoom node or a selected node moves it back to pending (silently), so it returns with its path.
196
+ - `zoom` is a path because keys only need to be unique among siblings. While it is pending, the nodes along it are loaded one at a time via `loadChildren` / `model` (set `restoreLoads = false` to only apply it as the host loads them).
197
+ - Live selection follows its key path across data changes. A restored `selected` key matches the first node, depth-first, carrying it — so with repeated keys (`readme.md` in many folders) a restored selection can land on a different node.
198
+ - Restored selection fires `select`; a restored zoom fires `zoom` (and `expand`). Any explicit selection change drops pending selection; any explicit zoom (`zoomTo()`, clicks, `navigateTo()`…) drops a pending zoom.
199
+
200
+ | Property | Type | Default | Description |
201
+ |---|---|---|---|
202
+ | `restoreLoads` | `boolean` | `true` | Load nodes along a pending saved zoom path |
203
+
174
204
  ## CSS variables and parts
175
205
 
176
206
  | Variable | Default | Description |