vlist 1.6.0 → 1.6.1
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 +364 -0
- package/README.md +27 -503
- package/dist/builder/types.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/internals.js +1 -1
- package/dist/size.json +1 -1
- package/dist/types.d.ts +7 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,42 +1,17 @@
|
|
|
1
1
|
# vlist
|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](https://www.npmjs.com/package/vlist)
|
|
8
6
|
[](https://github.com/floor/vlist/actions/workflows/ci.yml)
|
|
9
7
|
[](https://github.com/floor/vlist/blob/main/LICENSE)
|
|
10
8
|
|
|
11
|
-
- **Zero dependencies** —
|
|
12
|
-
- **
|
|
13
|
-
-
|
|
14
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
51
|
+
## Features
|
|
91
52
|
|
|
92
53
|
| Feature | Size | Description |
|
|
93
54
|
|---------|------|-------------|
|
|
94
|
-
| **Base** | 10.
|
|
95
|
-
| `withGrid()` | +3.
|
|
96
|
-
| `withMasonry()` | +3.3 KB | Pinterest-style masonry
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
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
|
-
| `
|
|
105
|
-
| `
|
|
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.4 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
|
-
|
|
|
414
|
-
|
|
|
415
|
-
|
|
|
416
|
-
|
|
|
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
|
-
##
|
|
77
|
+
## Docs & Examples
|
|
457
78
|
|
|
458
|
-
|
|
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)
|
package/dist/builder/types.d.ts
CHANGED
|
@@ -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.
|