vlist 1.6.1 → 1.6.2

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/README.github.md CHANGED
@@ -2,46 +2,19 @@
2
2
 
3
3
  The virtual list library for every framework. Accessible by default, batteries-included, with composable features — in 10.5 KB.
4
4
 
5
- **v1.6.1** — [Changelog](./changelog.txt)
6
-
7
5
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
6
  [![CI](https://github.com/floor/vlist/actions/workflows/ci.yml/badge.svg)](https://github.com/floor/vlist/actions/workflows/ci.yml)
9
7
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
10
8
 
11
- - **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering, ARIA live region
12
- - **Zero dependencies** — framework-agnostic core with tiny adapters for Vue, Svelte, Solid, React
9
+ - **Zero dependencies** — framework-agnostic core, tiny adapters for Vue, Svelte, Solid, React
10
+ - **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
11
  - **10.5 KB gzipped** — composable features with perfect tree-shaking
14
12
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
15
- - **Grid, masonry, table, groups, async, selection, scale** — all opt-in
16
- - **Vertical & horizontal** — dimension-agnostic API, every layout mode works in both orientations
17
-
18
- **18 interactive examples, docs & benchmarks → [vlist.io](https://vlist.io)**
19
-
20
- ## Why vlist
21
-
22
- | | vlist | TanStack Virtual | react-virtuoso | virtua | vue-virtual-scroller |
23
- |---|---|---|---|---|---|
24
- | **A11y built-in** | WAI-ARIA + 2D keyboard | None (DIY) | Partial | Minimal | None |
25
- | **Grid + Masonry + Table** | All | Grid only | Grid + Table | Grid only | None |
26
- | **Vue** | 0.6 KB adapter | Yes | — | Yes | 11.8 KB |
27
- | **Svelte** | 0.5 KB adapter | Yes | — | Yes | — |
28
- | **Solid** | 0.5 KB adapter | Yes | — | Yes | — |
29
- | **Vanilla JS** | Native | Yes | — | — | — |
30
- | **Constant memory** | ~0.1 MB at 1M | No | No | No | No |
31
-
32
- ## Framework Adapters
33
13
 
34
- | Framework | Package | Size |
35
- |-----------|---------|------|
36
- | Vanilla JS | `vlist` | Native — no adapter needed |
37
- | Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |
38
- | Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |
39
- | SolidJS | [`vlist-solidjs`](https://github.com/floor/vlist-solidjs) | 0.5 KB gzip |
40
- | React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |
14
+ ## Install
41
15
 
42
16
  ```bash
43
- npm install vlist # vanilla JS
44
- npm install vlist vlist-vue # or vlist-svelte / vlist-solidjs / vlist-react
17
+ npm install vlist
45
18
  ```
46
19
 
47
20
  ## Quick Start
@@ -62,303 +35,49 @@ const list = vlist({
62
35
  template: (item) => `<div>${item.name}</div>`,
63
36
  },
64
37
  }).build()
65
-
66
- list.scrollToIndex(10)
67
- list.setItems(newItems)
68
- list.on('item:click', ({ item }) => console.log(item))
69
38
  ```
70
39
 
71
- ## Builder Pattern
72
-
73
- Start with the base, add only what you need:
40
+ Add features with the builder pattern:
74
41
 
75
42
  ```typescript
76
- import { vlist, withGrid, withGroups, withSelection } from 'vlist'
43
+ import { vlist, withGrid, withSelection } from 'vlist'
77
44
 
78
- const list = vlist({
79
- container: '#app',
80
- items: photos,
81
- item: { height: 200, template: renderPhoto },
82
- })
45
+ const list = vlist({ container: '#app', items, item: { height: 200, template: render } })
83
46
  .use(withGrid({ columns: 4, gap: 16 }))
84
- .use(withGroups({
85
- getGroupForIndex: (i) => photos[i].category,
86
- header: { height: 40, template: (cat) => `<h2>${cat}</h2>` },
87
- }))
88
47
  .use(withSelection({ mode: 'multiple' }))
89
48
  .build()
90
49
  ```
91
50
 
92
- ### Features
51
+ ## Features
93
52
 
94
53
  | Feature | Size | Description |
95
54
  |---------|------|-------------|
96
- | **Base** | 10.5 KB | Core virtualization, gap, padding, ARIA live region, baseline keyboard nav |
97
- | `withGrid()` | +3.8 KB | 2D grid layout with context injection |
98
- | `withMasonry()` | +3.3 KB | Pinterest-style masonry layout with lane-aware nav |
99
- | `withGroups()` | +2.7 KB | Grouped lists with sticky/inline headers |
100
- | `withAsync()` | +4.5 KB | Lazy loading with adapters |
101
- | `withSelection()` | +2.7 KB | Single/multiple selection + 2D keyboard nav |
55
+ | **Base** | 10.5 KB | Virtualization, ARIA, keyboard nav, gap, padding |
56
+ | `withGrid()` | +3.8 KB | 2D grid layout |
57
+ | `withMasonry()` | +3.3 KB | Pinterest-style masonry with lane-aware keyboard nav |
58
+ | `withTable()` | +5.5 KB | Data table with columns, resize, sort |
59
+ | `withGroups()` | +2.7 KB | Sticky/inline headers |
60
+ | `withAsync()` | +4.5 KB | Lazy loading with velocity-aware fetching |
61
+ | `withSelection()` | +2.7 KB | Single/multiple selection with 2D keyboard nav |
102
62
  | `withScale()` | +3.1 KB | 1M+ items via scroll compression |
103
- | `withScrollbar()` | +1.1 KB | Custom scrollbar UI |
104
- | `withTable()` | +5.5 KB | Data table with columns, resize, sort, groups |
105
63
  | `withAutoSize()` | +0.9 KB | Auto-measure items via ResizeObserver |
106
- | `withPage()` | +0.4 KB | Document-level scrolling |
107
- | `withSnapshots()` | +0.7 KB | Scroll save/restore with autoSave |
108
-
109
- ## Examples
110
-
111
- More examples at **[vlist.io](https://vlist.io)**.
112
-
113
- ### Data Table
114
-
115
- ```typescript
116
- import { vlist, withTable, withSelection } from 'vlist'
117
-
118
- const table = vlist({
119
- container: '#my-table',
120
- items: contacts,
121
- item: { height: 36, template: () => '' },
122
- })
123
- .use(withTable({
124
- columns: [
125
- { key: 'name', label: 'Name', width: 200, sortable: true },
126
- { key: 'email', label: 'Email', width: 260, sortable: true },
127
- { key: 'role', label: 'Role', width: 160, sortable: true },
128
- { key: 'status', label: 'Status', width: 100, align: 'center' },
129
- ],
130
- rowHeight: 36,
131
- headerHeight: 36,
132
- resizable: true,
133
- }))
134
- .use(withSelection({ mode: 'single' }))
135
- .build()
136
-
137
- table.on('column:sort', ({ key, direction }) => { /* re-sort data */ })
138
- table.on('column:resize', ({ key, width }) => { /* persist widths */ })
139
- ```
140
-
141
- ### Grid Layout
142
-
143
- ```typescript
144
- import { vlist, withGrid, withScrollbar } from 'vlist'
145
-
146
- const gallery = vlist({
147
- container: '#gallery',
148
- items: photos,
149
- item: {
150
- height: 200,
151
- template: (photo) => `
152
- <div class="card">
153
- <img src="${photo.url}" />
154
- <span>${photo.title}</span>
155
- </div>
156
- `,
157
- },
158
- })
159
- .use(withGrid({ columns: 4, gap: 16 }))
160
- .use(withScrollbar({ autoHide: true }))
161
- .build()
162
- ```
163
-
164
- ### Async Loading
165
-
166
- ```typescript
167
- import { vlist, withAsync } from 'vlist'
168
-
169
- const list = vlist({
170
- container: '#list',
171
- item: {
172
- height: 64,
173
- template: (item) => item
174
- ? `<div>${item.name}</div>`
175
- : `<div class="placeholder">Loading…</div>`,
176
- },
177
- })
178
- .use(withAsync({
179
- adapter: {
180
- read: async ({ offset, limit }) => {
181
- const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
182
- const data = await res.json()
183
- return { items: data.items, total: data.total, hasMore: data.hasMore }
184
- },
185
- },
186
- }))
187
- .build()
188
- ```
189
-
190
- ## Accessibility
191
-
192
- Every vlist is accessible by default following the [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
193
-
194
- - **Arrow keys** move focus between items with a visible focus ring
195
- - **2D navigation** in grids and masonry — Up/Down by row, Left/Right by cell
196
- - **Masonry lane-aware nav** — arrows stay in the same visual column
197
- - **Home/End, PageUp/PageDown, Ctrl+Home/End** — full keyboard coverage
198
- - **Screen-reader DOM ordering** — items reordered on scroll idle for correct reading order
199
- - **ARIA live region** — announces loading state changes
200
- - **Focus recovery** — maintains focus when items are removed
201
-
202
- Set `interactive: false` for display-only lists (log viewers, activity feeds) where items contain their own interactive elements.
203
-
204
- ## API
205
-
206
- ```typescript
207
- const list = vlist(config).use(...features).build()
208
- ```
209
-
210
- ### Data
211
-
212
- | Method | Description |
213
- |--------|-------------|
214
- | `list.setItems(items)` | Replace all items |
215
- | `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
216
- | `list.prependItems(items)` | Add to start (preserves scroll position) |
217
- | `list.updateItem(index, partial)` | Update a single item by index |
218
- | `list.removeItem(index)` | Remove by index |
219
- | `list.getItemAt(index)` | Get item at index |
220
- | `list.getIndexById(id)` | Get index by item ID |
221
- | `list.reload()` | Re-fetch from adapter (async) |
222
-
223
- ### Navigation
224
-
225
- | Method | Description |
226
- |--------|-------------|
227
- | `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \| `'center'` \| `'end'`) |
228
- | `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |
229
- | `list.cancelScroll()` | Cancel smooth scroll animation |
230
- | `list.getScrollPosition()` | Current scroll offset |
231
-
232
- ### Selection (with `withSelection()`)
233
-
234
- | Method | Description |
235
- |--------|-------------|
236
- | `list.select(...ids)` | Select item(s) |
237
- | `list.deselect(...ids)` | Deselect item(s) |
238
- | `list.toggleSelect(id)` | Toggle |
239
- | `list.selectAll()` / `list.clearSelection()` | Bulk operations |
240
- | `list.getSelected()` | Array of selected IDs |
241
- | `list.getSelectedItems()` | Array of selected items |
242
-
243
- ### Events
244
-
245
- `list.on()` returns an unsubscribe function. You can also use `list.off(event, handler)`.
246
-
247
- ```typescript
248
- list.on('scroll', ({ scrollPosition, direction }) => {})
249
- list.on('range:change', ({ range }) => {})
250
- list.on('item:click', ({ item, index, event }) => {})
251
- list.on('item:dblclick', ({ item, index, event }) => {})
252
- list.on('selection:change', ({ selectedIds, selectedItems }) => {})
253
- list.on('load:start', ({ offset, limit }) => {})
254
- list.on('load:end', ({ items, offset, total }) => {})
255
- list.on('load:error', ({ error, offset, limit }) => {})
256
- ```
257
-
258
- ### Properties
259
-
260
- | Property | Description |
261
- |----------|-------------|
262
- | `list.element` | Root DOM element |
263
- | `list.items` | Current items (readonly) |
264
- | `list.total` | Total item count |
265
- | `list.destroy()` | Cleanup and remove from DOM |
266
-
267
- ## Feature Configuration
268
-
269
- Each feature's config is fully typed — hover in your IDE for details.
270
-
271
- ```typescript
272
- withGrid({ columns: 4, gap: 16 })
273
- withMasonry({ columns: 4, gap: 16 })
274
- withGroups({ getGroupForIndex, header: { height, template }, sticky?: true })
275
- withSelection({ mode: 'single' | 'multiple', initial?: [...ids] })
276
- withAsync({ adapter: { read }, loading?: { cancelThreshold? } })
277
- withTable({ columns, rowHeight, headerHeight?, resizable? })
278
- withAutoSize() // auto-measure items (requires estimatedHeight)
279
- withScale() // auto-activates at 16.7M px
280
- withScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
281
- withPage() // no config — uses document scroll
282
- withSnapshots({ autoSave: 'key' }) // automatic sessionStorage save/restore
283
- ```
284
-
285
- Full configuration reference → **[vlist.io](https://vlist.io)**
286
-
287
- ## Base Configuration
288
-
289
- | Option | Default | Description |
290
- |--------|---------|-------------|
291
- | `overscan` | `3` | Extra items rendered outside viewport |
292
- | `ariaLabel` | — | Accessible label for the listbox |
293
- | `orientation` | `'vertical'` | `'vertical'` or `'horizontal'` scroll direction |
294
- | `padding` | `0` | Content inset — number, `[v, h]`, or `[top, right, bottom, left]` |
295
- | `interactive` | `true` | Enable built-in keyboard navigation |
296
- | `reverse` | `false` | Reverse mode for chat UIs |
297
- | `scroll.wrap` | `false` | Wrap focus around at boundaries |
298
-
299
- ## Styling
300
-
301
- ```typescript
302
- import 'vlist/styles' // core (always required)
303
- import 'vlist/styles/grid' // when using withGrid()
304
- import 'vlist/styles/masonry' // when using withMasonry()
305
- import 'vlist/styles/table' // when using withTable()
306
- import 'vlist/styles/extras' // optional (variants, loading states, animations)
307
- ```
308
-
309
- Dark mode works out of the box via `prefers-color-scheme`, Tailwind's `.dark` class, or `data-theme-mode="dark"`. Override CSS custom properties to match your design system. See [vlist.io/tutorials/styling](https://vlist.io/tutorials/styling) for the full guide.
310
-
311
- ## Performance
312
-
313
- | Dataset Size | After Render | Scroll Delta |
314
- |--------------|-------------|--------------|
315
- | 10K items | 0.07 MB | ~0 MB |
316
- | 100K items | 0.08 MB | ~0 MB |
317
- | 1M items | 0.09 MB | 0.19 MB |
318
-
319
- - **Initial render:** ~8ms (constant, regardless of item count)
320
- - **Scroll:** 120 FPS at any scale
321
- - **DOM nodes:** ~26 in document with 100K items (visible + overscan only)
322
-
323
- Live benchmarks against 9 competitors → **[vlist.io/benchmarks](https://vlist.io/benchmarks)**
324
-
325
- ## TypeScript
326
-
327
- Fully typed. Generic over your item type:
328
-
329
- ```typescript
330
- import { vlist, withGrid, type VList } from 'vlist'
64
+ | `withScrollbar()` | +1.1 KB | Custom scrollbar UI |
65
+ | `withPage()` | +0.9 KB | Window-level scrolling |
66
+ | `withSnapshots()` | +0.7 KB | Scroll position save/restore |
331
67
 
332
- interface Photo { id: number; url: string; title: string }
68
+ ## Framework Adapters
333
69
 
334
- const list: VList<Photo> = vlist<Photo>({
335
- container: '#gallery',
336
- items: photos,
337
- item: {
338
- height: 200,
339
- template: (photo) => `<img src="${photo.url}" />`,
340
- },
341
- })
342
- .use(withGrid({ columns: 4 }))
343
- .build()
344
- ```
70
+ | Framework | Package | Size |
71
+ |-----------|---------|------|
72
+ | Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB |
73
+ | Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB |
74
+ | SolidJS | [`vlist-solidjs`](https://github.com/floor/vlist-solidjs) | 0.5 KB |
75
+ | React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB |
345
76
 
346
- ## Contributing
77
+ ## Docs & Examples
347
78
 
348
- 1. Fork branch make changes add testspull request
349
- 2. Run `bun test` and `bun run build` before submitting
79
+ **18 interactive examples, full API reference, tutorials, and live benchmarks[vlist.io](https://vlist.io)**
350
80
 
351
81
  ## License
352
82
 
353
- [MIT](LICENSE)
354
-
355
- ## Links
356
-
357
- - **Docs & Examples:** [vlist.io](https://vlist.io)
358
- - **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
359
- - **NPM:** [vlist](https://www.npmjs.com/package/vlist)
360
- - **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
361
-
362
- ---
363
-
364
- Built by [Floor IO](https://floor.io)
83
+ [MIT](LICENSE) — Built by [Floor IO](https://floor.io)
package/README.md CHANGED
@@ -62,7 +62,7 @@ const list = vlist({ container: '#app', items, item: { height: 200, template: re
62
62
  | `withScale()` | +3.1 KB | 1M+ items via scroll compression |
63
63
  | `withAutoSize()` | +0.9 KB | Auto-measure items via ResizeObserver |
64
64
  | `withScrollbar()` | +1.1 KB | Custom scrollbar UI |
65
- | `withPage()` | +0.4 KB | Window-level scrolling |
65
+ | `withPage()` | +0.9 KB | Window-level scrolling |
66
66
  | `withSnapshots()` | +0.7 KB | Scroll position save/restore |
67
67
 
68
68
  ## Framework Adapters
@@ -13,11 +13,56 @@
13
13
  * - Uses window.innerWidth/innerHeight for container dimensions
14
14
  * - Listens to window resize events instead of ResizeObserver
15
15
  * - Adjusts DOM styles (overflow: visible, height: auto)
16
+ * - Optionally accounts for fixed/sticky chrome via scrollPadding
17
+ * - Uses behavior: "instant" on all scrollTo calls to override CSS
18
+ * scroll-behavior: smooth that may be set on the page
16
19
  *
17
20
  * Bundle impact: ~0.3 KB gzipped when used
18
21
  */
19
22
  import type { VListItem } from "../../types";
20
23
  import type { VListFeature } from "../../builder/types";
24
+ /**
25
+ * Options for the window scroll mode feature.
26
+ */
27
+ export interface WithPageOptions {
28
+ /**
29
+ * Scroll padding — insets from the viewport edges where fixed/sticky
30
+ * elements (headers, footers, toolbars) live.
31
+ *
32
+ * When keyboard focus moves an item behind a sticky bar, the list
33
+ * auto-scrolls to keep it within the visible (unobstructed) area.
34
+ * Also affects `scrollToIndex` alignment (start/center/end).
35
+ *
36
+ * Mirrors CSS `scroll-padding` semantics: defines the optimal viewing
37
+ * region within the scrollport.
38
+ *
39
+ * Values can be numbers (pixels) or functions that return pixels
40
+ * (useful when the sticky element's height is dynamic).
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * withPage({
45
+ * scrollPadding: { top: 60, bottom: 50 }
46
+ * })
47
+ * ```
48
+ *
49
+ * @example Dynamic values
50
+ * ```ts
51
+ * withPage({
52
+ * scrollPadding: {
53
+ * top: () => document.getElementById('sticky-header')!.offsetHeight,
54
+ * bottom: 50
55
+ * }
56
+ * })
57
+ * ```
58
+ */
59
+ scrollPadding?: {
60
+ top?: number | (() => number);
61
+ bottom?: number | (() => number);
62
+ left?: number | (() => number);
63
+ right?: number | (() => number);
64
+ };
65
+ }
21
66
  /**
22
67
  * Create a window scroll mode feature.
23
68
  *
@@ -38,6 +83,17 @@ import type { VListFeature } from "../../builder/types";
38
83
  * .build()
39
84
  * ```
40
85
  *
86
+ * @example With scroll padding for sticky chrome
87
+ * ```ts
88
+ * const feed = vlist({
89
+ * container: '#infinite-feed',
90
+ * item: { height: 200, template: renderPost },
91
+ * items: posts
92
+ * })
93
+ * .use(withPage({ scrollPadding: { top: 60, bottom: 50 } }))
94
+ * .build()
95
+ * ```
96
+ *
41
97
  * @example Horizontal window scrolling
42
98
  * ```ts
43
99
  * const timeline = vlist({
@@ -49,5 +105,5 @@ import type { VListFeature } from "../../builder/types";
49
105
  * .build()
50
106
  * ```
51
107
  */
52
- export declare const withPage: <T extends VListItem = VListItem>() => VListFeature<T>;
108
+ export declare const withPage: <T extends VListItem = VListItem>(options?: WithPageOptions) => VListFeature<T>;
53
109
  //# sourceMappingURL=feature.d.ts.map
@@ -4,5 +4,6 @@
4
4
  * Entry point for the window scroll feature.
5
5
  */
6
6
  export { withPage } from "./feature";
7
+ export type { WithPageOptions } from "./feature";
7
8
  export type { VListFeature } from "../../builder/types";
8
9
  //# sourceMappingURL=index.d.ts.map
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export type { ScaleConfig } from "./features/scale";
14
14
  export { withAsync } from "./features/async";
15
15
  export { withScrollbar } from "./features/scrollbar";
16
16
  export { withPage } from "./features/page";
17
+ export type { WithPageOptions } from "./features/page";
17
18
  export { withGroups } from "./features/groups";
18
19
  export { withGrid } from "./features/grid";
19
20
  export { withMasonry } from "./features/masonry";