vlist 1.6.0 → 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.
@@ -0,0 +1,83 @@
1
+ # vlist
2
+
3
+ The virtual list library for every framework. Accessible by default, batteries-included, with composable features — in 10.5 KB.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
6
+ [![CI](https://github.com/floor/vlist/actions/workflows/ci.yml/badge.svg)](https://github.com/floor/vlist/actions/workflows/ci.yml)
7
+ [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
8
+
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
11
+ - **10.5 KB gzipped** — composable features with perfect tree-shaking
12
+ - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install vlist
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { vlist } from 'vlist'
24
+ import 'vlist/styles'
25
+
26
+ const list = vlist({
27
+ container: '#my-list',
28
+ items: [
29
+ { id: 1, name: 'Alice' },
30
+ { id: 2, name: 'Bob' },
31
+ { id: 3, name: 'Charlie' },
32
+ ],
33
+ item: {
34
+ height: 48,
35
+ template: (item) => `<div>${item.name}</div>`,
36
+ },
37
+ }).build()
38
+ ```
39
+
40
+ Add features with the builder pattern:
41
+
42
+ ```typescript
43
+ import { vlist, withGrid, withSelection } from 'vlist'
44
+
45
+ const list = vlist({ container: '#app', items, item: { height: 200, template: render } })
46
+ .use(withGrid({ columns: 4, gap: 16 }))
47
+ .use(withSelection({ mode: 'multiple' }))
48
+ .build()
49
+ ```
50
+
51
+ ## Features
52
+
53
+ | Feature | Size | Description |
54
+ |---------|------|-------------|
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 |
62
+ | `withScale()` | +3.1 KB | 1M+ items via scroll compression |
63
+ | `withAutoSize()` | +0.9 KB | Auto-measure items via ResizeObserver |
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 |
67
+
68
+ ## Framework Adapters
69
+
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 |
76
+
77
+ ## Docs & Examples
78
+
79
+ **18 interactive examples, full API reference, tutorials, and live benchmarks → [vlist.io](https://vlist.io)**
80
+
81
+ ## License
82
+
83
+ [MIT](LICENSE) — Built by [Floor IO](https://floor.io)
package/README.md CHANGED
@@ -1,42 +1,17 @@
1
1
  # vlist
2
2
 
3
- Lightweight, high-performance virtual list with zero dependencies and dimension-agnostic architecture.
4
-
5
- **v1.6.0** — [Changelog](./changelog.txt)
3
+ The virtual list library for every framework. Accessible by default, batteries-included, with composable features — in 10.5 KB.
6
4
 
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
- - **Zero dependencies** — no external libraries
12
- - **Ultra memory efficient** — ~0.1-0.2 MB constant overhead regardless of dataset size
13
- - **~10.3 KB gzipped** — pay only for features you use (vs 20 KB+ monolithic alternatives)
14
- - **Builder API** — composable features with perfect tree-shaking
15
- - **Grid, masonry, table, groups, async, selection, scale** — all opt-in
16
- - **Horizontal & vertical** — semantically correct orientation support
17
- - **Gap & padding** — built-in item spacing and content inset (CSS shorthand convention)
18
- - **Reverse, page-scroll, wrap** — every layout mode
19
- - **Accessible** — WAI-ARIA, keyboard navigation, focus-visible, screen-reader DOM ordering, ARIA live region
20
- - **React, Vue, Svelte, SolidJS** — framework adapters available
21
-
22
- **14+ interactive examples → [vlist.io](https://vlist.io)**
23
-
24
- ## Highlights
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
11
+ - **10.5 KB gzipped** — composable features with perfect tree-shaking
12
+ - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
25
13
 
26
- - **WAI-ARIA Grid keyboard navigation** — grids and masonry layouts support full 2D arrow-key navigation (Up/Down by row, Left/Right by cell), row-scoped Home/End, Ctrl+Home/End, PageUp/Down. Horizontal orientation swaps axes correctly.
27
- - **Masonry lane-aware navigation** — ArrowUp/Down stay in the same visual column, ArrowLeft/Right move to the nearest item in the adjacent lane. O(1) same-lane / O(log k) adjacent-lane via pre-built per-lane index arrays.
28
- - **Data table** — virtualized columns with resize, sort, horizontal scroll, and grouped sections via `withTable()`
29
- - **Dimension-agnostic API** — semantically correct terminology for both orientations
30
- - **Performance optimized** — 13-pattern optimization playbook applied across the entire rendering pipeline
31
- - **Horizontal groups** — sticky headers work in horizontal carousels
32
- - **Horizontal grid layouts** — 2D grids work in both orientations
33
- - **Masonry** — shortest-lane placement via `withMasonry()`
34
- - **Keyboard accessible** — focus-visible outlines, full 2D keyboard navigation, smart edge-scroll, Tab support
35
- - **Responsive grid & masonry** — context-injected `columnWidth` auto-recalculates on resize
36
- - **Modular CSS** — core (7.4 KB) + opt-in grid, masonry, table, and extras stylesheets. Import only what you use.
37
- - **Composable dark mode** — three strategies (`prefers-color-scheme`, `.dark` class, `data-theme-mode` attribute) with rgba state colors for clear visual hierarchy
38
-
39
- ## Installation
14
+ ## Install
40
15
 
41
16
  ```bash
42
17
  npm install vlist
@@ -60,500 +35,49 @@ const list = vlist({
60
35
  template: (item) => `<div>${item.name}</div>`,
61
36
  },
62
37
  }).build()
63
-
64
- list.scrollToIndex(10)
65
- list.setItems(newItems)
66
- list.on('item:click', ({ item }) => console.log(item))
67
38
  ```
68
39
 
69
- ## Builder Pattern
70
-
71
- Start with the base, add only what you need:
40
+ Add features with the builder pattern:
72
41
 
73
42
  ```typescript
74
- import { vlist, withGrid, withGroups, withSelection } from 'vlist'
43
+ import { vlist, withGrid, withSelection } from 'vlist'
75
44
 
76
- const list = vlist({
77
- container: '#app',
78
- items: photos,
79
- item: { height: 200, template: renderPhoto },
80
- })
45
+ const list = vlist({ container: '#app', items, item: { height: 200, template: render } })
81
46
  .use(withGrid({ columns: 4, gap: 16 }))
82
- .use(withGroups({
83
- getGroupForIndex: (i) => photos[i].category,
84
- header: { height: 40, template: (cat) => `<h2>${cat}</h2>` },
85
- }))
86
47
  .use(withSelection({ mode: 'multiple' }))
87
48
  .build()
88
49
  ```
89
50
 
90
- ### Features
51
+ ## Features
91
52
 
92
53
  | Feature | Size | Description |
93
54
  |---------|------|-------------|
94
- | **Base** | 10.4 KB | Core virtualization, gap, padding, ARIA live region, baseline keyboard nav |
95
- | `withGrid()` | +3.9 KB | 2D grid layout with context injection |
96
- | `withMasonry()` | +3.3 KB | Pinterest-style masonry layout with lane-aware nav |
97
- | `withGroups()` | +2.7 KB | Grouped lists with sticky/inline headers |
98
- | `withAsync()` | +4.4 KB | Lazy loading with adapters |
99
- | `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 |
100
62
  | `withScale()` | +3.1 KB | 1M+ items via scroll compression |
101
- | `withScrollbar()` | +1.1 KB | Custom scrollbar UI |
102
- | `withTable()` | +5.5 KB | Data table with columns, resize, sort, groups |
103
63
  | `withAutoSize()` | +0.9 KB | Auto-measure items via ResizeObserver |
104
- | `withPage()` | +0.4 KB | Document-level scrolling |
105
- | `withSnapshots()` | +0.7 KB | Scroll save/restore with autoSave |
106
-
107
- ## Examples
108
-
109
- More examples at **[vlist.io](https://vlist.io)**.
110
-
111
- ### Data Table
112
-
113
- ```typescript
114
- import { vlist, withTable, withSelection } from 'vlist'
115
-
116
- const table = vlist({
117
- container: '#my-table',
118
- items: contacts,
119
- item: { height: 36, template: () => '' },
120
- })
121
- .use(withTable({
122
- columns: [
123
- { key: 'name', label: 'Name', width: 200, sortable: true },
124
- { key: 'email', label: 'Email', width: 260, sortable: true },
125
- { key: 'role', label: 'Role', width: 160, sortable: true },
126
- { key: 'status', label: 'Status', width: 100, align: 'center' },
127
- ],
128
- rowHeight: 36,
129
- headerHeight: 36,
130
- resizable: true,
131
- }))
132
- .use(withSelection({ mode: 'single' }))
133
- .build()
134
-
135
- table.on('column:sort', ({ key, direction }) => { /* re-sort data */ })
136
- table.on('column:resize', ({ key, width }) => { /* persist widths */ })
137
- ```
138
-
139
- ### Grid Layout
140
-
141
- ```typescript
142
- import { vlist, withGrid, withScrollbar } from 'vlist'
143
-
144
- const gallery = vlist({
145
- container: '#gallery',
146
- items: photos,
147
- item: {
148
- height: 200,
149
- template: (photo) => `
150
- <div class="card">
151
- <img src="${photo.url}" />
152
- <span>${photo.title}</span>
153
- </div>
154
- `,
155
- },
156
- })
157
- .use(withGrid({ columns: 4, gap: 16 }))
158
- .use(withScrollbar({ autoHide: true }))
159
- .build()
160
- ```
161
-
162
- ### Sticky Headers
163
-
164
- ```typescript
165
- import { vlist, withGroups } from 'vlist'
166
-
167
- const contacts = vlist({
168
- container: '#contacts',
169
- items: sortedContacts,
170
- item: {
171
- height: 56,
172
- template: (contact) => `<div>${contact.name}</div>`,
173
- },
174
- })
175
- .use(withGroups({
176
- getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
177
- header: {
178
- height: 36,
179
- template: (letter) => `<div class="header">${letter}</div>`,
180
- },
181
- sticky: true,
182
- }))
183
- .build()
184
- ```
185
-
186
- Set `sticky: false` for inline headers (iMessage/WhatsApp style).
187
-
188
- ### Async Loading
189
-
190
- ```typescript
191
- import { vlist, withAsync } from 'vlist'
192
-
193
- const list = vlist({
194
- container: '#list',
195
- item: {
196
- height: 64,
197
- template: (item) => item
198
- ? `<div>${item.name}</div>`
199
- : `<div class="placeholder">Loading…</div>`,
200
- },
201
- })
202
- .use(withAsync({
203
- adapter: {
204
- read: async ({ offset, limit }) => {
205
- const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
206
- const data = await res.json()
207
- return { items: data.items, total: data.total, hasMore: data.hasMore }
208
- },
209
- },
210
- }))
211
- .build()
212
- ```
213
-
214
- ### More Patterns
215
-
216
- | Pattern | Key options |
217
- |---------|------------|
218
- | **Chat UI** | `reverse: true` + `withGroups({ sticky: false })` |
219
- | **Horizontal carousel** | `orientation: 'horizontal'`, `item.width` |
220
- | **Horizontal groups** | `orientation: 'horizontal'` + `withGroups()` |
221
- | **Horizontal grid** | `orientation: 'horizontal'` + `withGrid()` |
222
- | **Data table** | `withTable({ columns, rowHeight, resizable })` |
223
- | **Grouped table** | `withTable({ columns, rowHeight })` + `withGroups({ ... })` |
224
- | **Item gap** | `item: { height: 48, gap: 8 }` |
225
- | **Content padding** | `padding: 16` or `padding: [16, 12]` or `padding: [16, 12, 20, 8]` |
226
- | **Masonry** | `withMasonry({ columns: 4, gap: 16 })` |
227
- | **Page-level scroll** | `withPage()` |
228
- | **1M+ items** | `withScale()` — auto-compresses scroll space |
229
- | **Wrap navigation** | `scroll: { wrap: true }` |
230
- | **Variable heights** | `item: { height: (index) => heights[index] }` |
231
- | **Auto-measured sizes** | `item: { estimatedHeight: 120 }` + `withAutoSize()` |
232
- | **Zebra striping** | `item: { striped: true }` or `striped: 'even'` / `'odd'` / `'data'` (group-aware) |
233
-
234
- See **[vlist.io](https://vlist.io)** for live demos of each.
235
-
236
- ## API
237
-
238
- ```typescript
239
- const list = vlist(config).use(...features).build()
240
- ```
241
-
242
- ### Data
243
-
244
- | Method | Description |
245
- |--------|-------------|
246
- | `list.setItems(items)` | Replace all items |
247
- | `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
248
- | `list.prependItems(items)` | Add to start (preserves scroll position) |
249
- | `list.updateItem(index, partial)` | Update a single item by index |
250
- | `list.removeItem(index)` | Remove by index |
251
- | `list.getItemAt(index)` | Get item at index |
252
- | `list.getIndexById(id)` | Get index by item ID |
253
- | `list.reload()` | Re-fetch from adapter (async) |
254
- | `list.reload({ snapshot })` | Re-fetch and restore scroll position from snapshot |
255
-
256
- ### Navigation
257
-
258
- | Method | Description |
259
- |--------|-------------|
260
- | `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \| `'center'` \| `'end'`) |
261
- | `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |
262
- | `list.cancelScroll()` | Cancel smooth scroll animation |
263
- | `list.getScrollPosition()` | Current scroll offset |
264
-
265
- ### Snapshots (with `withSnapshots()`)
266
-
267
- | Method | Description |
268
- |--------|-------------|
269
- | `withSnapshots({ autoSave: 'key' })` | Automatic save/restore via sessionStorage |
270
- | `list.getScrollSnapshot()` | Save scroll state (for manual patterns) |
271
- | `list.restoreScroll(snapshot)` | Restore saved scroll state |
272
-
273
- ### Selection (with `withSelection()`)
274
-
275
- | Method | Description |
276
- |--------|-------------|
277
- | `list.select(...ids)` | Select item(s) |
278
- | `list.deselect(...ids)` | Deselect item(s) |
279
- | `list.toggleSelect(id)` | Toggle |
280
- | `list.selectAll()` / `list.clearSelection()` | Bulk operations |
281
- | `list.getSelected()` | Array of selected IDs |
282
- | `list.getSelectedItems()` | Array of selected items |
283
-
284
- ### Grid (with `withGrid()`)
285
-
286
- | Method | Description |
287
- |--------|-------------|
288
- | `list.updateGrid({ columns, gap })` | Update grid at runtime |
289
-
290
- ### Table (with `withTable()`)
291
-
292
- | Method | Description |
293
- |--------|-------------|
294
- | `list.setSort(key, direction?)` | Set sort indicator (visual only) |
295
- | `list.getSort()` | Get current `{ key, direction }` |
296
- | `list.updateColumns(columns)` | Replace column definitions at runtime |
297
- | `list.resizeColumn(key, width)` | Resize a column programmatically |
298
- | `list.getColumnWidths()` | Get current widths keyed by column key |
299
-
300
- | Event | Payload |
301
- |-------|---------|
302
- | `column:sort` | `{ key, direction, index }` |
303
- | `column:resize` | `{ key, width, previousWidth, index }` |
304
- | `column:click` | `{ key, index, event }` |
305
-
306
- ### Events
307
-
308
- `list.on()` returns an unsubscribe function. You can also use `list.off(event, handler)`.
309
-
310
- ```typescript
311
- list.on('scroll', ({ scrollPosition, direction }) => {}) // v0.9.0: scrollPosition (was scrollTop)
312
- list.on('range:change', ({ range }) => {})
313
- list.on('item:click', ({ item, index, event }) => {})
314
- list.on('item:dblclick', ({ item, index, event }) => {})
315
- list.on('selection:change', ({ selectedIds, selectedItems }) => {})
316
- list.on('load:start', ({ offset, limit }) => {})
317
- list.on('load:end', ({ items, offset, total }) => {})
318
- list.on('load:error', ({ error, offset, limit }) => {})
319
- list.on('velocity:change', ({ velocity, reliable }) => {})
320
- ```
321
-
322
- ### Statistics (with `createStats()`)
323
-
324
- | Method | Description |
325
- |--------|-------------|
326
- | `createStats(list)` | Create a stats tracker for scroll performance metrics |
327
-
328
- ### Properties
329
-
330
- | Property | Description |
331
- |----------|-------------|
332
- | `list.element` | Root DOM element |
333
- | `list.items` | Current items (readonly) |
334
- | `list.total` | Total item count |
335
-
336
- ### Lifecycle
337
-
338
- ```typescript
339
- list.destroy()
340
- ```
341
-
342
- ## Feature Configuration
343
-
344
- Each feature's config is fully typed — hover in your IDE for details.
345
-
346
- ```typescript
347
- withGrid({ columns: 4, gap: 16 })
348
- withMasonry({ columns: 4, gap: 16 })
349
- withGroups({ getGroupForIndex, header: { height, template }, sticky?: true })
350
- withSelection({ mode: 'single' | 'multiple', initial?: [...ids], shiftArrowToggle?: 'origin' | 'destination' })
351
- withAsync({ adapter: { read }, loading?: { cancelThreshold? } })
352
- withTable({ columns, rowHeight, headerHeight?, resizable?, columnBorders?, rowBorders? })
353
- withAutoSize() // auto-measure items (requires estimatedHeight)
354
- withScale() // auto-activates at 16.7M px
355
- withScale({ force: true }) // force compression on any list size
356
- withScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
357
- withPage() // no config — uses document scroll
358
- withSnapshots({ autoSave: 'key' }) // automatic sessionStorage save/restore
359
- withSnapshots({ restore: snapshot }) // manual restore from saved snapshot
360
- ```
361
-
362
- Full configuration reference → **[vlist.io](https://vlist.io)**
363
-
364
- ## Base Configuration
365
-
366
- The `vlist()` factory accepts these base options alongside `container`, `items`, and `item`:
367
-
368
- | Option | Default | Description |
369
- |--------|---------|-------------|
370
- | `overscan` | `3` | Extra items rendered outside viewport |
371
- | `classPrefix` | `'vlist'` | CSS class prefix for all generated elements |
372
- | `ariaLabel` | — | Accessible label for the listbox (`aria-label`) |
373
- | `orientation` | `'vertical'` | `'vertical'` or `'horizontal'` scroll direction |
374
- | `padding` | `0` | Content inset — number, `[v, h]`, or `[top, right, bottom, left]` |
375
- | `interactive` | `true` | Enable built-in keyboard navigation (see below) |
376
- | `reverse` | `false` | Reverse mode for chat UIs (new items appear at bottom) |
377
- | `scroll.wheel` | `true` | Enable mouse wheel scrolling |
378
- | `scroll.wrap` | `false` | Wrap focus around at boundaries |
379
- | `scroll.gutter` | `'auto'` | Scrollbar gutter: `'auto'` or `'stable'` |
380
- | `scroll.idleTimeout` | `150` | Scroll idle detection timeout (ms) |
381
-
382
- ### `interactive` — Baseline Keyboard Navigation
383
-
384
- By default, every vlist is keyboard-navigable following the [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
385
-
386
- - **Arrow keys** move focus between items (with a visible focus ring)
387
- - **Space / Enter** selects the focused item
388
- - **Home / End** jump to first / last item
389
- - **Click** selects + focuses the clicked item
390
-
391
- This works **without** `withSelection()` — it's built into the base. The `withSelection()` feature adds multi-select, Shift+Arrow toggle, Shift+Space range select, Ctrl+A, and other advanced selection APIs on top — following the [WAI-ARIA APG recommended listbox model](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
392
-
393
- Set `interactive: false` to disable all built-in keyboard handling:
394
-
395
- ```typescript
396
- const feed = vlist({
397
- container: '#feed',
398
- items: posts,
399
- item: { height: 120, template: renderPost },
400
- interactive: false, // no item-level keyboard nav or focus ring
401
- }).build()
402
- ```
403
-
404
- Use this when:
405
- - The list is **display-only** (log viewer, activity feed, chat history)
406
- - Your app provides its **own keyboard navigation**
407
- - Items contain **interactive elements** (inputs, buttons) that need to own focus
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 |
408
67
 
409
68
  ## Framework Adapters
410
69
 
411
70
  | Framework | Package | Size |
412
71
  |-----------|---------|------|
413
- | React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |
414
- | Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |
415
- | Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |
416
- | SolidJS | [`vlist-solidjs`](https://github.com/floor/vlist-solidjs) | 0.5 KB gzip |
417
-
418
- ```bash
419
- npm install vlist vlist-react # or vlist-vue / vlist-svelte / vlist-solidjs
420
- ```
421
-
422
- Each adapter README has setup examples and API docs.
423
-
424
- ## Styling
425
-
426
- ```typescript
427
- import 'vlist/styles' // core (always required)
428
- import 'vlist/styles/grid' // when using withGrid()
429
- import 'vlist/styles/masonry' // when using withMasonry()
430
- import 'vlist/styles/table' // when using withTable()
431
- import 'vlist/styles/extras' // optional (variants, loading states, animations)
432
- ```
433
-
434
- | Import | Size | Contents |
435
- |--------|------|----------|
436
- | `vlist/styles` | 7.4 KB | Tokens, base list, item states, scrollbar, groups, horizontal mode |
437
- | `vlist/styles/grid` | 1.2 KB | Grid layout |
438
- | `vlist/styles/masonry` | 1.3 KB | Masonry layout |
439
- | `vlist/styles/table` | 7.2 KB | Table layout (header, rows, cells, resize) |
440
- | `vlist/styles/extras` | 1.1 KB | Variants, loading/empty states, enter animation |
441
-
442
- Override tokens to match your design system. See [vlist.io/tutorials/styling](https://vlist.io/tutorials/styling) for the full guide.
443
-
444
- ### Dark Mode
445
-
446
- Dark mode is supported out of the box via three mechanisms (no extra imports needed):
447
-
448
- | Method | How it works |
449
- |--------|-------------|
450
- | **OS preference** | `prefers-color-scheme: dark` — automatic |
451
- | **Tailwind `.dark` class** | Add `.dark` to any ancestor element |
452
- | **`data-theme-mode`** | Set `data-theme-mode="dark"` on `<html>` for explicit control |
453
-
454
- To force light mode when `prefers-color-scheme` would otherwise activate dark, set `data-theme-mode="light"` on the root element.
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 |
455
76
 
456
- ## Architecture
77
+ ## Docs & Examples
457
78
 
458
- ### Dimension-Agnostic Design (v0.9.0)
459
-
460
- vlist uses semantically correct terminology that works for both vertical and horizontal orientations:
461
-
462
- ```typescript
463
- // ✅ Correct: Works for both orientations
464
- sizeCache.getSize(index) // Returns height OR width
465
- state.scrollPosition // scrollTop OR scrollLeft
466
- state.containerSize // height OR width
467
-
468
- // Previously (v0.8.2): Semantically wrong in horizontal mode
469
- heightCache.getHeight(index) // ❌ Returned WIDTH in horizontal!
470
- state.scrollTop // ❌ Stored scrollLEFT!
471
- ```
472
-
473
- This makes the codebase clearer and eliminates semantic confusion when working with horizontal lists.
474
-
475
- **Migration from v0.8.2:** See [v0.9.0 Migration Guide](https://vlist.io/docs/refactoring/v0.9.0-migration-guide.md)
476
-
477
- ## Performance
478
-
479
- ### Bundle Size
480
-
481
- | Configuration | Gzipped |
482
- |---------------|---------|
483
- | Base only | 10.5 KB |
484
- | + Grid | 14.3 KB |
485
- | + Groups | 13.2 KB |
486
- | + Async | 14.8 KB |
487
- | + Table | 15.9 KB |
488
-
489
- ### Memory Efficiency
490
-
491
- vlist uses **constant memory** regardless of dataset size through optimized internal architecture:
492
-
493
- | Dataset Size | After Render | Scroll Delta | Notes |
494
- |--------------|-------------|--------------|-------|
495
- | 10K items | 0.07 MB | ~0 MB | Constant baseline |
496
- | 100K items | 0.08 MB | ~0 MB | 10× items, same memory |
497
- | 1M items | 0.09 MB | 0.19 MB | 100× items, near-zero scroll overhead |
498
-
499
- **Key advantages:**
500
- - No array copying — uses references for zero-copy performance
501
- - No ID indexing overhead — O(1) memory complexity
502
- - Reusable event payloads — zero per-frame object allocation on scroll
503
- - Content height cap at 16M px — avoids browser overhead for extremely large lists
504
-
505
- ### DOM Efficiency
506
-
507
- With 100K items: **~26 DOM nodes** in the document (visible + overscan) instead of 100,000.
508
-
509
- ### Render Performance
510
-
511
- - **Initial render:** ~8ms (constant, regardless of item count)
512
- - **Scroll performance:** 120 FPS (perfect smoothness)
513
- - **1M items:** Same performance as 10K items
514
-
515
- ## TypeScript
516
-
517
- Fully typed. Generic over your item type:
518
-
519
- ```typescript
520
- import { vlist, withGrid, type VList } from 'vlist'
521
-
522
- interface Photo { id: number; url: string; title: string }
523
-
524
- const list: VList<Photo> = vlist<Photo>({
525
- container: '#gallery',
526
- items: photos,
527
- item: {
528
- height: 200,
529
- template: (photo) => `<img src="${photo.url}" />`,
530
- },
531
- })
532
- .use(withGrid({ columns: 4 }))
533
- .build()
534
- ```
535
-
536
- ## Contributing
537
-
538
- 1. Fork → branch → make changes → add tests → pull request
539
- 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)**
540
80
 
541
81
  ## License
542
82
 
543
- [MIT](LICENSE)
544
-
545
- ## Changelog
546
-
547
- See [CHANGELOG.md](https://vlist.io/docs/CHANGELOG.md) for the full release history. A simplified [changelog.txt](./changelog.txt) is also available.
548
-
549
- ## Links
550
-
551
- - **Docs & Examples:** [vlist.io](https://vlist.io)
552
- - **Migration Guide:** [v0.9.0 Migration](https://vlist.io/docs/refactoring/v0.9.0-migration-guide.md)
553
- - **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
554
- - **NPM:** [vlist](https://www.npmjs.com/package/vlist)
555
- - **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
556
-
557
- ---
558
-
559
- Built by [Floor IO](https://floor.io)
83
+ [MIT](LICENSE) — Built by [Floor IO](https://floor.io)
@@ -295,6 +295,12 @@ export interface BuilderContext<T extends VListItem = VListItem> {
295
295
  * Used by withCompression to inject compressed range calculation.
296
296
  */
297
297
  setVisibleRangeFn(fn: (scrollTop: number, containerHeight: number, sc: SizeCache, totalItems: number, out: Range) => void): void;
298
+ /**
299
+ * Calculate the visible range using the current visible-range function.
300
+ * This uses the compression-aware version if withScale has replaced it.
301
+ * Used by withTable and withGrid to correctly compute ranges in compressed mode.
302
+ */
303
+ getVisibleRange(scrollTop: number, containerHeight: number, totalItems: number, out: Range): void;
298
304
  /**
299
305
  * Replace the scroll-to-index position calculator.
300
306
  * Used by withCompression to inject compressed position calculation.