@ixfx/components 0.16.2 → 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.
- package/bundle/index.d.ts +61 -7
- package/bundle/index.js +151 -143
- package/dist/ac-text.d.ts +1 -1
- package/dist/{ac-token-Cq7lsYf-.d.ts → ac-token-qEs3FsQv.d.ts} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/ac-token.js +1 -1
- package/dist/{button-D1sPDUCM.d.ts → button-N92DKoWb.d.ts} +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/{crumb-navigation-nnr881n-.d.ts → crumb-navigation-BbqfmkYs.d.ts} +1 -1
- package/dist/crumbs.d.ts +1 -1
- package/dist/{data-grid-CaPLWt5E.js → data-grid-8TsiF2ES.js} +7 -6
- package/dist/{data-grid-BAEpfTkb.d.ts → data-grid-CslzF16R.d.ts} +4 -4
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-DK49TsJd.d.ts → detail-list-n1MbNbhF.d.ts} +4 -4
- package/dist/{detail-list-C3XKTRmE.js → detail-list-w3GXKO2k.js} +4 -3
- package/dist/detail-list.d.ts +2 -2
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-Bv4-qfjp.d.ts → flat-list-D_4y3wgn.d.ts} +4 -4
- package/dist/flat-list.d.ts +1 -1
- package/dist/flat-list.js +2 -2
- package/dist/{grid-list-DaeSHv9P.d.ts → grid-list-CqYS28NY.d.ts} +4 -2
- package/dist/{grid-list-51XsR2X2.js → grid-list-Cz2tKcNZ.js} +4 -3
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-DZIjo9nO.d.ts → grouped-item-lister-CS4yInuw.d.ts} +2 -2
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +1 -1
- package/dist/icons.d.ts +2 -2
- package/dist/{incr-search-2YaxUUA9.js → incr-search-Dc6gYpJG.js} +24 -4
- package/dist/incr-search.d.ts +1 -1
- package/dist/incr-search.js +1 -1
- package/dist/{index-BjRoPrwj.d.ts → index-Bm9a9S3_.d.ts} +3 -2
- package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +1 -1
- package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +1 -1
- package/dist/{index-Bz3loIAf.d.ts → index-DN2BWctP.d.ts} +1 -1
- package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +1 -1
- package/dist/{index-vdbbGE7N.d.ts → index-VAs9akt4.d.ts} +18 -2
- package/dist/{index-DXDz60XH.d.ts → index-YCDugjdu.d.ts} +4 -4
- package/dist/{index-DM9OLgTH.d.ts → index-ZaA90u09.d.ts} +2 -2
- package/dist/index.d.ts +37 -26
- package/dist/index.js +88 -13
- package/dist/item-appearance-BjcMyauw.js +29 -0
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +1 -1
- package/dist/labelled-radial-input.d.ts +2 -2
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-slider-input.d.ts +1 -1
- package/dist/list-group-header.d.ts +1 -1
- package/dist/{list-selection-controller-CJDdjH7E.js → list-selection-controller-D4iIrvJH.js} +12 -8
- package/dist/{list-selection-types-hj9Njei3.d.ts → list-selection-types-Dnamh2Lu.d.ts} +29 -3
- package/dist/{menu-item-B0zqw2xp.d.ts → menu-item-bF6narIz.d.ts} +1 -1
- package/dist/menu.d.ts +2 -2
- package/dist/{miller-BfrTcujx.js → miller-BrmBywNZ.js} +20 -9
- package/dist/miller.d.ts +1 -1
- package/dist/miller.js +1 -1
- package/dist/narrowed-text.js +1 -1
- package/dist/panel.d.ts +1 -1
- package/dist/{item-appearance-Bl6rMIID.js → partial-keys-Ck6CiprB.js} +1 -29
- package/dist/{split-layout-ChZ0Glrs.d.ts → split-layout-CophkNeU.d.ts} +1 -1
- package/dist/split-layout.d.ts +1 -1
- package/dist/sunburst-tree.d.ts +1 -1
- package/dist/sunburst-tree.js +1 -1
- package/dist/{sunbursttree-list-BAS0KrDG.d.ts → sunbursttree-list-Bk20giTF.d.ts} +1 -1
- package/dist/{sunbursttree-list-906G33ME.js → sunbursttree-list-DQF6DcsS.js} +1 -1
- package/dist/{tab-list-wC0xNz1b.d.ts → tab-list-Df371rqw.d.ts} +1 -1
- package/dist/tabs.d.ts +1 -1
- package/dist/timeline.d.ts +1 -1
- package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/{tree-C_H1COTf.js → tree-BaXYyt5U.js} +185 -69
- package/dist/tree.d.ts +1 -1
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-od4050Bo.d.ts → vertical-list-DtHX26JO.d.ts} +2 -2
- package/dist/{vertical-list-qMRIxZIT.js → vertical-list-o5hfqH87.js} +4 -3
- package/dist/vertical-list.d.ts +2 -2
- package/dist/vertical-list.js +1 -1
- package/docs-user/README.md +6 -0
- package/docs-user/bottom-drawer.md +2 -2
- package/docs-user/crumbs.md +1 -1
- package/docs-user/data-grid.md +3 -3
- package/docs-user/detail-list.md +3 -3
- package/docs-user/dock.md +3 -3
- package/docs-user/flat-list.md +7 -7
- package/docs-user/grid-list.md +4 -4
- package/docs-user/hyperbolic-tree.md +6 -6
- package/docs-user/icicle-tree.md +2 -2
- package/docs-user/incr-search.md +1 -1
- package/docs-user/index.json +26 -2
- package/docs-user/infra-handle.md +122 -0
- package/docs-user/infra-incremental.md +154 -0
- package/docs-user/infra-theming.md +510 -0
- package/docs-user/infra-view-state.md +210 -0
- package/docs-user/list-state.md +656 -0
- package/docs-user/llms.txt +6 -0
- package/docs-user/miller.md +5 -1
- package/docs-user/multicol-list.md +6 -0
- package/docs-user/panel.md +3 -3
- package/docs-user/select-horiz.md +1 -1
- package/docs-user/split-layout.md +2 -2
- package/docs-user/sunburst-tree.md +3 -3
- package/docs-user/titlebar.md +2 -2
- package/docs-user/tree.md +8 -2
- package/docs-user/treemap.md +2 -2
- package/docs-user/twosplit-layout.md +3 -3
- package/docs-user/user-selection-intents.md +86 -0
- package/docs-user/user-theming.md +1 -1
- package/docs-user/vertical-list.md +3 -3
- package/llms.txt +6 -0
- package/package.json +1 -1
- /package/dist/{index-DV3iaUBW.d.ts → index-DV3iaUBW2.d.ts} +0 -0
|
@@ -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).
|