vlist 3.0.0-next.2 → 3.0.0-next.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 (72) hide show
  1. package/README.github.md +88 -39
  2. package/README.md +44 -27
  3. package/dist/config.d.ts +129 -12
  4. package/dist/config.js +1 -1
  5. package/dist/core/create.d.ts +22 -4
  6. package/dist/core/dom.d.ts +22 -2
  7. package/dist/core/fold.d.ts +52 -0
  8. package/dist/core/hooks.d.ts +1 -1
  9. package/dist/core/index.d.ts +1 -1
  10. package/dist/core/pipeline.d.ts +1 -1
  11. package/dist/core/pool.d.ts +9 -2
  12. package/dist/core/runway.d.ts +29 -3
  13. package/dist/core/scroll.d.ts +1 -1
  14. package/dist/core/sizes.d.ts +1 -1
  15. package/dist/core/state.d.ts +13 -1
  16. package/dist/core/types.d.ts +151 -61
  17. package/dist/core/velocity.d.ts +1 -1
  18. package/dist/index.d.ts +17 -2
  19. package/dist/index.js +1 -1
  20. package/dist/internals.d.ts +0 -1
  21. package/dist/internals.js +1 -1
  22. package/dist/plugins/a11y/index.d.ts +1 -0
  23. package/dist/plugins/a11y/plugin.d.ts +1 -1
  24. package/dist/plugins/autosize/index.d.ts +2 -1
  25. package/dist/plugins/autosize/plugin.d.ts +13 -2
  26. package/dist/plugins/carousel/engine.d.ts +1 -1
  27. package/dist/plugins/carousel/index.d.ts +1 -0
  28. package/dist/plugins/carousel/plugin.d.ts +42 -6
  29. package/dist/plugins/carousel/presets.d.ts +1 -1
  30. package/dist/plugins/data/index.d.ts +1 -0
  31. package/dist/plugins/data/plugin.d.ts +15 -2
  32. package/dist/plugins/grid/index.d.ts +2 -1
  33. package/dist/plugins/grid/plugin.d.ts +14 -2
  34. package/dist/plugins/groups/async-bridge.d.ts +6 -6
  35. package/dist/plugins/groups/index.d.ts +1 -0
  36. package/dist/plugins/groups/layout.d.ts +2 -2
  37. package/dist/plugins/groups/plugin.d.ts +12 -4
  38. package/dist/plugins/groups/sticky.d.ts +2 -1
  39. package/dist/plugins/groups/types.d.ts +8 -3
  40. package/dist/plugins/masonry/index.d.ts +2 -1
  41. package/dist/plugins/masonry/plugin.d.ts +10 -2
  42. package/dist/plugins/masonry/renderer.d.ts +5 -1
  43. package/dist/plugins/page/plugin.d.ts +1 -1
  44. package/dist/plugins/scrollbar/index.d.ts +2 -1
  45. package/dist/plugins/scrollbar/plugin.d.ts +7 -2
  46. package/dist/plugins/search/index.d.ts +1 -0
  47. package/dist/plugins/search/plugin.d.ts +29 -1
  48. package/dist/plugins/selection/index.d.ts +2 -1
  49. package/dist/plugins/selection/plugin.d.ts +23 -2
  50. package/dist/plugins/snapshots/index.d.ts +1 -0
  51. package/dist/plugins/snapshots/plugin.d.ts +9 -2
  52. package/dist/plugins/sortable/index.d.ts +1 -0
  53. package/dist/plugins/sortable/plugin.d.ts +14 -3
  54. package/dist/plugins/table/index.d.ts +1 -0
  55. package/dist/plugins/table/plugin.d.ts +19 -3
  56. package/dist/plugins/transition/plugin.d.ts +1 -1
  57. package/dist/plugins/tree/index.d.ts +1 -0
  58. package/dist/plugins/tree/layout.d.ts +3 -1
  59. package/dist/plugins/tree/plugin.d.ts +37 -3
  60. package/dist/plugins/tree/types.d.ts +1 -1
  61. package/dist/rendering/sizes.d.ts +19 -1
  62. package/dist/size.json +1 -1
  63. package/dist/synthetic.d.ts +2 -2
  64. package/dist/synthetic.js +1 -1
  65. package/dist/types.d.ts +14 -21
  66. package/dist/utils/apply-scroll-behavior.d.ts +15 -0
  67. package/dist/utils/scroll-padding.d.ts +36 -0
  68. package/dist/utils/stats.d.ts +9 -1
  69. package/dist/vlist-tree.css +1 -1
  70. package/dist/vlist.css +1 -1
  71. package/package.json +6 -2
  72. package/dist/plugins/grid/renderer.d.ts +0 -68
package/README.github.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins.
4
4
 
