@ixfx/components 0.15.5 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/bundle/index.d.ts +986 -49
  2. package/bundle/index.js +765 -729
  3. package/dist/ac-token.js +1 -1
  4. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  5. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  6. package/dist/crumbs.d.ts +2 -115
  7. package/dist/crumbs.js +1 -1179
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-Cqxuwgbb.js} +615 -210
  9. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-Csh5EJW9.d.ts} +55 -4
  10. package/dist/data-grid.d.ts +3 -3
  11. package/dist/data-grid.js +1 -1
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-Bo5ze3SN.js} +234 -9
  13. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-DSZ1DDor.d.ts} +48 -4
  14. package/dist/detail-list.d.ts +4 -4
  15. package/dist/detail-list.js +1 -1
  16. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-CT9kX3TQ.d.ts} +27 -4
  17. package/dist/flat-list.d.ts +2 -2
  18. package/dist/flat-list.js +123 -4
  19. package/dist/{grid-list-y6dWJYu3.js → grid-list-D0dJNVEF.js} +278 -76
  20. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-DDLrWRxm.d.ts} +128 -22
  21. package/dist/grid-list.d.ts +3 -3
  22. package/dist/grid-list.js +1 -1
  23. package/dist/{incr-search-JIhwB49y.js → incr-search-CgbL-OlS.js} +140 -10
  24. package/dist/incr-search.d.ts +1 -1
  25. package/dist/incr-search.js +1 -1
  26. package/dist/index-9ttcdwlL.d.ts +71 -0
  27. package/dist/index-Bnm-kKGC.d.ts +475 -0
  28. package/dist/{index-KN2MW_xD.d.ts → index-CUErGhCg.d.ts} +31 -4
  29. package/dist/{index-DEVyVIn8.d.ts → index-D-lZNlHa.d.ts} +52 -6
  30. package/dist/index.d.ts +341 -21
  31. package/dist/index.js +2286 -884
  32. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  33. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  34. package/dist/miller-DUqiUAWQ.js +2230 -0
  35. package/dist/miller.d.ts +4 -437
  36. package/dist/miller.js +1 -1815
  37. package/dist/narrowed-text.js +1 -1
  38. package/dist/pending-BGidHppL.js +91 -0
  39. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  40. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  41. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  42. package/dist/split-layout.d.ts +1 -1
  43. package/dist/split-layout.js +1 -1
  44. package/dist/sunburst-tree.d.ts +2 -2
  45. package/dist/sunburst-tree.js +1 -1
  46. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_M0YTEA.js} +299 -80
  47. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-DePuFYlU.d.ts} +40 -3
  48. package/dist/tree-CftHwtEb.js +146 -0
  49. package/dist/{tree-D8VxvR9u.js → tree-DAlgvaHB.js} +276 -48
  50. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  51. package/dist/tree.d.ts +3 -3
  52. package/dist/tree.js +1 -1
  53. package/dist/types-LU1u0CB8.d.ts +87 -0
  54. package/dist/vertical-list-DyKPKGxK.js +1993 -0
  55. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-SAx56U-l.d.ts} +31 -3
  56. package/dist/vertical-list.d.ts +3 -3
  57. package/dist/vertical-list.js +1 -1789
  58. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  59. package/docs-user/README.md +2 -0
  60. package/docs-user/crumbs.md +27 -0
  61. package/docs-user/data-grid.md +79 -5
  62. package/docs-user/detail-list.md +69 -0
  63. package/docs-user/dock.md +63 -12
  64. package/docs-user/flat-list.md +27 -0
  65. package/docs-user/grid-list.md +69 -13
  66. package/docs-user/hyperbolic-tree.md +31 -4
  67. package/docs-user/icicle-tree.md +36 -0
  68. package/docs-user/index.json +12 -2
  69. package/docs-user/llms.txt +2 -0
  70. package/docs-user/miller.md +52 -21
  71. package/docs-user/multicol-list.md +50 -8
  72. package/docs-user/split-layout.md +45 -17
  73. package/docs-user/sunburst-tree.md +32 -2
  74. package/docs-user/tree.md +45 -6
  75. package/docs-user/treemap.md +32 -0
  76. package/docs-user/twosplit-layout.md +22 -0
  77. package/docs-user/user-catalog.md +1 -1
  78. package/docs-user/vertical-list.md +55 -7
  79. package/llms.txt +2 -0
  80. package/package.json +2 -2
