@floor/vlist 0.7.6 → 0.8.0

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.md CHANGED
@@ -4,35 +4,19 @@ Lightweight, high-performance virtual list with zero dependencies and optimal tr
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40floor%2Fvlist.svg)](https://www.npmjs.com/package/@floor/vlist)
6
6
  [![bundle size](https://img.shields.io/bundlephobia/minzip/@floor/vlist)](https://bundlephobia.com/package/@floor/vlist)
7
- [![tests](https://img.shields.io/badge/tests-1739%20passing-brightgreen)](https://github.com/floor/vlist)
7
+ [![tests](https://img.shields.io/badge/tests-1181%20passing-brightgreen)](https://github.com/floor/vlist)
8
8
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
9
9
 
10
- ## Features
11
-
12
- - ðŸŠķ **Zero dependencies** - No external libraries required
13
- - ⚡ **Tiny bundle** - 8-12 KB gzipped (vs 20+ KB for traditional virtual lists)
14
- - ðŸŒē **Perfect tree-shaking** - Pay only for features you use
15
- - ðŸŽŊ **Builder API** - Explicit, composable plugin system
16
- - 📐 **Grid layout** - 2D virtualized grid with configurable columns
17
- - 📏 **Variable heights** - Fixed or per-item height calculation
18
- - ↔ïļ **Horizontal scrolling** - Horizontal lists and carousels
19
- - 📜 **Async loading** - Built-in lazy loading with adapters
20
- - ✅ **Selection** - Single and multiple selection modes
21
- - 📌 **Sticky headers** - Grouped lists with sticky section headers
22
- - 🊟 **Page scrolling** - Document-level scrolling mode
23
- - 🔄 **Wrap navigation** - Circular scrolling for wizards
24
- - 💎 **Reverse mode** - Chat UI with auto-scroll and history loading
25
- - ⚖ïļ **Scale to millions** - Handle 1M+ items with automatic compression
26
- - ðŸŽĻ **Customizable** - Beautiful, customizable styles
27
- - â™ŋ **Accessible** - Full WAI-ARIA support and keyboard navigation
28
- - 🔌 **Framework adapters** - React, Vue, and Svelte support
29
- - ðŸ“ą **Mobile optimized** - Touch-friendly with momentum scrolling
30
-
31
- ## Sandbox & Documentation
32
-
33
- Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
34
-
35
- **30+ examples** with multi-framework implementations (JavaScript, React, Vue, Svelte)
10
+ - **Zero dependencies** — no external libraries
11
+ - **Ultra memory efficient** — ~0.1-0.2 MB constant overhead regardless of dataset size
12
+ - **8–12 KB gzipped** — pay only for features you use (vs 20 KB+ monolithic alternatives)
13
+ - **Builder API** — composable plugins with perfect tree-shaking
14
+ - **Grid, sections, async, selection, scale** — all opt-in
15
+ - **Horizontal, reverse, page-scroll, wrap** — every layout mode
16
+ - **Accessible** — WAI-ARIA, keyboard navigation, screen-reader friendly
17
+ - **React, Vue, Svelte** — framework adapters available
18
+
19
+ **30+ interactive examples → [vlist.dev](https://vlist.dev)**
36
20
 
37
21
  ## Installation
38
22
 
@@ -40,13 +24,11 @@ Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
40
24
  npm install @floor/vlist
41
25
  ```
42
26
 
43
- > **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to `vlist`.
44
-
45
27
  ## Quick Start
46
28
 
47
29
  ```typescript
48
- import { vlist } from 'vlist';
49
- import 'vlist/styles';
30
+ import { vlist } from '@floor/vlist'
31
+ import '@floor/vlist/styles'
50
32
 
51
33
  const list = vlist({
52
34
  container: '#my-list',
@@ -59,87 +41,57 @@ const list = vlist({
59
41
  height: 48,
60
42
  template: (item) => `<div>${item.name}</div>`,
61
43
  },
62
- }).build();
44
+ }).build()
63
45
 
64
- // API methods
65
- list.scrollToIndex(10);
66
- list.setItems(newItems);
67
- list.on('item:click', ({ item }) => console.log(item));
46
+ list.scrollToIndex(10)
47
+ list.setItems(newItems)
48
+ list.on('item:click', ({ item }) => console.log(item))
68
49
  ```
69
50
 
70
- **Bundle:** ~8 KB gzipped
71
-
72
51
  ## Builder Pattern
73
52
 
74
- VList uses a composable builder pattern. Start with the base, add only the features you need:
53
+ Start with the base, add only what you need:
75
54
 
76
55
  ```typescript
77
- import { vlist, withGrid, withSections, withSelection } from 'vlist';
56
+ import { vlist, withGrid, withSections, withSelection } from '@floor/vlist'
78
57
 
79
58
  const list = vlist({
80
59
  container: '#app',
81
60
  items: photos,
82
- item: {
83
- height: 200,
84
- template: renderPhoto,
85
- },
61
+ item: { height: 200, template: renderPhoto },
86
62
  })
87
63
  .use(withGrid({ columns: 4, gap: 16 }))
88
- .use(withSections({
64
+ .use(withSections({
89
65
  getGroupForIndex: (i) => photos[i].category,
90
66
  headerHeight: 40,
91
67
  headerTemplate: (cat) => `<h2>${cat}</h2>`,
92
68
  }))
93
69
  .use(withSelection({ mode: 'multiple' }))
94
- .build();
70
+ .build()
95
71
  ```
96
72
 
97
- **Bundle:** ~12 KB gzipped (only includes used plugins)
73
+ ### Plugins
98
74
 
99
- ### Available Plugins
100
-
101
- | Plugin | Cost | Description |
75
+ | Plugin | Size | Description |
102
76
  |--------|------|-------------|
103
- | **Base** | 7.7 KB gzip | Core virtualization, no plugins |
77
+ | **Base** | 7.7 KB | Core virtualization |
104
78
  | `withGrid()` | +4.0 KB | 2D grid layout |
105
79
  | `withSections()` | +4.6 KB | Grouped lists with sticky/inline headers |
106
- | `withAsync()` | +5.3 KB | Async data loading with adapters |
107
- | `withSelection()` | +2.3 KB | Single/multiple item selection |
108
- | `withScale()` | +2.2 KB | Handle 1M+ items with compression |
80
+ | `withAsync()` | +5.3 KB | Lazy loading with adapters |
81
+ | `withSelection()` | +2.3 KB | Single/multiple selection + keyboard nav |
82
+ | `withScale()` | +2.2 KB | 1M+ items via scroll compression |
109
83
  | `withScrollbar()` | +1.0 KB | Custom scrollbar UI |
110
84
  | `withPage()` | +0.9 KB | Document-level scrolling |
111
- | `withSnapshots()` | Included | Scroll save/restore |
112
-
113
- **Compare to monolithic:** Traditional virtual lists bundle everything = 20-23 KB gzipped minimum, regardless of usage.
85
+ | `withSnapshots()` | included | Scroll save/restore |
114
86
 
115
87
  ## Examples
116
88
 
117
- ### Simple List (No Plugins)
118
-
119
- ```typescript
120
- import { vlist } from 'vlist';
121
-
122
- const list = vlist({
123
- container: '#list',
124
- items: users,
125
- item: {
126
- height: 64,
127
- template: (user) => `
128
- <div class="user">
129
- <img src="${user.avatar}" />
130
- <span>${user.name}</span>
131
- </div>
132
- `,
133
- },
134
- }).build();
135
- ```
136
-
137
- **Bundle:** 8.2 KB gzipped
89
+ More examples at **[vlist.dev](https://vlist.dev)**.
138
90
 
139
91
  ### Grid Layout
140
92
 
141
93
  ```typescript
142
- import { vlist, withGrid, withScrollbar } from 'vlist';
94
+ import { vlist, withGrid, withScrollbar } from '@floor/vlist'
143
95
 
144
96
  const gallery = vlist({
145
97
  container: '#gallery',
@@ -156,887 +108,262 @@ const gallery = vlist({
156
108
  })
157
109
  .use(withGrid({ columns: 4, gap: 16 }))
158
110
  .use(withScrollbar({ autoHide: true }))
159
- .build();
111
+ .build()
160
112
  ```
161
113
 
162
- **Bundle:** 11.7 KB gzipped
163
-
164
- Grid mode virtualizes by **rows** - only visible rows are in the DOM. Each item is positioned with `translate(x, y)` for GPU-accelerated rendering.
165
-
166
- ### Sticky Headers (Contact List)
114
+ ### Sticky Headers
167
115
 
168
116
  ```typescript
169
- import { vlist, withSections } from 'vlist';
117
+ import { vlist, withSections } from '@floor/vlist'
170
118
 
171
119
  const contacts = vlist({
172
120
  container: '#contacts',
173
- items: sortedContacts, // Must be pre-sorted by group
121
+ items: sortedContacts,
174
122
  item: {
175
123
  height: 56,
176
124
  template: (contact) => `<div>${contact.name}</div>`,
177
125
  },
178
126
  })
179
127
  .use(withSections({
180
- getGroupForIndex: (i) => contacts[i].lastName[0].toUpperCase(),
128
+ getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
181
129
  headerHeight: 36,
182
130
  headerTemplate: (letter) => `<div class="header">${letter}</div>`,
183
- sticky: true, // Headers stick to top (Telegram style)
131
+ sticky: true,
184
132
  }))
185
- .build();
133
+ .build()
186
134
  ```
187
135
 
188
- **Bundle:** 12.3 KB gzipped
189
-
190
136
  Set `sticky: false` for inline headers (iMessage/WhatsApp style).
191
137
 
192
- ### Chat UI (Reverse + Sections)
193
-
194
- ```typescript
195
- import { vlist, withSections } from 'vlist';
196
-
197
- const chat = vlist({
198
- container: '#messages',
199
- reverse: true, // Start at bottom, newest messages visible
200
- items: messages, // Chronological order (oldest first)
201
- item: {
202
- height: (i) => messages[i].height || 60,
203
- template: (msg) => `<div class="message">${msg.text}</div>`,
204
- },
205
- })
206
- .use(withSections({
207
- getGroupForIndex: (i) => {
208
- const date = new Date(messages[i].timestamp);
209
- return date.toLocaleDateString(); // "Jan 15", "Jan 16", etc.
210
- },
211
- headerHeight: 32,
212
- headerTemplate: (date) => `<div class="date-header">${date}</div>`,
213
- sticky: false, // Inline date headers (iMessage style)
214
- }))
215
- .build();
216
-
217
- // New messages - auto-scrolls to bottom
218
- chat.appendItems([newMessage]);
219
-
220
- // Load history - preserves scroll position
221
- chat.prependItems(olderMessages);
222
- ```
223
-
224
- **Bundle:** 11.9 KB gzipped
225
-
226
- Perfect for iMessage, WhatsApp, Telegram-style chat interfaces.
227
-
228
- ### Large Datasets (1M+ Items)
229
-
230
- ```typescript
231
- import { vlist, withScale, withScrollbar } from 'vlist';
232
-
233
- const bigList = vlist({
234
- container: '#big-list',
235
- items: generateItems(5_000_000),
236
- item: {
237
- height: 48,
238
- template: (item) => `<div>#${item.id}: ${item.name}</div>`,
239
- },
240
- })
241
- .use(withScale()) // Auto-activates when height > 16.7M pixels
242
- .use(withScrollbar({ autoHide: true }))
243
- .build();
244
- ```
245
-
246
- **Bundle:** 9.9 KB gzipped
247
-
248
- The scale plugin automatically compresses scroll space when total height exceeds browser limits (~16.7M pixels), enabling smooth scrolling through millions of items.
249
-
250
- ### Async Loading with Pagination
138
+ ### Async Loading
251
139
 
252
140
  ```typescript
253
- import { vlist, withAsync } from 'vlist';
141
+ import { vlist, withAsync } from '@floor/vlist'
254
142
 
255
143
  const list = vlist({
256
144
  container: '#list',
257
145
  item: {
258
146
  height: 64,
259
- template: (item) => {
260
- if (!item) return `<div class="loading">Loading...</div>`;
261
- return `<div>${item.name}</div>`;
262
- },
147
+ template: (item) => item
148
+ ? `<div>${item.name}</div>`
149
+ : `<div class="placeholder">Loadingâ€Ķ</div>`,
263
150
  },
264
151
  })
265
152
  .use(withAsync({
266
153
  adapter: {
267
154
  read: async ({ offset, limit }) => {
268
- const response = await fetch(`/api/users?offset=${offset}&limit=${limit}`);
269
- const data = await response.json();
270
- return {
271
- items: data.items,
272
- total: data.total,
273
- hasMore: data.hasMore,
274
- };
155
+ const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
156
+ const data = await res.json()
157
+ return { items: data.items, total: data.total, hasMore: data.hasMore }
275
158
  },
276
159
  },
277
- loading: {
278
- cancelThreshold: 15, // Cancel loads when scrolling fast (pixels/ms)
279
- },
280
160
  }))
281
- .build();
282
- ```
283
-
284
- **Bundle:** 13.5 KB gzipped
285
-
286
- The async plugin shows placeholders for unloaded items and fetches data as you scroll. Velocity-aware loading cancels requests when scrolling fast.
287
-
288
- ### Page-Level Scrolling
289
-
290
- ```typescript
291
- import { vlist, withPage } from 'vlist';
292
-
293
- const list = vlist({
294
- container: '#list',
295
- items: articles,
296
- item: {
297
- height: 200,
298
- template: (article) => `<article>...</article>`,
299
- },
300
- })
301
- .use(withPage()) // Uses document scroll instead of container
302
- .build();
303
- ```
304
-
305
- **Bundle:** 8.6 KB gzipped
306
-
307
- Perfect for blog posts, infinite scroll feeds, and full-page lists.
308
-
309
- ### Horizontal Carousel
310
-
311
- ```typescript
312
- import { vlist } from 'vlist';
313
-
314
- const carousel = vlist({
315
- container: '#carousel',
316
- direction: 'horizontal',
317
- items: cards,
318
- item: {
319
- width: 300, // Required for horizontal
320
- height: 400, // Optional (can use CSS)
321
- template: (card) => `<div class="card">...</div>`,
322
- },
323
- scroll: {
324
- wrap: true, // Circular scrolling
325
- },
326
- }).build();
161
+ .build()
327
162
  ```
328
163
 
329
- **Bundle:** 8.6 KB gzipped
330
-
331
- ### Selection & Navigation
332
-
333
- ```typescript
334
- import { vlist, withSelection } from 'vlist';
164
+ ### More Patterns
335
165
 
336
- const list = vlist({
337
- container: '#list',
338
- items: users,
339
- item: {
340
- height: 48,
341
- template: (user, index, { selected }) => {
342
- const cls = selected ? 'item--selected' : '';
343
- return `<div class="${cls}">${user.name}</div>`;
344
- },
345
- },
346
- })
347
- .use(withSelection({
348
- mode: 'multiple',
349
- initial: [1, 5, 10], // Pre-select items
350
- }))
351
- .build();
352
-
353
- // Selection API
354
- list.selectItem(5);
355
- list.deselectItem(5);
356
- list.toggleSelection(5);
357
- list.getSelectedIds(); // [1, 5, 10]
358
- list.clearSelection();
359
- ```
166
+ | Pattern | Key options |
167
+ |---------|------------|
168
+ | **Chat UI** | `reverse: true` + `withSections({ sticky: false })` |
169
+ | **Horizontal carousel** | `direction: 'horizontal'`, `item.width` |
170
+ | **Page-level scroll** | `withPage()` |
171
+ | **1M+ items** | `withScale()` — auto-compresses scroll space |
172
+ | **Wrap navigation** | `scroll: { wrap: true }` |
173
+ | **Variable heights** | `item: { height: (index) => heights[index] }` |
360
174
 
361
- **Bundle:** 10.0 KB gzipped
175
+ See **[vlist.dev](https://vlist.dev)** for live demos of each.
362
176
 
363
- Supports `mode: 'single'` or `'multiple'` with keyboard navigation (Arrow keys, Home, End, Space, Enter).
364
-
365
- ### Variable Heights (Chat Messages)
177
+ ## API
366
178
 
367
179
  ```typescript
368
- import { vlist } from 'vlist';
369
-
370
- const list = vlist({
371
- container: '#messages',
372
- items: messages,
373
- item: {
374
- height: (index) => {
375
- // Heights computed from actual DOM measurements
376
- return messages[index].measuredHeight || 60;
377
- },
378
- template: (msg) => `
379
- <div class="message">
380
- <div class="author">${msg.user}</div>
381
- <div class="text">${msg.text}</div>
382
- </div>
383
- `,
384
- },
385
- }).build();
180
+ const list = vlist(config).use(...plugins).build()
386
181
  ```
387
182
 
388
- **Bundle:** 10.9 KB gzipped
183
+ ### Data
389
184
 
390
- Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search.
185
+ | Method | Description |
186
+ |--------|-------------|
187
+ | `list.setItems(items)` | Replace all items |
188
+ | `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
189
+ | `list.prependItems(items)` | Add to start (preserves scroll position) |
190
+ | `list.updateItem(index, partial)` | Update a single item by index |
191
+ | `list.removeItem(index)` | Remove by index |
192
+ | `list.reload()` | Re-fetch from adapter (async) |
391
193
 
392
- ## API Reference
194
+ ### Navigation
393
195
 
394
- ### Core Methods
196
+ | Method | Description |
197
+ |--------|-------------|
198
+ | `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \| `'center'` \| `'end'`) |
199
+ | `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |
200
+ | `list.cancelScroll()` | Cancel smooth scroll animation |
201
+ | `list.getScrollPosition()` | Current scroll offset |
202
+ | `list.getVisibleRange()` | `{ start, end }` of visible indices |
203
+ | `list.getScrollSnapshot()` | Save scroll state (for SPA navigation) |
204
+ | `list.restoreScroll(snapshot)` | Restore saved scroll state |
395
205
 
396
- ```typescript
397
- const list = vlist(config).use(...plugins).build();
398
-
399
- // Data manipulation
400
- list.setItems(items: T[]): void
401
- list.appendItems(items: T[]): void
402
- list.prependItems(items: T[]): void
403
- list.updateItem(id: string | number, item: Partial<T>): boolean
404
- list.removeItem(id: string | number): boolean
405
-
406
- // Navigation
407
- list.scrollToIndex(index: number, align?: 'start' | 'center' | 'end'): void
408
- list.scrollToIndex(index: number, options?: {
409
- align?: 'start' | 'center' | 'end',
410
- behavior?: 'auto' | 'smooth',
411
- duration?: number
412
- }): void
413
- list.scrollToItem(id: string | number, align?: string): void
414
-
415
- // State
416
- list.getScrollPosition(): number
417
- list.getVisibleRange(): { start: number, end: number }
418
- list.getScrollSnapshot(): ScrollSnapshot
419
- list.restoreScroll(snapshot: ScrollSnapshot): void
420
-
421
- // Events
422
- list.on(event: string, handler: Function): Unsubscribe
423
- list.off(event: string, handler: Function): void
424
-
425
- // Lifecycle
426
- list.destroy(): void
427
-
428
- // Properties
429
- list.element: HTMLElement
430
- list.items: readonly T[]
431
- list.total: number
432
- ```
433
-
434
- ### Selection Methods (with `withSelection()`)
206
+ ### Selection (with `withSelection()`)
435
207
 
436
- ```typescript
437
- list.selectItem(id: string | number): void
438
- list.deselectItem(id: string | number): void
439
- list.toggleSelection(id: string | number): void
440
- list.selectAll(): void
441
- list.clearSelection(): void
442
- list.getSelectedIds(): Array<string | number>
443
- list.getSelectedItems(): T[]
444
- list.setSelectionMode(mode: 'none' | 'single' | 'multiple'): void
445
- ```
208
+ | Method | Description |
209
+ |--------|-------------|
210
+ | `list.selectItem(id)` | Select item |
211
+ | `list.deselectItem(id)` | Deselect item |
212
+ | `list.toggleSelection(id)` | Toggle |
213
+ | `list.selectAll()` / `list.clearSelection()` | Bulk operations |
214
+ | `list.getSelectedIds()` | Array of selected IDs |
215
+ | `list.getSelectedItems()` | Array of selected items |
446
216
 
447
- ### Grid Methods (with `withGrid()`)
217
+ ### Grid (with `withGrid()`)
448
218
 
449
- ```typescript
450
- list.updateGrid(config: { columns?: number, gap?: number }): void
451
- ```
219
+ | Method | Description |
220
+ |--------|-------------|
221
+ | `list.updateGrid({ columns, gap })` | Update grid at runtime |
452
222
 
453
223
  ### Events
454
224
 
455
- ```typescript
456
- list.on('scroll', ({ scrollTop, direction }) => { })
457
- list.on('range:change', ({ range }) => { })
458
- list.on('item:click', ({ item, index, event }) => { })
459
- list.on('item:dblclick', ({ item, index, event }) => { })
460
- list.on('selection:change', ({ selectedIds, selectedItems }) => { })
461
- list.on('load:start', ({ offset, limit }) => { })
462
- list.on('load:end', ({ items, offset, total }) => { })
463
- list.on('load:error', ({ error, offset, limit }) => { })
464
- list.on('velocity:change', ({ velocity, reliable }) => { })
465
- ```
466
-
467
- ## Configuration
468
-
469
- ### Base Configuration
470
-
471
- ```typescript
472
- interface VListConfig<T> {
473
- // Required
474
- container: HTMLElement | string;
475
- item: {
476
- height?: number | ((index: number) => number); // Required for vertical
477
- width?: number | ((index: number) => number); // Required for horizontal
478
- template: (item: T, index: number, state: ItemState) => string | HTMLElement;
479
- };
480
-
481
- // Optional
482
- items?: T[]; // Initial items
483
- overscan?: number; // Extra items to render (default: 3)
484
- direction?: 'vertical' | 'horizontal'; // Default: 'vertical'
485
- reverse?: boolean; // Reverse mode for chat (default: false)
486
- classPrefix?: string; // CSS class prefix (default: 'vlist')
487
- ariaLabel?: string; // Accessible label
488
-
489
- scroll?: {
490
- wheel?: boolean; // Enable mouse wheel (default: true)
491
- wrap?: boolean; // Circular scrolling (default: false)
492
- scrollbar?: 'none'; // Hide scrollbar
493
- idleTimeout?: number; // Scroll idle detection (default: 150ms)
494
- };
495
- }
496
- ```
497
-
498
- ### Plugin: `withGrid(config)`
499
-
500
- 2D grid layout with virtualized rows.
501
-
502
- ```typescript
503
- interface GridConfig {
504
- columns: number; // Number of columns (required)
505
- gap?: number; // Gap between items in pixels (default: 0)
506
- }
507
- ```
508
-
509
- **Example:**
510
- ```typescript
511
- .use(withGrid({ columns: 4, gap: 16 }))
512
- ```
513
-
514
- ### Plugin: `withSections(config)`
515
-
516
- Grouped lists with sticky or inline headers.
517
-
518
- ```typescript
519
- interface SectionsConfig {
520
- getGroupForIndex: (index: number) => string;
521
- headerHeight: number | ((group: string, groupIndex: number) => number);
522
- headerTemplate: (group: string, groupIndex: number) => string | HTMLElement;
523
- sticky?: boolean; // Default: true (Telegram style), false = inline (iMessage style)
524
- }
525
- ```
526
-
527
- **Example:**
528
- ```typescript
529
- .use(withSections({
530
- getGroupForIndex: (i) => items[i].category,
531
- headerHeight: 40,
532
- headerTemplate: (cat) => `<h2>${cat}</h2>`,
533
- sticky: true,
534
- }))
535
- ```
536
-
537
- **Important:** Items must be pre-sorted by group!
538
-
539
- ### Plugin: `withSelection(config)`
540
-
541
- Single or multiple item selection with keyboard navigation.
542
-
543
- ```typescript
544
- interface SelectionConfig {
545
- mode: 'single' | 'multiple';
546
- initial?: Array<string | number>; // Pre-selected item IDs
547
- }
548
- ```
549
-
550
- **Example:**
551
- ```typescript
552
- .use(withSelection({
553
- mode: 'multiple',
554
- initial: [1, 5, 10],
555
- }))
556
- ```
557
-
558
- ### Plugin: `withAsync(config)`
559
-
560
- Asynchronous data loading with lazy loading and placeholders.
561
-
562
- ```typescript
563
- interface AsyncConfig {
564
- adapter: {
565
- read: (params: { offset: number, limit: number }) => Promise<{
566
- items: T[];
567
- total?: number;
568
- hasMore?: boolean;
569
- }>;
570
- };
571
- loading?: {
572
- cancelThreshold?: number; // Cancel loads when scrolling fast (px/ms)
573
- };
574
- }
575
- ```
576
-
577
- **Example:**
578
- ```typescript
579
- .use(withAsync({
580
- adapter: {
581
- read: async ({ offset, limit }) => {
582
- const response = await fetch(`/api?offset=${offset}&limit=${limit}`);
583
- return response.json();
584
- },
585
- },
586
- loading: {
587
- cancelThreshold: 15, // Skip loads when scrolling > 15px/ms
588
- },
589
- }))
590
- ```
591
-
592
- ### Plugin: `withScale()`
593
-
594
- Automatically handles lists with 1M+ items by compressing scroll space when total height exceeds browser limits (~16.7M pixels).
595
-
596
- ```typescript
597
- .use(withScale()) // No config needed - auto-activates
598
- ```
599
-
600
- **Example:**
601
- ```typescript
602
- const bigList = vlist({
603
- container: '#list',
604
- items: generateItems(5_000_000),
605
- item: { height: 48, template: renderItem },
606
- })
607
- .use(withScale())
608
- .build();
609
- ```
610
-
611
- ### Plugin: `withScrollbar(config)`
612
-
613
- Custom scrollbar with auto-hide and smooth dragging.
614
-
615
- ```typescript
616
- interface ScrollbarConfig {
617
- autoHide?: boolean; // Hide when not scrolling (default: false)
618
- autoHideDelay?: number; // Hide delay in ms (default: 1000)
619
- minThumbSize?: number; // Minimum thumb size in px (default: 20)
620
- }
621
- ```
622
-
623
- **Example:**
624
- ```typescript
625
- .use(withScrollbar({
626
- autoHide: true,
627
- autoHideDelay: 1500,
628
- }))
629
- ```
630
-
631
- ### Plugin: `withPage()`
632
-
633
- Use document-level scrolling instead of container scrolling.
634
-
635
- ```typescript
636
- .use(withPage()) // No config needed
637
- ```
638
-
639
- **Example:**
640
- ```typescript
641
- const list = vlist({
642
- container: '#list',
643
- items: articles,
644
- item: { height: 300, template: renderArticle },
645
- })
646
- .use(withPage())
647
- .build();
648
- ```
649
-
650
- Perfect for blog posts, infinite scroll feeds, and full-page lists.
651
-
652
- ### Plugin: `withSnapshots()`
653
-
654
- Scroll position save/restore for SPA navigation.
225
+ `list.on()` returns an unsubscribe function. You can also use `list.off(event, handler)`.
655
226
 
656
227
  ```typescript
657
- // Included in base - no need to import
658
- const snapshot = list.getScrollSnapshot();
659
- list.restoreScroll(snapshot);
228
+ list.on('scroll', ({ scrollTop, direction }) => {})
229
+ list.on('range:change', ({ range }) => {})
230
+ list.on('item:click', ({ item, index, event }) => {})
231
+ list.on('item:dblclick', ({ item, index, event }) => {})
232
+ list.on('selection:change', ({ selectedIds, selectedItems }) => {})
233
+ list.on('load:start', ({ offset, limit }) => {})
234
+ list.on('load:end', ({ items, offset, total }) => {})
235
+ list.on('load:error', ({ error, offset, limit }) => {})
236
+ list.on('velocity:change', ({ velocity, reliable }) => {})
660
237
  ```
661
238
 
662
- **Example:**
663
- ```typescript
664
- // Save on navigation away
665
- const snapshot = list.getScrollSnapshot();
666
- sessionStorage.setItem('scroll', JSON.stringify(snapshot));
239
+ ### Properties
667
240
 
668
- // Restore on return
669
- const snapshot = JSON.parse(sessionStorage.getItem('scroll'));
670
- list.restoreScroll(snapshot);
671
- ```
672
-
673
- ## Advanced Usage
241
+ | Property | Description |
242
+ |----------|-------------|
243
+ | `list.element` | Root DOM element |
244
+ | `list.items` | Current items (readonly) |
245
+ | `list.total` | Total item count |
674
246
 
675
- ### Variable Heights with DOM Measurement
247
+ ### Lifecycle
676
248
 
677
249
  ```typescript
678
- import { vlist } from 'vlist';
679
-
680
- // Measure items before creating list
681
- const measuringDiv = document.createElement('div');
682
- measuringDiv.style.cssText = 'position:absolute;visibility:hidden;width:400px';
683
- document.body.appendChild(measuringDiv);
684
-
685
- items.forEach(item => {
686
- measuringDiv.innerHTML = renderItem(item);
687
- item.measuredHeight = measuringDiv.offsetHeight;
688
- });
689
-
690
- document.body.removeChild(measuringDiv);
691
-
692
- // Use measured heights
693
- const list = vlist({
694
- container: '#list',
695
- items,
696
- item: {
697
- height: (i) => items[i].measuredHeight,
698
- template: renderItem,
699
- },
700
- }).build();
250
+ list.destroy()
701
251
  ```
702
252
 
703
- ### Dynamic Grid Columns
704
-
705
- ```typescript
706
- import { vlist, withGrid } from 'vlist';
253
+ ## Plugin Configuration
707
254
 
708
- let currentColumns = 4;
709
-
710
- const list = vlist({
711
- container: '#gallery',
712
- items: photos,
713
- item: {
714
- height: 200,
715
- template: renderPhoto,
716
- },
717
- })
718
- .use(withGrid({ columns: currentColumns, gap: 16 }))
719
- .build();
720
-
721
- // Update grid on resize
722
- window.addEventListener('resize', () => {
723
- const width = container.clientWidth;
724
- const newColumns = width > 1200 ? 6 : width > 800 ? 4 : 2;
725
- if (newColumns !== currentColumns) {
726
- currentColumns = newColumns;
727
- list.updateGrid({ columns: newColumns });
728
- }
729
- });
730
- ```
731
-
732
- ### Combining Multiple Plugins
255
+ Each plugin's config is fully typed — hover in your IDE for details.
733
256
 
734
257
  ```typescript
735
- import {
736
- vlist,
737
- withGrid,
738
- withSections,
739
- withSelection,
740
- withAsync,
741
- withScrollbar
742
- } from 'vlist';
743
-
744
- const list = vlist({
745
- container: '#gallery',
746
- item: {
747
- height: 200,
748
- template: renderPhoto,
749
- },
750
- })
751
- .use(withAsync({ adapter: photoAdapter }))
752
- .use(withGrid({ columns: 4, gap: 16 }))
753
- .use(withSections({
754
- getGroupForIndex: (i) => items[i]?.category || 'Loading...',
755
- headerHeight: 48,
756
- headerTemplate: (cat) => `<h2>${cat}</h2>`,
757
- }))
758
- .use(withSelection({ mode: 'multiple' }))
759
- .use(withScrollbar({ autoHide: true }))
760
- .build();
258
+ withGrid({ columns: 4, gap: 16 })
259
+ withSections({ getGroupForIndex, headerHeight, headerTemplate, sticky?: true })
260
+ withSelection({ mode: 'single' | 'multiple', initial?: [...ids] })
261
+ withAsync({ adapter: { read }, loading?: { cancelThreshold? } })
262
+ withScale() // no config — auto-activates at 16.7M px
263
+ withScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
264
+ withPage() // no config — uses document scroll
265
+ withSnapshots() // included by default
761
266
  ```
762
267
 
763
- **Bundle:** ~15 KB gzipped (includes only used plugins)
268
+ Full configuration reference → **[vlist.dev](https://vlist.dev)**
764
269
 
765
270
  ## Framework Adapters
766
271
 
767
- Framework-specific adapters are available as separate packages for easier integration and smaller bundle sizes.
768
-
769
- ### React
770
-
771
- **Package:** [`vlist-react`](https://github.com/floor/vlist-react) - 1.4 KB (0.6 KB gzipped)
772
-
773
- ```bash
774
- npm install @floor/vlist vlist-react
775
- ```
776
-
777
- ```tsx
778
- import { useVList } from 'vlist-react';
779
- import '@floor/vlist/styles';
780
-
781
- function UserList({ users }) {
782
- const { containerRef, instanceRef } = useVList({
783
- item: {
784
- height: 48,
785
- template: (user) => `<div class="user">${user.name}</div>`,
786
- },
787
- items: users,
788
- });
789
-
790
- return <div ref={containerRef} style={{ height: 400 }} />;
791
- }
792
- ```
793
-
794
- **Documentation:** [github.com/floor/vlist-react](https://github.com/floor/vlist-react)
795
-
796
- ### Vue
797
-
798
- **Package:** [`vlist-vue`](https://github.com/floor/vlist-vue) - 1.1 KB (0.6 KB gzipped)
272
+ | Framework | Package | Size |
273
+ |-----------|---------|------|
274
+ | React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |
275
+ | Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |
276
+ | Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |
799
277
 
800
278
  ```bash
801
- npm install @floor/vlist vlist-vue
802
- ```
803
-
804
- ```vue
805
- <script setup>
806
- import { useVList } from 'vlist-vue';
807
- import '@floor/vlist/styles';
808
-
809
- const users = ref([...]);
810
-
811
- const { containerRef, instance } = useVList({
812
- item: {
813
- height: 48,
814
- template: (user) => `<div class="user">${user.name}</div>`,
815
- },
816
- items: users,
817
- });
818
- </script>
819
-
820
- <template>
821
- <div ref="containerRef" style="height: 400px" />
822
- </template>
823
- ```
824
-
825
- **Documentation:** [github.com/floor/vlist-vue](https://github.com/floor/vlist-vue)
826
-
827
- ### Svelte
828
-
829
- **Package:** [`vlist-svelte`](https://github.com/floor/vlist-svelte) - 0.9 KB (0.5 KB gzipped)
830
-
831
- ```bash
832
- npm install @floor/vlist vlist-svelte
833
- ```
834
-
835
- ```svelte
836
- <script>
837
- import { vlist } from 'vlist-svelte';
838
- import '@floor/vlist/styles';
839
-
840
- let users = [...];
841
- let instance;
842
-
843
- const config = {
844
- item: {
845
- height: 48,
846
- template: (user) => `<div class="user">${user.name}</div>`,
847
- },
848
- items: users,
849
- };
850
- </script>
851
-
852
- <div
853
- use:vlist={{ config, onInstance: (i) => (instance = i) }}
854
- style="height: 400px"
855
- />
279
+ npm install @floor/vlist vlist-react # or vlist-vue / vlist-svelte
856
280
  ```
857
281
 
858
- **Documentation:** [github.com/floor/vlist-svelte](https://github.com/floor/vlist-svelte)
282
+ Each adapter README has setup examples and API docs.
859
283
 
860
284
  ## Styling
861
285
 
862
- Import the base styles:
863
-
864
286
  ```typescript
865
- import 'vlist/styles';
287
+ import '@floor/vlist/styles' // base styles (required)
288
+ import '@floor/vlist/styles/extras' // optional enhancements
866
289
  ```
867
290
 
868
- Or customize with your own CSS:
869
-
870
- ```css
871
- .vlist {
872
- /* Container styles */
873
- }
874
-
875
- .vlist-viewport {
876
- /* Scroll viewport */
877
- }
878
-
879
- .vlist-item {
880
- /* Item wrapper - positioned absolutely */
881
- }
882
-
883
- .vlist-item--selected {
884
- /* Selected state */
885
- }
886
-
887
- .vlist-item--focused {
888
- /* Focused state (keyboard navigation) */
889
- }
890
-
891
- .vlist-scrollbar {
892
- /* Custom scrollbar track */
893
- }
894
-
895
- .vlist-scrollbar__thumb {
896
- /* Scrollbar thumb/handle */
897
- }
898
- ```
291
+ Override with your own CSS using the `.vlist`, `.vlist-item`, `.vlist-item--selected`, `.vlist-scrollbar` selectors. See [vlist.dev](https://vlist.dev) for theming examples.
899
292
 
900
293
  ## Performance
901
294
 
902
- ### Bundle Sizes (Gzipped)
903
-
904
- | Configuration | Size | What's Included |
905
- |---------------|------|-----------------|
906
- | Base only | 7.7 KB | Core virtualization |
907
- | + Grid | 11.7 KB | + 2D layout |
908
- | + Sections | 12.3 KB | + Grouped lists |
909
- | + Selection | 10.0 KB | + Item selection |
910
- | + Async | 13.5 KB | + Data loading |
911
- | + Scale | 9.9 KB | + 1M+ items |
912
- | + All plugins | ~16 KB | Everything |
295
+ ### Bundle Size
913
296
 
914
- **Traditional virtual lists:** 20-23 KB minimum (all features bundled regardless of usage)
297
+ | Configuration | Gzipped |
298
+ |---------------|---------|
299
+ | Base only | 7.7 KB |
300
+ | + Grid | 11.7 KB |
301
+ | + Sections | 12.3 KB |
302
+ | + Async | 13.5 KB |
303
+ | All plugins | ~16 KB |
915
304
 
916
305
  ### Memory Efficiency
917
306
 
918
- With 100,000 items at 48px each:
919
- - **Total height:** 4,800,000 pixels
920
- - **Visible items:** ~20 (depending on viewport height)
921
- - **DOM nodes:** ~26 (visible + overscan)
922
- - **Memory saved:** ~99.97% (26 DOM nodes vs 100,000)
307
+ vlist uses **constant memory** regardless of dataset size through optimized internal architecture:
923
308
 
924
- ### Benchmarks
309
+ | Dataset Size | Memory Usage | Notes |
310
+ |--------------|--------------|-------|
311
+ | 10K items | ~0.2 MB | Constant baseline |
312
+ | 100K items | ~0.2 MB | 10× items, same memory |
313
+ | 1M items | ~0.4 MB | 100× items, 2× memory |
925
314
 
926
- See [benchmarks](https://vlist.dev/benchmarks/) for detailed performance comparisons.
315
+ **Key advantages:**
316
+ - No array copying — uses references for zero-copy performance
317
+ - No ID indexing overhead — O(1) memory complexity
318
+ - Industry-leading memory efficiency for virtual list libraries
927
319
 
928
- ## Browser Support
320
+ ### DOM Efficiency
929
321
 
930
- - Chrome/Edge: Latest 2 versions
931
- - Firefox: Latest 2 versions
932
- - Safari: Latest 2 versions
933
- - iOS Safari: 12.4+
934
- - Chrome Android: Latest
322
+ With 100K items: **~26 DOM nodes** in the document (visible + overscan) instead of 100,000.
935
323
 
936
- Relies on:
937
- - `IntersectionObserver` (widely supported)
938
- - `ResizeObserver` (polyfill available if needed)
939
- - CSS `transform` (universal support)
324
+ ### Render Performance
325
+
326
+ - **Initial render:** ~8ms (constant, regardless of item count)
327
+ - **Scroll performance:** 120 FPS (perfect smoothness)
328
+ - **1M items:** Same performance as 10K items
940
329
 
941
330
  ## TypeScript
942
331
 
943
- Fully typed with comprehensive TypeScript definitions:
332
+ Fully typed. Generic over your item type:
944
333
 
945
334
  ```typescript
946
- import { vlist, withGrid, type VList, type VListConfig } from 'vlist';
335
+ import { vlist, withGrid, type VList } from '@floor/vlist'
947
336
 
948
- interface Photo {
949
- id: number;
950
- url: string;
951
- title: string;
952
- }
337
+ interface Photo { id: number; url: string; title: string }
953
338
 
954
- const config: VListConfig<Photo> = {
339
+ const list: VList<Photo> = vlist<Photo>({
955
340
  container: '#gallery',
956
341
  items: photos,
957
342
  item: {
958
343
  height: 200,
959
- template: (photo: Photo) => `<img src="${photo.url}" />`,
344
+ template: (photo) => `<img src="${photo.url}" />`,
960
345
  },
961
- };
962
-
963
- const list: VList<Photo> = vlist(config)
964
- .use(withGrid({ columns: 4 }))
965
- .build();
966
- ```
967
-
968
- ## Migration from Monolithic API
969
-
970
- If you're using the old monolithic API:
971
-
972
- ### Before (Monolithic - Deprecated)
973
-
974
- ```typescript
975
- import { createVList } from 'vlist';
976
-
977
- const list = createVList({
978
- container: '#app',
979
- items: data,
980
- grid: { columns: 4 },
981
- groups: { ... },
982
- selection: { mode: 'single' },
983
- });
984
- ```
985
-
986
- ### After (Builder - Recommended)
987
-
988
- ```typescript
989
- import { vlist, withGrid, withSections, withSelection } from 'vlist';
990
-
991
- const list = vlist({
992
- container: '#app',
993
- items: data,
994
346
  })
995
347
  .use(withGrid({ columns: 4 }))
996
- .use(withSections({ ... }))
997
- .use(withSelection({ mode: 'single' }))
998
- .build();
348
+ .build()
999
349
  ```
1000
350
 
1001
- **Benefits:**
1002
- - 2-3x smaller bundles (20 KB → 8-12 KB gzipped)
1003
- - Explicit about what's included
1004
- - Better tree-shaking
1005
- - Easier to understand and debug
1006
-
1007
- ## Plugin Naming Changes
1008
-
1009
- If you're upgrading from an earlier version:
1010
-
1011
- | Old Name | New Name | Import |
1012
- |----------|----------|--------|
1013
- | `withCompression()` | `withScale()` | `import { withScale } from 'vlist'` |
1014
- | `withData()` | `withAsync()` | `import { withAsync } from 'vlist'` |
1015
- | `withWindow()` | `withPage()` | `import { withPage } from 'vlist'` |
1016
- | `withGroups()` | `withSections()` | `import { withSections } from 'vlist'` |
1017
- | `withScroll()` | `withScrollbar()` | `import { withScrollbar } from 'vlist'` |
1018
-
1019
351
  ## Contributing
1020
352
 
1021
- Contributions are welcome! Please:
1022
-
1023
- 1. Fork the repository
1024
- 2. Create a feature branch
1025
- 3. Make your changes
1026
- 4. Add/update tests
1027
- 5. Submit a pull request
353
+ 1. Fork → branch → make changes → add tests → pull request
354
+ 2. Run `bun test` (1739 tests) and `bun run build` before submitting
1028
355
 
1029
356
  ## License
1030
357
 
1031
- MIT License - see [LICENSE](LICENSE) for details
358
+ [MIT](LICENSE)
1032
359
 
1033
360
  ## Links
1034
361
 
1035
- - **Documentation & Examples:** [vlist.dev](https://vlist.dev)
362
+ - **Docs & Examples:** [vlist.dev](https://vlist.dev)
1036
363
  - **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
1037
364
  - **NPM:** [@floor/vlist](https://www.npmjs.com/package/@floor/vlist)
1038
365
  - **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
1039
366
 
1040
367
  ---
1041
368
 
1042
- **Built by [Floor IO](https://floor.io)** with âĪïļ for the web community
369
+ Built by [Floor IO](https://floor.io)