@ixfx/components 0.16.3 → 0.16.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.
Files changed (39) hide show
  1. package/bundle/index.d.ts +2 -2
  2. package/bundle/index.js +5 -5
  3. package/dist/{index-DyaHA9Kk.d.ts → index-YCDugjdu.d.ts} +2 -2
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/{miller-Be7x4ufN.js → miller-BrmBywNZ.js} +17 -7
  7. package/dist/miller.d.ts +1 -1
  8. package/dist/miller.js +1 -1
  9. package/docs-user/README.md +5 -0
  10. package/docs-user/bottom-drawer.md +2 -2
  11. package/docs-user/crumbs.md +1 -1
  12. package/docs-user/data-grid.md +2 -2
  13. package/docs-user/detail-list.md +3 -3
  14. package/docs-user/dock.md +3 -3
  15. package/docs-user/flat-list.md +7 -7
  16. package/docs-user/grid-list.md +3 -3
  17. package/docs-user/hyperbolic-tree.md +6 -6
  18. package/docs-user/icicle-tree.md +2 -2
  19. package/docs-user/incr-search.md +1 -1
  20. package/docs-user/index.json +22 -2
  21. package/docs-user/infra-handle.md +122 -0
  22. package/docs-user/infra-incremental.md +154 -0
  23. package/docs-user/infra-theming.md +510 -0
  24. package/docs-user/infra-view-state.md +210 -0
  25. package/docs-user/list-state.md +656 -0
  26. package/docs-user/llms.txt +5 -0
  27. package/docs-user/miller.md +2 -2
  28. package/docs-user/panel.md +3 -3
  29. package/docs-user/split-layout.md +2 -2
  30. package/docs-user/sunburst-tree.md +3 -3
  31. package/docs-user/titlebar.md +2 -2
  32. package/docs-user/tree.md +2 -2
  33. package/docs-user/treemap.md +2 -2
  34. package/docs-user/twosplit-layout.md +3 -3
  35. package/docs-user/user-selection-intents.md +3 -3
  36. package/docs-user/user-theming.md +1 -1
  37. package/docs-user/vertical-list.md +2 -2
  38. package/llms.txt +5 -0
  39. package/package.json +1 -1