5
- **v3.0.0-next.2** (prerelease on the npm `next` tag) — Native scrolling by default, opt-in `vlist/synthetic`, and removal of deprecated scroll configuration and plugin hooks. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
5
+ **v3.0.0-next.4** (prerelease on the npm `next` tag) — Native scrolling by default, opt-in `vlist/synthetic`, and removal of deprecated scroll configuration and plugin hooks. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
8
  [![bundle size](https://img.shields.io/bundlephobia/minzip/vlist)](https://bundlephobia.com/package/vlist)
9
9
  [![CI](https://github.com/floor/vlist/actions/workflows/ci.yml/badge.svg)](https://github.com/floor/vlist/actions/workflows/ci.yml)
10
10
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
11
11
 
12
- - **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
12
+ - **Accessible** — `a11y()` or a selection mode adds WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
13
  - **Zero dependencies** — framework-agnostic core with tiny adapters for Vue, Svelte, Solid, React
14
- - **9.3 KB gzipped (3.0 prerelease)** — composable plugins with perfect tree-shaking
14
+ - **10.0 KB gzipped (3.0 prerelease)** — composable plugins with perfect tree-shaking
15
15
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
16
16
  - **Tree, grid, masonry, carousel, table, groups, data, selection, search, sortable, transition** — all opt-in
17
17
  - **Axis-neutral** — vertical and horizontal scrolling through a single code path, all plugins work in both orientations
@@ -22,7 +22,7 @@ The virtual list library for every framework. Ultra efficient, batteries-include
22
22
 
23
23
  | | vlist | TanStack Virtual | react-virtuoso | virtua | vue-virtual-scroller |
24
24
  |---|---|---|---|---|---|
25
- | **A11y built-in** | WAI-ARIA + 2D keyboard | None (DIY) | Partial | Minimal | None |
25
+ | **A11y** | WAI-ARIA + 2D keyboard, one plugin | None (DIY) | Partial | Minimal | None |
26
26
  | **Grid + Masonry + Table** | All | Grid only | Grid + Table | Grid only | None |
27
27
  | **Vue** | 0.6 KB adapter | Yes | — | Yes | 11.8 KB |
28
28
  | **Svelte** | 0.5 KB adapter | Yes | — | Yes | — |
@@ -108,7 +108,7 @@ const list = createVList({
108
108
 
109
109
  ## Scroll input
110
110
 
111
- Native scrolling is the default: import `createVList` and plugins from `vlist`. This preserves browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. Carousel, sortable and horizontal RTL lists work with this factory. Carousel supplies its private wrap runway only when the plugin is imported; bounded scrolling is no longer a public mode.
111
+ Native scrolling is the default: import `createVList` and plugins from `vlist`. This preserves browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. Carousel and sortable work with this factory; horizontal RTL lists are rejected by both entries. Carousel supplies its private wrap runway only when the plugin is imported; bounded scrolling is no longer a public mode.
112
112
 
113
113
  | Entry | Input model | Use |
114
114
  |---|---|---|
@@ -134,9 +134,11 @@ const list = createVList({
134
134
 
135
135
  `page()` uses native document scrolling through an external source with either entry. Its content must fit the 16,777,216 px document element limit; creation throws above that limit when the size is known, and later growth warns once. A deferred custom renderer whose size is first committed during rendering also warns once. Use `vlist/synthetic` viewport scrolling for larger lists. Default native viewport lists warn once when content exceeds their browser-size safety limit, pointing to that entry.
136
136
 
137
+ `carousel()` works with either entry. Synthetic carousel folds preserve touch motion and smooth snapping across whole laps. `page()` and carousel remain incompatible. The shared carousel plugin includes its native wrap handler even when used with synthetic input; the combined size is reported below.
138
+
137
139
  Synthetic input limitations:
138
140
 
139
- - `carousel()`, `sortable()` and horizontal RTL lists throw with instructions to use `vlist`. Vertical lists and tables support `dir="rtl"`, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
141
+ - Horizontal RTL lists throw at creation — in this entry and in `vlist`. Vertical lists and tables support `dir="rtl"` in both, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
140
142
  - Same-axis touch stops at either boundary with no parent handoff, including gestures starting inside an edge-pinned list. Use the native default when boundary gestures must scroll the parent page.
141
143
  - There is no native main-axis scrollbar. Add `scrollbar()` for an accessible custom scrollbar. The synthetic entry rejects the `"native"` and `"none"` scrollbar strings.
142
144
  - Inertia initializes its frame clock on the first frame after release, adding up to one frame of release latency.
@@ -144,6 +146,19 @@ Synthetic input limitations:
144
146
 
145
147
  Measurement corrections from autosize preserve ongoing synthetic motion. Existing plugin conflicts still apply. See the [scroll input contract](https://vlist.io/docs/rfcs/RFC-014-Scroll-Input-Model).
146
148
 
149
+ ### Sortable gestures
150
+
151
+ `sortable()` works with either input entry. Mouse and trackpad dragging still starts after `dragThreshold` (5 px by default). Touch and pen without a handle use `touchDelay`, a 350 ms long press; moving at least `dragThreshold` pixels in any direction before it completes yields to scrolling. This delay separates a deliberate hold from a quick flick. With `handle`, dragging starts at the threshold without waiting; touches elsewhere scroll.
152
+
153
+ ```typescript
154
+ sortable({ touchDelay: 350, dragThreshold: 5 })
155
+ sortable({ handle: '.drag-handle' })
156
+ ```
157
+
158
+ A touch during scrolling catches the motion and does not arm a hold for that contact. Lift and press again once the list is at rest. A second finger or `pointercancel` cancels the pending press or active drag. Edge auto-scroll uses the list's logical position and stops on drop.
159
+
160
+ The touch-only `.vlist-item--touch-sort` class suppresses selection and callouts on the pressed item. A claimed touch/pen drag adds `.vlist-sort-ghost--touch` to the ghost as a visual cue; override that class in CSS to customize it. Keyboard reordering remains Space to grab/drop, arrows to move and Escape to cancel.
161
+
147
162
  ## Migrating to 3.0
148
163
 
149
164
  `3.0.0-next.1` used synthetic input by default. `3.0.0-next.2` restores the 2.x default. Import `vlist/synthetic` explicitly to retain synthetic behavior across these prereleases. `vlist/native` remains a deprecated compatibility alias; its `NativeScrollConfig` and `NativeCreateVListConfig` types alias the standard `ScrollConfig` and `CreateVListConfig`.
@@ -153,36 +168,39 @@ Measurement corrections from autosize preserve ongoing synthetic motion. Existin
153
168
  | `scroll.mode` (all values, both entries) | Omit it. `vlist` provides native scrolling; import `vlist/synthetic` for synthetic input. |
154
169
  | `scroll.runway` (both entries) | Remove it. For huge lists use `vlist/synthetic`, which needs no native runway. |
155
170
  | `scroll.scrollbar: "native"` or `"none"` | Retained in default `ScrollConfig`. With synthetic input, omit the string and optionally install `scrollbar()`. |
156
- | `PluginContext.setScrollFns`, `disableDefaultScroll` | Use `setScrollSource` to supply an external position source and commit callback. |
171
+ | `PluginContext.setScrollFns`, `disableDefaultScroll` | Use `ctx.scroll.setSource` to supply an external position source and commit callback. |
172
+ | Flat `PluginContext` members (`registerMethod`, `forceRender`, `sizeCache`, `scrollTo`, …) | Grouped by capability: `ctx.hooks.method`, `ctx.render.force`, `ctx.sizes.cache`, `ctx.scroll.to`, `ctx.items.all`. `pool`, `config`, `emitter`, `template` and `getState()` stay at the top level. |
157
173
  | `scale()` and `ScalePluginConfig` | Remove them; use `vlist/synthetic` for the full logical range. Config no longer installs a scale stub. |
158
174
 
159
- Removed mode/runway options throw a migration error before creating DOM. `vlist/config` retains scrollbar omission/options convenience and its top-level `scrollbar: "none"` option. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers now supply their handler factory as the second argument of `ctx.setBoundedWrap`.
175
+ Removed mode/runway options throw a migration error before creating DOM. `vlist/config` wires only what the config asks for: `selection`, the custom scrollbar, `snapshots` and `a11y` are each opt-in, so an adapter list matches a core list built from the same options. It keeps the scrollbar options convenience and its top-level `scrollbar: "none"`; pass `scrollbar: true` for the overlay scrollbar earlier versions added to every list. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers now supply their handler factory as the second argument of `ctx.scroll.setBoundedWrap`.
160
176
 
161
177
  ## Plugins
162
178
 
163
179
  | Entry / export | Minified | Gzipped |
164
180
  |---|---:|---:|
165
- | **Base (`vlist`)** | 25.4 KB | 9.3 KB |
166
- | `vlist/synthetic` | 31.1 KB | 11.4 KB |
167
- | `vlist/native` (alias) | 25.4 KB | 9.3 KB |
168
- | `a11y()` | 28.7 KB | 10.5 KB |
169
- | `selection()` | 34.8 KB | 12.1 KB |
170
- | `data()` | 39.0 KB | 14.1 KB |
171
- | `scrollbar()` | 33.6 KB | 12.2 KB |
172
- | `sortable()` | 34.9 KB | 12.2 KB |
173
- | `groups()` | 41.3 KB | 14.6 KB |
174
- | `page()` | 27.9 KB | 10.2 KB |
175
- | `snapshots()` | 28.7 KB | 10.4 KB |
176
- | `transition()` | 32.1 KB | 11.3 KB |
177
- | `autosize()` | 28.5 KB | 10.3 KB |
178
- | `grid()` | 32.5 KB | 11.8 KB |
179
- | `table()` | 43.8 KB | 15.2 KB |
180
- | `masonry()` | 36.8 KB | 13.4 KB |
181
- | `tree()` | 40.6 KB | 14.3 KB |
182
- | `search()` | 34.5 KB | 12.4 KB |
183
- | `carousel()` | 38.0 KB | 13.7 KB |
184
-
185
- Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin. The base is **9,523 bytes gzipped**, below the 9.9 KB target; synthetic input is **11,687 bytes** before plugins. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
181
+ | **Base (`vlist`)** | 26.9 KB | 10.0 KB |
182
+ | `vlist/synthetic` | 33.8 KB | 12.6 KB |
183
+ | `vlist/native` (alias) | 26.9 KB | 10.0 KB |
184
+ | `a11y()` | 30.7 KB | 11.3 KB |
185
+ | `selection()` | 36.4 KB | 12.9 KB |
186
+ | `data()` | 40.6 KB | 14.8 KB |
187
+ | `scrollbar()` | 35.1 KB | 12.8 KB |
188
+ | `sortable()` | 38.7 KB | 13.4 KB |
189
+ | `groups()` | 43.0 KB | 15.4 KB |
190
+ | `page()` | 29.5 KB | 10.9 KB |
191
+ | `snapshots()` | 30.2 KB | 11.1 KB |
192
+ | `transition()` | 33.7 KB | 12.0 KB |
193
+ | `autosize()` | 30.1 KB | 11.1 KB |
194
+ | `grid()` | 34.4 KB | 12.5 KB |
195
+ | `table()` | 45.4 KB | 15.9 KB |
196
+ | `masonry()` | 39.0 KB | 14.4 KB |
197
+ | `tree()` | 43.1 KB | 15.3 KB |
198
+ | `search()` | 36.5 KB | 13.3 KB |
199
+ | `carousel()` | 41.7 KB | 15.1 KB |
200
+ | `vlist/synthetic` + `carousel()` | 47.8 KB | 17.4 KB |
201
+ | `vlist/synthetic` + `sortable()` | 45.7 KB | 16.1 KB |
202
+
203
+ Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin. The base is **10,200 bytes gzipped**, within the 10.0 KB budget (10,240 bytes). `bun run size` fails if a scenario fails to build, an unused plugin leaks into a bundle, or any published size exceeds its gzip budget. Synthetic input is **12,889 bytes** before plugins. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
186
204
 
187
205
  ## Examples
188
206
 
@@ -307,6 +325,13 @@ const list = createVList({
307
325
  ])
308
326
  ```
309
327
 
328
+ Search filters client-side over the items the list holds, so it cannot be
329
+ combined with `data()` — creating a list with both throws. With an adapter, the
330
+ rows in memory are only the loaded window; query the remote dataset through the
331
+ adapter instead. It cannot be combined with `tree()` either: filtering a tree
332
+ means keeping the ancestors of each match, which belongs to the plugin that
333
+ owns the layout, and `tree()` offers no such hook.
334
+
310
335
  ## Custom scrollbar (3.0 preview)
311
336
 
312
337
  On `next`, `scrollbar()` remains a plugin. macOS and Android default to thin,
@@ -367,7 +392,9 @@ colors take priority while forced colors are active.
367
392
 
368
393
  ## Accessibility
369
394
 
370
- Every vlist is accessible by default following the [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
395
+ Accessibility is a plugin, not a default. Add `a11y()`, or a `selection()` mode
396
+ other than `"none"` which brings the same behaviour, and the list follows the
397
+ [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
371
398
 
372
399
  - **Arrow keys** move focus between items with a visible focus ring
373
400
  - **2D navigation** in grids and masonry — Up/Down by row, Left/Right by cell
@@ -376,7 +403,9 @@ Every vlist is accessible by default following the [WAI-ARIA listbox pattern](ht
376
403
  - **Screen-reader DOM ordering** — items reordered on scroll idle for correct reading order
377
404
  - **Focus recovery** — maintains focus when items are removed
378
405
 
379
- Set `interactive: false` for display-only lists (log viewers, activity feeds) where items contain their own interactive elements.
406
+ A list created without either stays display-only: `role="list"`, no item roles
407
+ and out of the tab order. That is the right shape for log viewers and activity
408
+ feeds, and for items carrying their own interactive elements.
380
409
 
381
410
  ## API
382
411
 
@@ -390,7 +419,7 @@ const list = createVList(config, [plugin1(), plugin2()])
390
419
  |--------|-------------|
391
420
  | `list.setItems(items)` | Replace all items |
392
421
  | `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
393
- | `list.prependItems(items)` | Add to start (preserves scroll position) |
422
+ | `list.prependItems(items)` | Add to start (in reverse mode, the visible rows hold their position) |
394
423
  | `list.updateItem(id, partial)` | Update a single item by ID |
395
424
  | `list.insertItem(item, index?)` | Insert at index (animated with `transition`) |
396
425
  | `list.removeItem(id)` | Remove by ID (animated with `transition`) |
@@ -410,12 +439,14 @@ const list = createVList(config, [plugin1(), plugin2()])
410
439
 
411
440
  | Method | Description |
412
441
  |--------|-------------|
413
- | `list.select(...ids)` | Select item(s) |
442
+ | `list.select(...ids)` | Select item(s) without moving the viewport |
414
443
  | `list.deselect(...ids)` | Deselect item(s) |
415
444
  | `list.toggleSelect(id)` | Toggle |
416
445
  | `list.selectAll()` / `list.clearSelection()` | Bulk operations |
417
446
  | `list.getSelected()` | Array of selected IDs |
418
447
  | `list.getSelectedItems()` | Array of selected items |
448
+ | `list.selectNext()` | Select the next item and scroll it into view |
449
+ | `list.selectPrevious()` | Select the previous item and scroll it into view |
419
450
 
420
451
  ### Events
421
452
 
@@ -434,6 +465,10 @@ list.on('sort:end', ({ fromIndex, toIndex }) => {})
434
465
  list.on('sort:cancel', ({ originalItems }) => {})
435
466
  ```
436
467
 
468
+ The `index` on `item:click`, `item:dblclick` and `item:contextmenu` is the data
469
+ index — the one `getItemAt`, `scrollToIndex` and `removeItem` take — not the
470
+ layout index, which counts group headers and carousel laps.
471
+
437
472
  ### Properties
438
473
 
439
474
  | Property | Description |
@@ -481,8 +516,7 @@ Full configuration reference → **[vlist.io](https://vlist.io)**
481
516
  | `ariaLabel` | — | Accessible label for the listbox |
482
517
  | `orientation` | `'vertical'` | `'vertical'` or `'horizontal'` scroll direction |
483
518
  | `padding` | `0` | Content inset — number, `[v, h]`, or `[top, right, bottom, left]` |
484
- | `interactive` | `true` | Enable built-in keyboard navigation |
485
- | `reverse` | `false` | Reverse mode for chat UIs |
519
+ | `reverse` | `false` | The list reads bottom-up (chat UIs): a view sitting at the end stays there as `appendItems` adds to it, `prependItems` holds the visible rows in place as older history loads above them, arrow keys invert, and `transition()` animates from the bottom. Item order is yours to supply; the layout itself is not reversed. `masonry()` and `table()` reject it |
486
520
 
487
521
  ## Styling
488
522
 
@@ -512,23 +546,38 @@ Live benchmarks against 9 competitors → **[vlist.io/benchmarks](https://vlist.
512
546
 
513
547
  ## TypeScript
514
548
 
515
- Fully typed. Generic over your item type:
549
+ Fully typed and generic over your item type. An item only needs an `id`, so ordinary
550
+ interfaces work:
516
551
 
517
552
  ```typescript
518
- import { createVList, grid, type VList } from 'vlist'
553
+ import { createVList, grid, selection, type CreateVListConfig } from 'vlist'
519
554
 
520
555
  interface Photo { id: number; url: string; title: string }
521
556
 
522
- const list: VList<Photo> = createVList<Photo>({
557
+ const config: CreateVListConfig<Photo> = {
523
558
  container: '#gallery',
524
559
  items: photos,
525
560
  item: {
526
561
  height: 200,
527
562
  template: (photo) => `<img src="${photo.url}" />`,
528
563
  },
529
- }, [grid({ columns: 4 })])
564
+ }
565
+
566
+ const list = createVList(config, [grid({ columns: 4 }), selection<Photo>({ mode: 'multiple' })])
567
+
568
+ list.select(photos[0].id) // typed: selection is in the array
569
+ const chosen: Photo[] = list.getSelectedItems()
570
+ // list.expand(1) // compile error: no tree plugin here
530
571
  ```
531
572
 
573
+ `createVList` infers the methods each plugin adds, so a list exposes exactly what its
574
+ plugins provide, and a mistyped call does not compile.
575
+
576
+ TypeScript does not allow a partial type argument list. Writing
577
+ `createVList<Photo>(config, plugins)` supplies the item type and leaves the plugin list on
578
+ its default, so plugin methods are not inferred. Type the config as above, or let the item
579
+ type come from your data.
580
+
532
581
  ## Migrating from v1
533
582
 
534
583
  v2 is a ground-up rewrite — simpler API, 55% smaller base bundle, zero-allocation scroll path. [Full announcement →](https://vlist.io/blog/v2)
package/README.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins.
4
4
 
5
- **v3.0.0-next.2** (prerelease on the npm `next` tag) — Native scrolling by default, opt-in `vlist/synthetic`, and removal of deprecated scroll configuration and plugin hooks. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
5
+ **v3.0.0-next.4** (prerelease on the npm `next` tag) — Native scrolling by default, opt-in `vlist/synthetic`, and removal of deprecated scroll configuration and plugin hooks. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
8
  [![bundle size](https://img.shields.io/bundlephobia/minzip/vlist)](https://bundlephobia.com/package/vlist)
9
9
  [![CI](https://github.com/floor/vlist/actions/workflows/ci.yml/badge.svg)](https://github.com/floor/vlist/actions/workflows/ci.yml)
10
10
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
11
11
 
12
- - **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
12
+ - **Accessible** — `a11y()` or a selection mode adds WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
13
  - **Zero dependencies** — framework-agnostic core, tiny adapters for Vue, Svelte, Solid, React
14
- - **9.3 KB gzipped (3.0 prerelease)** — composable plugins with perfect tree-shaking
14
+ - **10.0 KB gzipped (3.0 prerelease)** — composable plugins with perfect tree-shaking
15
15
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
16
16
  - **Axis-neutral** — vertical and horizontal scrolling through a single code path, all plugins work in both orientations
17
17
 
@@ -55,7 +55,7 @@ const list = createVList({ container: '#app', items, item: { height: 200, templa
55
55
 
56
56
  ## Scroll input
57
57
 
58
- Native scrolling is the default: import `createVList` and plugins from `vlist`. This preserves browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. Carousel, sortable and horizontal RTL lists work with this factory. Carousel supplies its private wrap runway only when the plugin is imported; bounded scrolling is no longer a public mode.
58
+ Native scrolling is the default: import `createVList` and plugins from `vlist`. This preserves browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. Carousel and sortable work with this factory; horizontal RTL lists are rejected by both entries. Carousel supplies its private wrap runway only when the plugin is imported; bounded scrolling is no longer a public mode.
59
59
 
60
60
  | Entry | Input model | Use |
61
61
  |---|---|---|
@@ -81,9 +81,11 @@ const list = createVList({
81
81
 
82
82
  `page()` uses native document scrolling through an external source with either entry. Its content must fit the 16,777,216 px document element limit; creation throws above that limit when the size is known, and later growth warns once. A deferred custom renderer whose size is first committed during rendering also warns once. Use `vlist/synthetic` viewport scrolling for larger lists. Default native viewport lists warn once when content exceeds their browser-size safety limit, pointing to that entry.
83
83
 
84
+ `carousel()` works with either entry. Synthetic carousel folds preserve touch motion and smooth snapping across whole laps. `page()` and carousel remain incompatible. The shared carousel plugin includes its native wrap handler even when used with synthetic input; the combined size is reported below.
85
+
84
86
  Synthetic input limitations:
85
87
 
86
- - `carousel()`, `sortable()` and horizontal RTL lists throw with instructions to use `vlist`. Vertical lists and tables support `dir="rtl"`, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
88
+ - Horizontal RTL lists throw at creation — in this entry and in `vlist`. Vertical lists and tables support `dir="rtl"` in both, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
87
89
  - Same-axis touch stops at either boundary with no parent handoff, including gestures starting inside an edge-pinned list. Use the native default when boundary gestures must scroll the parent page.
88
90
  - There is no native main-axis scrollbar. Add `scrollbar()` for an accessible custom scrollbar. The synthetic entry rejects the `"native"` and `"none"` scrollbar strings.
89
91
  - Inertia initializes its frame clock on the first frame after release, adding up to one frame of release latency.
@@ -91,6 +93,19 @@ Synthetic input limitations:
91
93
 
92
94
  Measurement corrections from autosize preserve ongoing synthetic motion. Existing plugin conflicts still apply. See the [scroll input contract](https://vlist.io/docs/rfcs/RFC-014-Scroll-Input-Model).
93
95
 
96
+ ### Sortable gestures
97
+
98
+ `sortable()` works with either input entry. Mouse and trackpad dragging still starts after `dragThreshold` (5 px by default). Touch and pen without a handle use `touchDelay`, a 350 ms long press; moving at least `dragThreshold` pixels in any direction before it completes yields to scrolling. This delay separates a deliberate hold from a quick flick. With `handle`, dragging starts at the threshold without waiting; touches elsewhere scroll.
99
+
100
+ ```typescript
101
+ sortable({ touchDelay: 350, dragThreshold: 5 })
102
+ sortable({ handle: '.drag-handle' })
103
+ ```
104
+
105
+ A touch during scrolling catches the motion and does not arm a hold for that contact. Lift and press again once the list is at rest. A second finger or `pointercancel` cancels the pending press or active drag. Edge auto-scroll uses the list's logical position and stops on drop.
106
+
107
+ The touch-only `.vlist-item--touch-sort` class suppresses selection and callouts on the pressed item. A claimed touch/pen drag adds `.vlist-sort-ghost--touch` to the ghost as a visual cue; override that class in CSS to customize it. Keyboard reordering remains Space to grab/drop, arrows to move and Escape to cancel.
108
+
94
109
  ## Migrating to 3.0
95
110
 
96
111
  `3.0.0-next.1` used synthetic input by default. `3.0.0-next.2` restores the 2.x default. Import `vlist/synthetic` explicitly to retain synthetic behavior across these prereleases. `vlist/native` remains a deprecated compatibility alias; its `NativeScrollConfig` and `NativeCreateVListConfig` types alias the standard `ScrollConfig` and `CreateVListConfig`.
@@ -103,33 +118,35 @@ Measurement corrections from autosize preserve ongoing synthetic motion. Existin
103
118
  | `PluginContext.setScrollFns`, `disableDefaultScroll` | Use `setScrollSource` to supply an external position source and commit callback. |
104
119
  | `scale()` and `ScalePluginConfig` | Remove them; use `vlist/synthetic` for the full logical range. Config no longer installs a scale stub. |
105
120
 
106
- Removed mode/runway options throw a migration error before creating DOM. `vlist/config` retains scrollbar omission/options convenience and its top-level `scrollbar: "none"` option. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers now supply their handler factory as the second argument of `ctx.setBoundedWrap`.
121
+ Removed mode/runway options throw a migration error before creating DOM. `vlist/config` wires only what the config asks for: `selection`, the custom scrollbar, `snapshots` and `a11y` are each opt-in, so an adapter list matches a core list built from the same options. It keeps the scrollbar options convenience and its top-level `scrollbar: "none"`; pass `scrollbar: true` for the overlay scrollbar earlier versions added to every list. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers now supply their handler factory as the second argument of `ctx.scroll.setBoundedWrap`.
107
122
 
108
123
  ## Plugins
109
124
 
110
125
  | Entry / export | Minified | Gzipped |
111
126
  |---|---:|---:|
112
- | **Base (`vlist`)** | 25.4 KB | 9.3 KB |
113
- | `vlist/synthetic` | 31.1 KB | 11.4 KB |
114
- | `vlist/native` (alias) | 25.4 KB | 9.3 KB |
115
- | `a11y()` | 28.7 KB | 10.5 KB |
116
- | `selection()` | 34.8 KB | 12.1 KB |
117
- | `data()` | 39.0 KB | 14.1 KB |
118
- | `scrollbar()` | 33.6 KB | 12.2 KB |
119
- | `sortable()` | 34.9 KB | 12.2 KB |
120
- | `groups()` | 41.3 KB | 14.6 KB |
121
- | `page()` | 27.9 KB | 10.2 KB |
122
- | `snapshots()` | 28.7 KB | 10.4 KB |
123
- | `transition()` | 32.1 KB | 11.3 KB |
124
- | `autosize()` | 28.5 KB | 10.3 KB |
125
- | `grid()` | 32.5 KB | 11.8 KB |
126
- | `table()` | 43.8 KB | 15.2 KB |
127
- | `masonry()` | 36.8 KB | 13.4 KB |
128
- | `tree()` | 40.6 KB | 14.3 KB |
129
- | `search()` | 34.5 KB | 12.4 KB |
130
- | `carousel()` | 38.0 KB | 13.7 KB |
131
-
132
- Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin. The base is **9,523 bytes gzipped**, below the 9.9 KB target; synthetic input is **11,687 bytes** before plugins. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
127
+ | **Base (`vlist`)** | 26.9 KB | 10.0 KB |
128
+ | `vlist/synthetic` | 33.8 KB | 12.6 KB |
129
+ | `vlist/native` (alias) | 26.9 KB | 10.0 KB |
130
+ | `a11y()` | 30.7 KB | 11.3 KB |
131
+ | `selection()` | 36.4 KB | 12.9 KB |
132
+ | `data()` | 40.6 KB | 14.8 KB |
133
+ | `scrollbar()` | 35.1 KB | 12.8 KB |
134
+ | `sortable()` | 38.7 KB | 13.4 KB |
135
+ | `groups()` | 43.0 KB | 15.4 KB |
136
+ | `page()` | 29.5 KB | 10.9 KB |
137
+ | `snapshots()` | 30.2 KB | 11.1 KB |
138
+ | `transition()` | 33.7 KB | 12.0 KB |
139
+ | `autosize()` | 30.1 KB | 11.1 KB |
140
+ | `grid()` | 34.4 KB | 12.5 KB |
141
+ | `table()` | 45.4 KB | 15.9 KB |
142
+ | `masonry()` | 39.0 KB | 14.4 KB |
143
+ | `tree()` | 43.1 KB | 15.3 KB |
144
+ | `search()` | 36.5 KB | 13.3 KB |
145
+ | `carousel()` | 41.7 KB | 15.1 KB |
146
+ | `vlist/synthetic` + `carousel()` | 47.8 KB | 17.4 KB |
147
+ | `vlist/synthetic` + `sortable()` | 45.7 KB | 16.1 KB |
148
+
149
+ Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin. The base is **10,200 bytes gzipped**, within the 10.0 KB budget (10,240 bytes). `bun run size` fails if a scenario fails to build, an unused plugin leaks into a bundle, or any published size exceeds its gzip budget. Synthetic input is **12,889 bytes** before plugins. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
133
150
 
134
151
  ## Framework Adapters
135
152
 
package/dist/config.d.ts CHANGED
@@ -17,11 +17,20 @@
17
17
  */
18
18
  import type { VListItem, GroupsConfig, VListAdapter, ScrollConfig } from "./types";
19
19
  import { createVList } from "./core/create";
20
- import type { CreateVListConfig, VList, VListPlugin } from "./core/types";
20
+ import type { CreateVListConfig, VList, VListPlugin, PluginMethods } from "./core/types";
21
+ import type { AutosizeMethods } from "./plugins/autosize/plugin";
22
+ import type { DataMethods } from "./plugins/data/plugin";
23
+ import type { GridMethods } from "./plugins/grid/plugin";
24
+ import type { MasonryMethods } from "./plugins/masonry/plugin";
25
+ import type { GroupsMethods } from "./plugins/groups/plugin";
26
+ import type { SelectionMethods } from "./plugins/selection/plugin";
27
+ import type { ScrollbarMethods } from "./plugins/scrollbar/plugin";
28
+ import type { SnapshotsMethods } from "./plugins/snapshots/plugin";
21
29
  import type { DataPluginConfig } from "./plugins/data";
22
30
  import type { GridPluginConfig } from "./plugins/grid";
23
31
  import type { MasonryPluginConfig } from "./plugins/masonry";
24
32
  import type { SelectionPluginConfig } from "./plugins/selection";
33
+ import type { A11yPluginConfig } from "./plugins/a11y";
25
34
  import type { ScrollbarPluginConfig } from "./plugins/scrollbar";
26
35
  /** List creation function injected by an adapter consumer. */
27
36
  export type VListFactory<T extends VListItem = VListItem> = typeof createVList<T>;
@@ -43,11 +52,32 @@ export interface VListConfig<T extends VListItem = VListItem> extends Omit<Creat
43
52
  /** Masonry layout options — applied when `layout: "masonry"`. */
44
53
  masonry?: MasonryPluginConfig;
45
54
  /** Sticky group headers. */
46
- groups?: GroupsConfig;
47
- /** Row selection (single / multi). */
55
+ groups?: GroupsConfig<T>;
56
+ /**
57
+ * Row selection (single / multi). Omit it for a display-only list: nothing
58
+ * is wired then, so the list keeps `role="list"` and stays out of the tab
59
+ * order, exactly like a core list created without the plugin.
60
+ */
48
61
  selection?: SelectionPluginConfig;
49
- /** Custom scrollbar options, or `"none"` to disable the custom scrollbar. */
50
- scrollbar?: ScrollbarPluginConfig | "none";
62
+ /**
63
+ * Keyboard navigation, focus management and the WAI-ARIA listbox roles.
64
+ * Off by default, as in core: pass `true` for the defaults or an object to
65
+ * tune it. A selection mode other than `"none"` already provides both, so a
66
+ * list with one does not need this as well.
67
+ */
68
+ a11y?: A11yPluginConfig | boolean;
69
+ /**
70
+ * Custom overlay scrollbar. Off by default, as in core, which leaves the
71
+ * browser's native scrollbar in place: pass `true` for the defaults or an
72
+ * object to tune them. `"none"` is accepted for symmetry with
73
+ * `scroll.scrollbar` and wires nothing.
74
+ */
75
+ scrollbar?: ScrollbarPluginConfig | "none" | boolean;
76
+ /**
77
+ * Scroll position save/restore (`saveScroll` / `restoreScroll`).
78
+ * Off by default, as in core.
79
+ */
80
+ snapshots?: boolean;
51
81
  /** Async data source — enables the data plugin. */
52
82
  adapter?: VListAdapter<T>;
53
83
  /** Async loading tuning (used together with `adapter`). */
@@ -57,23 +87,110 @@ export interface VListConfig<T extends VListItem = VListItem> extends Omit<Creat
57
87
  * the convenience fields: a plugin whose `name` matches an auto-wired one
58
88
  * (e.g. passing `grid()` while `layout: "grid"` is set) replaces it rather
59
89
  * than duplicating; plugins with new names are appended.
90
+ *
91
+ * Looser than `createVList`'s plugin list: a plugin explicitly typed for a
92
+ * different item, such as `groups<Other>()`, is accepted with no check.
60
93
  */
61
- plugins?: VListPlugin<T>[];
94
+ plugins?: VListPlugin<any, any>[];
62
95
  }
63
96
  /**
64
97
  * Translate a {@link VListConfig} into the ordered plugin array that the core
65
- * `createVList` expects. Mirrors the adapters' historical behavior exactly:
66
- * `snapshots` is always included, and `selection` is always
67
- * present (in `"none"` mode when unset) so its API is available. Any user
68
- * `plugins` are appended last as an escape hatch.
98
+ * `createVList` expects. Every plugin comes from a field the caller set, so a
99
+ * config with no feature fields resolves to no plugins and an adapter list
100
+ * behaves like a core list given the same options. Any user `plugins` are
101
+ * appended last as an escape hatch.
69
102
  */
70
103
  export declare function resolvePlugins<T extends VListItem = VListItem>(config: VListConfig<T>): VListPlugin<T>[];
104
+ /** A field value that wires no plugin. `"none"` is the scrollbar's spelling of off. */
105
+ type Off = false | undefined | null | "none";
106
+ /**
107
+ * The methods a field adds when it is on. Distributive on purpose: a widened
108
+ * `boolean` is `true | false`, so the result is `M | {}` and a call on it is a
109
+ * type error — the honest answer when the compiler cannot know whether the
110
+ * plugin is seated. Narrow the config, or spell the field as a literal.
111
+ */
112
+ type OnOff<V, M> = V extends Off ? {} : M;
113
+ /**
114
+ * The methods field `K` adds, or nothing when the config has no such field.
115
+ * Indexing `C[K]` directly would fall back to the constraint's type for an
116
+ * absent key and hand a plain config every plugin's methods.
117
+ */
118
+ type Field<C, K extends PropertyKey, M> = C extends Record<K, infer V> ? OnOff<V, M> : {};
119
+ /** Whether the item spec carries an estimate, which wires autosize(). */
120
+ type Estimated<C> = C extends {
121
+ item: {
122
+ estimatedHeight: number;
123
+ } | {
124
+ estimatedWidth: number;
125
+ };
126
+ } ? C extends {
127
+ item: {
128
+ height: number;
129
+ } | {
130
+ width: number;
131
+ };
132
+ } ? {} : AutosizeMethods : {};
133
+ /**
134
+ * The plugin methods a {@link VListConfig} wires, derived from its fields the
135
+ * same way {@link resolvePlugins} derives the plugins — the two are the type
136
+ * and runtime halves of one mapping, and `test/types/config.ts` asserts they
137
+ * agree. This is what lets an adapter list carry `select()` or `reload()` the
138
+ * way a core list built with `createVList(config, [selection()])` does.
139
+ */
140
+ export type ConfigMethods<T extends VListItem, C extends VListConfig<T>> = Field<C, "adapter", DataMethods> & (C extends {
141
+ layout: "grid";
142
+ } ? GridMethods : {}) & (C extends {
143
+ layout: "masonry";
144
+ } ? MasonryMethods : {}) & Field<C, "groups", GroupsMethods<T>> & Field<C, "selection", SelectionMethods<T>> & Field<C, "scrollbar", ScrollbarMethods> & Field<C, "snapshots", SnapshotsMethods> & Estimated<C> & (C extends {
145
+ plugins: infer P extends readonly unknown[];
146
+ } ? PluginMethods<P> : {});
147
+ /**
148
+ * The item type a config is written for: the element type of its `items`, else
149
+ * the parameter of its template. `VListConfig<infer T>` would do neither
150
+ * reliably — a config with no `items` inferred `any`.
151
+ */
152
+ export type ConfigItem<C> = C extends {
153
+ items: readonly (infer T extends VListItem)[];
154
+ } ? T : C extends {
155
+ item: {
156
+ template: (item: infer T extends VListItem, ...rest: never[]) => unknown;
157
+ };
158
+ } ? T : VListItem;
159
+ /**
160
+ * {@link VListConfig} with everything but `items` blocked as an inference site,
161
+ * so `items` decides the item type and every callback is *checked* against it.
162
+ *
163
+ * Blocking the template alone was not enough: a grouping callback annotated for
164
+ * a supertype (`entity?: Entity` on a list of `Row`) is a second candidate, and
165
+ * it widened the whole list to `Entity` — `getItemAt()` lost `category`, and
166
+ * `setItems()` accepted an item without it. A callback for a supertype is still
167
+ * accepted, as it should be: it safely takes every `Row`.
168
+ */
169
+ type CheckedConfig<T extends VListItem> = VListConfig<NoInfer<T>>;
71
170
  /**
72
171
  * Create a vlist instance from a high-level {@link VListConfig}, resolving its
73
172
  * feature fields into plugins via {@link resolvePlugins}. This is the single
74
173
  * entry point every framework adapter delegates to.
174
+ *
175
+ * The item type comes from `items` when present, otherwise the template, and
176
+ * the list's methods from the config's feature fields ({@link ConfigMethods}).
177
+ * Do not pass a type argument — `createVListFromConfig<Row>` does not compile
178
+ * (TypeScript requires both parameters), so the partial-argument trap
179
+ * `createVList<Row>` falls into stays closed.
180
+ *
181
+ * `groups.getGroupForIndex` is typed with that item: an inline callback
182
+ * receives `item?: T`, not `any`, and a callback written for a different
183
+ * item type is a type error, including when `groups` is optional on the
184
+ * input type.
75
185
  */
76
- export declare function createVListFromConfig<T extends VListItem = VListItem>(config: VListConfig<T> & {
186
+ export declare function createVListFromConfig<T extends VListItem, const C extends CheckedConfig<T> & {
187
+ container: HTMLElement | string;
188
+ items: readonly T[];
189
+ }>(config: {
190
+ items: readonly T[];
191
+ } & CheckedConfig<T> & C): VList<T> & ConfigMethods<T, C>;
192
+ export declare function createVListFromConfig<T extends VListItem, const C extends VListConfig<T> & {
77
193
  container: HTMLElement | string;
78
- }): VList<T>;
194
+ }>(config: VListConfig<T> & C): VList<T> & ConfigMethods<T, C>;
195
+ export {};
79
196
  //# sourceMappingURL=config.d.ts.map