@@ -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 |
package/docs-user/tree.md CHANGED
@@ -20,6 +20,7 @@ Three components share a common `TreeComponent` interface: **`ixfx-tree-list`**,
20
20
  7. [ARIA](#aria)
21
21
  8. [CSS variables](#css-variables)
22
22
  9. [Controller authoring guide](#controller-authoring-guide)
23
+ 10. [View state](#view-state-ixfx-tree-list)
23
24
 
24
25
  ---
25
26
 
@@ -148,11 +149,12 @@ These properties are present on all three components (`ixfx-tree-list`, `ixfx-mi
148
149
  | `interactionMode` | `TreeInteractionMode` | `'standard'` | How gestures map to selection changes (`ixfx-tree-list` / `ixfx-miller-list` only) |
149
150
  | `selectionFilter` | `'none' \| 'leaf' \| 'branch'` | `'leaf'` | Which node kinds can be selected (reflected attribute) |
150
151
  | `alignCheckboxes` (`align-checkboxes`) | `boolean` | `false` | In `checked` mode, unselectable rows keep the space their checkbox would take so they line up with selectable siblings (`ixfx-tree-list` only). See [Unselectable items](#unselectable-items-selectable-aligncheckboxes) |
151
- | `filterPredicate` | `TreeFilterPredicate \| undefined` | `undefined` | Filter; only matching nodes and their ancestors are rendered (non-matching rows are not in the DOM, keyboard navigation, `selectAll` and Shift-range skip them) |
152
+ | `filterPredicate` | `TreeFilterPredicate \| undefined` | `undefined` | Filter; only matching nodes and their ancestors are rendered (non-matching rows are not in the DOM; keyboard navigation, `selectAll`/`deselectAll` and Shift-range skip them) |
152
153
  | `exclusivity` | `'none' \| 'depth' \| 'global'` | `'none'` | Expansion exclusivity (reflected attribute) |
153
154
  | `stickyHeaders` (`sticky-headers`) | `boolean` | `true` | VS Code-style sticky headers; set `sticky-headers="false"` to opt out (`ixfx-tree-list` only) |
154
155
  | `virtualize` | `'auto' \| 'on' \| 'off'` | `'auto'` | Render only the rows near the viewport; `auto` switches on above `virtualizeThreshold` visible rows (`ixfx-tree-list` only). See [Virtualisation](#virtualisation-ixfx-tree-list) |
155
156
  | `virtualizeThreshold` (`virtualize-threshold`) | `number` | `200` | Visible-row count above which `virtualize="auto"` windows the list |
157
+ | `scrollHoverTickle` (`scroll-hover-tickle`) | `boolean` | `false` | Let the hover cursor follow rows that scroll beneath a stationary pointer. Off by default: during a scroll gesture row `pointerenter` is ignored (host carries `data-scroll-hover-suppress`), so the hover highlight and `tickle` hold still instead of trailing the content. Hover resumes on the next real pointer movement or when the pointer leaves |
156
158
  | `animation` | `'auto' \| 'always' \| 'never'` | `'auto'` | Controls row enter/leave animations. `'auto'` respects `prefers-reduced-motion`; `'always'` always animates; `'never'` skips all animations. Can be overridden per-call on `expandAll`, `collapseAll`, `navigateTo`, `revealKey`, and `navigateToNode` |
157
159
 
158
160
  ### Replacing `root`
@@ -387,14 +389,14 @@ el.selectAll();
387
389
  el.deselectAll();
388
390
  ```
389
391
 
390
- `selectAll()` and `deselectAll()` behaviour depends on `interactionMode`:
392
+ `selectAll()` selects every selectable node in the tree, including nodes inside collapsed branches (unloaded lazy children are not included). `deselectAll()` deselects the same set. Pass `{ visibleOnly: true }` to act only on the rows that are showing; Ctrl+A and the `select-all` command do this.
391
393
 
392
- | Mode | `selectAll()` | `deselectAll()` |
394
+ | Mode | `selectAll()` | `selectAll({ visibleOnly: true })` |
393
395
  |------|---------------|-----------------|
394
- | `checked` | Checks the checkbox of each visible node (recursively toggles descendants) | Unchecks each visible node |
395
- | All other modes | Adds every visible selectable node to the selection | Clears the selection |
396
+ | `checked` | Checks every selectable node | Same: checking a row checks its descendants |
397
+ | All other modes | Adds every selectable node to the selection | Adds the showing selectable rows |
396
398
 
397
- While a `filterPredicate` is active, `selectAll()` / `deselectAll()` and Shift-range selection cover only the rows that are shown (as in `ixfx-vertical-list`).
399
+ While a `filterPredicate` is active, `selectAll()` / `deselectAll()` cover only the matching nodes (and their matching ancestors), and Shift-range selection covers only the rows that are shown (as in `ixfx-vertical-list`). `deselectAll()` leaves selected nodes the filter hides alone.
398
400
 
399
401
  Both methods dispatch a single `select` event with the full old and new selection sets. `selectAll` respects `selectionFilter` — only nodes that pass the filter are selected.
400
402
 
@@ -455,6 +457,43 @@ Notes:
455
457
 
456
458
  ---
457
459
 
460
+ ## View state (`ixfx-tree-list`)
461
+
462
+ Snapshot and restore selection, expansion, keyboard cursor and scroll
463
+ separately from the data — see [`docs/infra-view-state.md`](../../docs/infra-view-state.md)
464
+ for the shared contract. `kind: 'tree-list'`; fields `selected`, `expanded`,
465
+ `focus`, `scroll`, all keyed by `item.key`.
466
+
467
+ ```typescript
468
+ import { bindViewState } from '@ixfx/components';
469
+
470
+ bindViewState(tree, {
471
+ load: () => JSON.parse(localStorage.getItem(`tree`) ?? `null`),
472
+ save: s => localStorage.setItem(`tree`, JSON.stringify(s)),
473
+ });
474
+ tree.root = initialRoot; // any time, before or after
475
+ ```
476
+
477
+ - **Keys, not nodes.** Replacing `root` keeps selection, expansion and cursor
478
+ for every key the new root still has. State for keys that disappear is kept
479
+ *pending* and comes back if they reappear — expand `C`, swap in a root
480
+ without it, swap back, and `C` is open again.
481
+ - **Built up over time.** `setViewState({ expanded: { A: true, B: true, C: true } })`
482
+ then grow the tree (new `root`, or mutate `children` + `invalidateStructure()`):
483
+ each branch opens as it arrives.
484
+ - **Lazy trees.** With `restoreLoads` (default `true`) saved-open branches that
485
+ aren't loaded are fetched through `loadChildren` / `model`, one at a time, so
486
+ a saved path cascades open. `el.restoreLoads = false` to only open branches
487
+ the host loads itself.
488
+ - **Selection.** A restored key fires the normal `select` event
489
+ (`origin: 'programmatic'`). Any explicit selection change — click, key,
490
+ `select()`, `clearSelection()` — drops a saved selection that hasn't matched yet.
491
+ - **Scroll** is saved as the first visible row's key plus an offset, and is
492
+ applied once that row exists. The user's own scroll cancels it.
493
+ - `focus` is recorded only for a keyboard-moved cursor, not mouse hover.
494
+
495
+ ---
496
+
458
497
  ## Events
459
498
 
460
499
  All events **bubble** and are **composed** (cross shadow-DOM boundaries).
@@ -0,0 +1,32 @@
1
+ # Treemap
2
+
3
+ `<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 branch zooms into it, and ancestors collapse into a breadcrumb strip. It implements the shared `TreeComponent` interface (see [`src/tree/README.md`](../tree/README.md)). See the class docs in `treemap-list.ts` for properties and events.
4
+
5
+ ---
6
+
7
+ ## View state
8
+
9
+ 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 `treemap-list`.
10
+
11
+ ```typescript
12
+ type TreemapListViewState = ViewStateBase & {
13
+ zoom?: string[]; // key path from the root's children to the zoom root; [] = full tree
14
+ };
15
+ ```
16
+
17
+ | Field | Meaning |
18
+ |---|---|
19
+ | `selected` | Selected item keys. |
20
+ | `focus` | Keyboard cursor key. It is only recorded when the cursor was moved by keyboard or search navigation. |
21
+ | `zoom` | The zoomed-into node, as a key path. The target is matched by key anywhere in the tree. The ancestors in the path only matter when the target isn't loaded yet. |
22
+
23
+ There is no `expanded` (zoom stands in for it) and no `scroll` (the treemap fills its box).
24
+
25
+ - **Data changes keep state.** A new `root`, a model `root` / `invalidate` event or a lazy load re-applies state by key. If the zoom root goes away, the view falls back to the full tree silently and `zoom` stays pending. Selected nodes that go away are deselected silently and their keys stay pending.
26
+ - **Lazy data.** With `restoreLoads` (default `true`), a saved `zoom` whose target isn't loaded yet is reached by loading each ancestor in turn via `loadChildren` / `model`.
27
+ - **Events.** A restored selection fires `select`. A restored zoom fires `zoom` (not `expand`/`collapse`) and doesn't animate.
28
+ - An explicit zoom (click, key, wheel, crumbs, `zoomTo()` / `zoomOut()`) drops a pending `zoom`. Any explicit selection change drops pending `selected`.
29
+
30
+ | Property | Type | Default | Description |
31
+ |---|---|---|---|
32
+ | `restoreLoads` | `boolean` | `true` | Whether restoring a saved `zoom` may load its unloaded ancestors. |
@@ -46,6 +46,28 @@ theme it with the `--ixfx-handle-*` variables (see
46
46
  | Event | Detail | Description |
47
47
  |-------|--------|-------------|
48
48
  | `ixfx-split-collapsed` | `{ panel: 'A' \| 'B', collapsed: boolean }` | Fired when a panel is collapsed or uncollapsed via drag. |
49
+ | `view-state-change` | `{ state: TwoSplitLayoutViewState, reason }` | Debounced, after a drag that changed the sizes and after `setViewState()`. See [View state](#view-state). |
50
+
51
+ ### View state
52
+
53
+ `getViewState()` / `setViewState()` snapshot and restore both sides as plain
54
+ JSON (`kind: 'twosplit-layout'`), keyed by slot name. The component never
55
+ persists anything itself — see
56
+ [docs/infra-view-state.md](../../docs/infra-view-state.md) and `bindViewState()`.
57
+
58
+ ```ts
59
+ split.getViewState();
60
+ // { v: 1, kind: 'twosplit-layout', layout: { a: { size: '30%' }, b: { size: '70%' } } }
61
+ split.setViewState({ layout: { a: { size: '0px', collapsed: true }, b: { size: '100%' } } });
62
+ ```
63
+
64
+ | Field | Type | Meaning |
65
+ |---|---|---|
66
+ | `layout` | `{ a?, b?: { size: string; collapsed?: boolean } }` | `size` as in `sizeA` / `sizeB`; `collapsed` absent ⇒ expanded. A side missing from a restored `layout` is left as it is. |
67
+
68
+ Both slots always exist, so nothing is ever pending (`clearPendingViewState()`
69
+ is a no-op). Note the `layout` *field* is unrelated to the `layout`
70
+ (orientation) attribute.
49
71
 
50
72
  ---
51
73
 
@@ -124,7 +124,7 @@ A meta-component that renders a typed item list using any sub-component you supp
124
124
  ## Layout Components
125
125
 
126
126
  ### ixfx-dock-host / ixfx-dock-panel
127
- A dockable panel system in the style of VS Code. Author a flat pool of `ixfx-dock-panel` elements inside an `ixfx-dock-host`; drag panels by titlebar or tab to tab them together, split panels horizontally/vertically, or drag between hosts. Layouts serialize via `saveLayout()` / `loadLayout()` for persistence. Drop restrictions are available per host (`accepts` attribute, `canAccept()` callback) and per panel (`accepts`). Use for IDE-like workspaces, dashboards, or any composable multi-panel UI.
127
+ A dockable panel system in the style of VS Code. Author a flat pool of `ixfx-dock-panel` elements inside an `ixfx-dock-host`; drag panels by titlebar or tab to tab them together, split panels horizontally/vertically, or drag between hosts. Layouts save and restore via `getViewState()` / `setViewState()` (see `docs/infra-view-state.md`). Drop restrictions are available per host (`accepts` attribute, `canAccept()` callback) and per panel (`accepts`). Use for IDE-like workspaces, dashboards, or any composable multi-panel UI.
128
128
 
129
129
  ### ixfx-split-layout
130
130
  A resizable n-way split pane that allows users to adjust the proportion between adjacent sections. Supports `row`/`column` directions, positional `sizes` weights, per-child `size`/`min`/`collapsed` attributes, and nested splits for cross-axis layouts. Use for editor layouts, file explorers with previews, or any interface where users need to control space allocation.
@@ -20,8 +20,9 @@ A scrollable vertical list of items with selection, keyboard navigation, and inc
20
20
  12. [Context menu](#context-menu)
21
21
  13. [In-place rename](#in-place-rename)
22
22
  14. [Virtualisation](#virtualisation)
23
- 15. [CSS variables](#css-variables)
24
- 16. [CSS parts](#css-parts)
23
+ 15. [View state](#view-state)
24
+ 16. [CSS variables](#css-variables)
25
+ 17. [CSS parts](#css-parts)
25
26
 
26
27
  ---
27
28
 
@@ -387,7 +388,8 @@ list.addItem(myElement);
387
388
  // In checked mode, add a row that shows no checkbox and can't be checked
388
389
  list.addItem('Header', { notChecked: true });
389
390
 
390
- // Remove a specific element (returns a Promise that resolves once it's gone)
391
+ // Remove a specific element (returns a Promise that resolves once it's gone).
392
+ // A selected item is deselected silently; its key stays pending (see View state).
391
393
  list.removeItem(element);
392
394
  list.removeItems(list.selectedItems);
393
395
 
@@ -453,12 +455,13 @@ list.addEventListener('list-select', ({ detail }) => {
453
455
  });
454
456
  ```
455
457
 
456
- Restore a selection across a data reload:
458
+ Selection survives a data reload by itself: keys of selected items that
459
+ leave the list (`clearItems`, `removeItem`, a DOM removal, an adapter rebuild)
460
+ go pending and are re-selected when items with the same keys come back.
457
461
 
458
462
  ```typescript
459
- const keep = [...list.selectedKeys];
460
- list.setItems(await fetchRows()); // Element refs are now stale
461
- list.selectKeys(keep); // ...but the keys still resolve
463
+ list.clearItems();
464
+ list.setItems(await fetchRows()); // previously-selected keys re-select
462
465
  ```
463
466
 
464
467
  The `data-key` requirement is documented, not enforced: items without a key
@@ -768,6 +771,51 @@ dispatch. Mirrors `ixfx-grid-list`'s `grid-list-viewport-change`.
768
771
 
769
772
  ---
770
773
 
774
+ ## View state
775
+
776
+ The list implements the shared view-state API (see `docs/infra-view-state.md`):
777
+ snapshot *how* it shows its items as plain JSON and restore it later, before or
778
+ after the items exist. Kind: `vertical-list`. All state is keyed by each row's
779
+ `data-key` / `data-value`, so rows without a key don't take part.
780
+
781
+ | Field | Type | Meaning |
782
+ |---|---|---|
783
+ | `selected` | `string[]` | Selected keys. In `checked` mode this is the checked set. |
784
+ | `focus` | `string` | Keyboard cursor key. Only recorded while the cursor was last moved by keyboard. |
785
+ | `scroll` | `{ top, left?, anchor?: { key, offset } }` | First visible row's key plus px offset into it. Works with `virtualize` too. |
786
+
787
+ ```typescript
788
+ import { bindViewState } from '@ixfx/components';
789
+
790
+ bindViewState(list, {
791
+ load: () => JSON.parse(localStorage.getItem('files') ?? 'null'),
792
+ save: s => localStorage.setItem('files', JSON.stringify(s)),
793
+ });
794
+
795
+ list.getViewState(); // { v: 1, kind: 'vertical-list', selected, focus, scroll }
796
+ list.setViewState({ selected: ['a'] }); // partial: only fields present apply
797
+ list.clearPendingViewState();
798
+ list.addEventListener('view-state-change', e => e.detail /* { state, reason } */);
799
+ ```
800
+
801
+ - **Pending state.** Keys that aren't in the list yet stay pending and apply as
802
+ matching rows arrive through `addItem`, `setItems`, a plain `appendChild`, an
803
+ `items` rebuild, or a virtual registry change. `getViewState()` includes pending
804
+ state by default (`includePending: false` leaves it out).
805
+ - **Selection.** A restored selection fires the normal `list-select`. Any explicit
806
+ selection change (click, key, `select*()`, `clearSelection()`) drops pending
807
+ selection. A selected row that leaves the list is deselected **silently** (no
808
+ `list-select`) and its key goes pending.
809
+ - **Scroll** is applied once the anchor row exists and the list is laid out
810
+ (it is retried on data changes and resizes). The user's own wheel, touch,
811
+ pointer or navigation key cancels a pending scroll.
812
+ - **Focus.** A keyboard cursor whose row leaves the list goes pending and comes
813
+ back when the key reappears. Moving the cursor by keyboard, or `focusKey()`,
814
+ drops a pending one.
815
+ - `view-state-change` is debounced (150 ms), bubbles and is composed.
816
+
817
+ ---
818
+
771
819
  ## CSS variables
772
820
 
773
821
  | Variable | Default | Description |
package/llms.txt CHANGED
@@ -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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
3
  "type": "module",
4
- "version": "0.15.5",
4
+ "version": "0.16.1",
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "description": "Web components for ixfx — Lit-based UI toolkit",
7
7
  "author": "Clint Heyer",
@@ -247,7 +247,7 @@
247
247
  "lit": { "optional": false }
248
248
  },
249
249
  "dependencies": {
250
- "@clinth/ui-commands": "1.1.0",
250
+ "@clinth/ui-commands": "1.1.1",
251
251
  "@clinth/vcanvas": "0.0.5",
252
252
  "@ixfx/arrays": "0.56.14",
253
253
  "@ixfx/collections": "0.56.14",