@@ -0,0 +1,210 @@
1
+ # View state
2
+
3
+ A common API for snapshotting and restoring *how* a component is showing its
4
+ data — selection, expansion, scroll, column sizes, panel layout — separately
5
+ from the data itself.
6
+
7
+ - The component **never persists** anything. It hands out plain JSON and takes
8
+ it back; where it is stored is the host's business (`bindViewState()` makes
9
+ that a two-liner).
10
+ - State is **keyed**, never positional: item keys, column ids, group keys,
11
+ panel ids. That is what lets it be re-applied to data that was rebuilt,
12
+ re-sorted or streamed in.
13
+ - State for keys the data doesn't contain yet is kept **pending** and applied
14
+ when a matching item appears. Set a saved tree state, then build the tree up
15
+ over time: as `A > B > C` arrives, each branch opens.
16
+
17
+ Core lives in `src/common/view-state/` and is exported from the package index.
18
+
19
+ ---
20
+
21
+ ## Using it
22
+
23
+ ```ts
24
+ import { bindViewState } from '@ixfx/components';
25
+
26
+ const stop = bindViewState(tree, {
27
+ load: () => JSON.parse(localStorage.getItem(`files-tree`) ?? `null`),
28
+ save: s => localStorage.setItem(`files-tree`, JSON.stringify(s)),
29
+ });
30
+
31
+ tree.root = await fetchRoot(); // pending state applies as items arrive
32
+ ```
33
+
34
+ `bindViewState` loads once (immediately), saves on every debounced
35
+ `view-state-change` (skipping `restore` echoes) and on `pagehide`. Returns a
36
+ function that stops saving.
37
+
38
+ Without the helper:
39
+
40
+ ```ts
41
+ const saved = el.getViewState(); // plain JSON
42
+ el.setViewState(saved); // any time — before or after data
43
+ el.setViewState({ selected: [`a`] }); // partial: only fields present apply
44
+ el.getViewState({ include: [`columns`] }) // only some fields
45
+ el.clearPendingViewState(); // forget not-yet-matched state
46
+ el.addEventListener(`view-state-change`, e => e.detail /* { state, reason } */);
47
+ ```
48
+
49
+ ---
50
+
51
+ ## API (every view-stateful component)
52
+
53
+ | Member | Description |
54
+ |---|---|
55
+ | `getViewState(options?)` | Snapshot as plain JSON. `include` limits fields; `includePending` (default **true**) adds state for keys not currently present. |
56
+ | `setViewState(state)` | Apply a partial state. Fields present **replace** the current value; absent fields are untouched. Keys not in the data go pending. A state whose `kind` or `v` doesn't match is ignored with a warning. |
57
+ | `clearPendingViewState()` | Drop pending (unmatched) state. |
58
+ | `view-state-change` event | Debounced (150 ms trailing), `bubbles`, **not** `composed` — events from view-stateful children inside a component's shadow root stay inside it. `detail: { state, reason }`. Flushed immediately on disconnect. |
59
+
60
+ `reason` is the strongest cause since the last event: `user` (interaction) >
61
+ `data` (component adjusted to a data change) > `restore` (pending state applied,
62
+ or `setViewState()`).
63
+
64
+ ### Shared fields (`ViewStateBase`)
65
+
66
+ | Field | Type | Meaning |
67
+ |---|---|---|
68
+ | `v` | `1` | Schema version. |
69
+ | `kind` | `string` | Component kind, e.g. `tree-list`. |
70
+ | `selected` | `string[]` | Selected item keys. In `checked` interaction modes this is the checked set. |
71
+ | `focus` | `string` | Keyboard cursor key (only recorded when the cursor was moved by keyboard). |
72
+ | `scroll` | `{ top, left?, anchor?: { key, offset } }` | `anchor` = first visible item's key + px offset into it. Preferred over `top` because it survives inserts above. |
73
+ | `expanded` | `Record<string, boolean>` | Explicit open / closed choices for branches or groups. A missing key means "component default", so it works for things that default open (groups) and closed (tree branches). |
74
+
75
+ Components add their own fields (see the table at the end).
76
+
77
+ ---
78
+
79
+ ## Semantics
80
+
81
+ **Pending state.** Every keyed piece of state is either *live* (its key exists
82
+ in the data) or *pending*. On every data change a component:
83
+
84
+ 1. moves live state whose key has gone to pending (so a data reset does not lose it), and
85
+ 2. applies pending state whose key now exists.
86
+
87
+ Pending sets are capped at 2000 keys per facet (oldest dropped).
88
+
89
+ **What supersedes pending state:**
90
+
91
+ | Facet | Pending is dropped when… |
92
+ |---|---|
93
+ | selection | any explicit selection change (click, key, `select*()`, `clearSelection()`). Selection is all-or-nothing, so an old saved selection must never merge into a new one. |
94
+ | expansion | never implicitly — tracked per key. Collapsing `A` doesn't forget that `C` (below it) was open. |
95
+ | scroll | the user scrolls / navigates (wheel, touch, pointer, nav key), or the position is reached. |
96
+ | focus | the cursor is moved. |
97
+ | anything | `setViewState()` with that field, or `clearPendingViewState()`. |
98
+
99
+ **Restored selection fires the normal selection event** (with
100
+ `origin: 'programmatic'` where the event has one). A key that *leaves* the data
101
+ is deselected silently.
102
+
103
+ **Lazy trees.** With `restoreLoads` (default `true`), a saved-open branch whose
104
+ children aren't loaded is opened via `loadChildren` / `model`, one load at a
105
+ time, so a saved deep path cascades open. Set `restoreLoads = false` to only
106
+ re-open branches as the host loads them.
107
+
108
+ **Save while restoring.** `getViewState()` includes pending state by default,
109
+ and the pending scroll (not the current one) while a scroll restore is in
110
+ flight. Without this, a save during a streamed rebuild would overwrite the
111
+ stored state with a partial one.
112
+
113
+ ---
114
+
115
+ ## Adding view state to a component
116
+
117
+ 1. **Types.** Declare `XxxViewState = ViewStateBase & { …own fields }` in the
118
+ component's `types.ts` (or a `*-view-state.ts`), JSON-only (arrays /
119
+ records, no Sets / Maps / class instances). Export it.
120
+
121
+ 2. **Controller.** One field:
122
+
123
+ ```ts
124
+ #viewState = new VS.ViewStateController<XxxViewState>(this, {
125
+ kind: `xxx`,
126
+ capture: () => this.getViewState(),
127
+ });
128
+ ```
129
+
130
+ `import * as VS from '../common/view-state/index.js';`
131
+
132
+ 3. **Pending facets.** Hold pending state next to live state, using the pure
133
+ helpers in `pending.ts` (`partitionKeys`, `partitionFlags`, `capKeys`,
134
+ `flagsToRecord`, `keysToArray`…). Put reconcile logic in a pure function in
135
+ its own module (`tree/tree-view-state.ts` is the reference) — the big
136
+ components are already over the 1000-line budget.
137
+ - Element lists: `ListSelectionController` already has pending keys —
138
+ `restoreKeys()`, `resolvePending()`, `demote()`, `pendingKeys`,
139
+ `clearPending()`. `reset()` demotes rather than forgets. Call
140
+ `resolvePending()` after items are added / rebuilt, and `demote()` where
141
+ removed items are pruned from the selection.
142
+
143
+ 4. **Reconcile on data change.** Wherever the component learns its data
144
+ changed (new `items` / `root`, added children, provider batch, lazy load),
145
+ run reconcile inside `#viewState.restoring(() => …)`.
146
+
147
+ 5. **Notify.** Call `#viewState.notify()` wherever user-visible state changes
148
+ (selection dispatch, expand/collapse, column resize end, sort, scroll). It
149
+ is cheap and debounced; inside `restoring()` it reports `restore`.
150
+
151
+ 6. **Scroll.** After render: `observeScrollTarget(viewport)` once per viewport,
152
+ call `notifyScroll()` from the viewport's `scroll` listener (it ignores
153
+ scrolls while a restore is pending and labels the restore's own scroll),
154
+ then `applyPendingScroll(s => …)` — return `true` only when the position was
155
+ fully reached (anchor key rendered *and* content tall enough), so it is
156
+ retried after the next batch. Use `captureElementScroll` /
157
+ `restoreElementScroll` for DOM lists, `anchorFromRow` + `scrollToOffset`
158
+ for virtualised lists with arithmetic geometry.
159
+
160
+ 7. **Public API.** `getViewState(options)` builds the fields and passes them
161
+ through `#viewState.shape(fields, options)`. `setViewState(state)` starts
162
+ with `if (!this.#viewState.accepts(state)) return;`, then replaces each
163
+ present field's pending state inside `restoring()` and reconciles.
164
+ `clearPendingViewState()` empties pending facets and `pendingScroll`.
165
+
166
+ 8. **Tests.** At minimum: restore before data; restore as data streams in;
167
+ data reset keeps state; explicit selection drops pending selection;
168
+ `getViewState()` includes pending; round-trip through `JSON.stringify`;
169
+ wrong `kind` ignored.
170
+
171
+ ---
172
+
173
+ ## Component fields
174
+
175
+ | Component | `kind` | Fields |
176
+ |---|---|---|
177
+ | `ixfx-tree-list` | `tree-list` | `selected`, `expanded`, `focus`, `scroll` |
178
+ | `ixfx-vertical-list` | `vertical-list` | `selected`, `focus`, `scroll` |
179
+ | `ixfx-detail-list` | `detail-list` | `selected`, `expanded` (groups), `focus`, `scroll` |
180
+ | `ixfx-grid-list` | `grid-list` | `selected`, `expanded` (groups), `focus`, `scroll` |
181
+ | `ixfx-data-grid` | `data-grid` | `selected`, `expanded` (groups), `focus`, `scroll`, `columns`, `sort`, `groupBy` |
182
+ | `ixfx-multicol-list` | `multicol-list` | `columns`, `selectedByColumn`, `scrollByColumn` |
183
+ | `ixfx-miller-list` | `miller-list` | `selected`, `focus`, `path`, `columnWidths`, `columnScroll` |
184
+ | `ixfx-crumb-navigation` | `crumb-navigation` | `path` |
185
+ | `ixfx-flat-list` | `flat-list` | `path`, `selected`, `focus`, `scroll` (delegated to the inner list) |
186
+ | `ixfx-sunbursttree-list` | `sunbursttree-list` | `selected`, `focus`, `zoom` (key path) |
187
+ | `ixfx-icicletree-list` | `icicletree-list` | `selected`, `focus`, `zoom` (key path), `scroll` |
188
+ | `ixfx-treemap-list` | `treemap-list` | `selected`, `focus`, `zoom` (key path) |
189
+ | `ixfx-hyperbolictree-list` | `hyperbolictree-list` | `selected`, `focus`, `center` (key path), `zoom` (number) |
190
+ | `ixfx-split-layout` | `split-layout` | `layout` (by child `id`) |
191
+ | `ixfx-twosplit-layout` | `twosplit-layout` | `layout` (`a` / `b`) |
192
+ | `ixfx-dock-host` | `dock-host` | `layout` (`DockLayout`) |
193
+
194
+ User-facing guide and migration notes: [`list-state.md`](./list-state.md).
195
+
196
+ ### Shared helpers
197
+
198
+ - `src/common/view-state/pending.ts` — pending key / flag sets.
199
+ - `src/common/view-state/tree.ts` — `reconcileSelection`, `reconcileExpansion`,
200
+ `reconcileCursor`, `resolveKeyPath`, `pathFromJson` for tree-like components.
201
+ Not exported from the package index.
202
+ - `ListSelectionController` (`src/interaction/`) — pending selection for element lists.
203
+
204
+ ### Known gaps
205
+
206
+ - Components that hold a pending cursor / scroll anchor on a hidden item
207
+ (filtered, collapsed group) mostly retry only when items are added.
208
+ - vertical-list, detail-list and data-grid report the scroll caused by their own
209
+ restore as `user`; move them to `notifyScroll()`.
210
+ - `ixfx-panel-group` has no view state (its panels have no stable key).