@floor/vlist 0.7.6 → 0.7.7

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
@@ -7,32 +7,15 @@ Lightweight, high-performance virtual list with zero dependencies and optimal tr
7
7
  [![tests](https://img.shields.io/badge/tests-1739%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
+ - **8–12 KB gzipped** — pay only for features you use (vs 20 KB+ monolithic alternatives)
12
+ - **Builder API** — composable plugins with perfect tree-shaking
13
+ - **Grid, sections, async, selection, scale** — all opt-in
14
+ - **Horizontal, reverse, page-scroll, wrap** — every layout mode
15
+ - **Accessible** — WAI-ARIA, keyboard navigation, screen-reader friendly
16
+ - **React, Vue, Svelte** — framework adapters available
17
+
18
+ **30+ interactive examples → [vlist.dev](https://vlist.dev)**
36
19
 
37
20
  ## Installation
38
21
 
@@ -40,13 +23,11 @@ Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
40
23
  npm install @floor/vlist
41
24
  ```
42
25
 
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
26
  ## Quick Start
46
27
 
47
28
  ```typescript
48
- import { vlist } from 'vlist';
49
- import 'vlist/styles';
29
+ import { vlist } from '@floor/vlist'
30
+ import '@floor/vlist/styles'
50
31
 
51
32
  const list = vlist({
52
33
  container: '#my-list',
@@ -59,87 +40,57 @@ const list = vlist({
59
40
  height: 48,
60
41
  template: (item) => `<div>${item.name}</div>`,
61
42
  },
62
- }).build();
43
+ }).build()
63
44
 
64
- // API methods
65
- list.scrollToIndex(10);
66
- list.setItems(newItems);
67
- list.on('item:click', ({ item }) => console.log(item));
45
+ list.scrollToIndex(10)
46
+ list.setItems(newItems)
47
+ list.on('item:click', ({ item }) => console.log(item))
68
48
  ```
69
49
 
70
- **Bundle:** ~8 KB gzipped
71
-
72
50
  ## Builder Pattern
73
51
 
74
- VList uses a composable builder pattern. Start with the base, add only the features you need:
52
+ Start with the base, add only what you need:
75
53
 
76
54
  ```typescript
77
- import { vlist, withGrid, withSections, withSelection } from 'vlist';
55
+ import { vlist, withGrid, withSections, withSelection } from '@floor/vlist'
78
56
 
79
57
  const list = vlist({
80
58
  container: '#app',
81
59
  items: photos,
82
- item: {
83
- height: 200,
84
- template: renderPhoto,
85
- },
60
+ item: { height: 200, template: renderPhoto },
86
61
  })
87
62
  .use(withGrid({ columns: 4, gap: 16 }))
88
- .use(withSections({
63
+ .use(withSections({
89
64
  getGroupForIndex: (i) => photos[i].category,
90
65
  headerHeight: 40,
91
66
  headerTemplate: (cat) => `<h2>${cat}</h2>`,
92
67
  }))
93
68
  .use(withSelection({ mode: 'multiple' }))
94
- .build();
69
+ .build()
95
70
  ```
96
71
 
97
- **Bundle:** ~12 KB gzipped (only includes used plugins)
72
+ ### Plugins
98
73
 
99
- ### Available Plugins
100
-
101
- | Plugin | Cost | Description |
74
+ | Plugin | Size | Description |
102
75
  |--------|------|-------------|
103
- | **Base** | 7.7 KB gzip | Core virtualization, no plugins |
76
+ | **Base** | 7.7 KB | Core virtualization |
104
77
  | `withGrid()` | +4.0 KB | 2D grid layout |
105
78
  | `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 |
79
+ | `withAsync()` | +5.3 KB | Lazy loading with adapters |
80
+ | `withSelection()` | +2.3 KB | Single/multiple selection + keyboard nav |
81
+ | `withScale()` | +2.2 KB | 1M+ items via scroll compression |
109
82
  | `withScrollbar()` | +1.0 KB | Custom scrollbar UI |
110
83
  | `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.
84
+ | `withSnapshots()` | included | Scroll save/restore |
114
85
 
115
86
  ## Examples
116
87
 
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
88
+ More examples at **[vlist.dev](https://vlist.dev)**.
138
89
 
139
90
  ### Grid Layout
140
91
 
141
92
  ```typescript
142
- import { vlist, withGrid, withScrollbar } from 'vlist';
93
+ import { vlist, withGrid, withScrollbar } from '@floor/vlist'
143
94
 
144
95
  const gallery = vlist({
145
96
  container: '#gallery',
@@ -156,887 +107,238 @@ const gallery = vlist({
156
107
  })
157
108
  .use(withGrid({ columns: 4, gap: 16 }))
158
109
  .use(withScrollbar({ autoHide: true }))
159
- .build();
110
+ .build()
160
111
  ```
161
112
 
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)
113
+ ### Sticky Headers
167
114
 
168
115
  ```typescript
169
- import { vlist, withSections } from 'vlist';
116
+ import { vlist, withSections } from '@floor/vlist'
170
117
 
171
118
  const contacts = vlist({
172
119
  container: '#contacts',
173
- items: sortedContacts, // Must be pre-sorted by group
120
+ items: sortedContacts,
174
121
  item: {
175
122
  height: 56,
176
123
  template: (contact) => `<div>${contact.name}</div>`,
177
124
  },
178
125
  })
179
126
  .use(withSections({
180
- getGroupForIndex: (i) => contacts[i].lastName[0].toUpperCase(),
127
+ getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
181
128
  headerHeight: 36,
182
129
  headerTemplate: (letter) => `<div class="header">${letter}</div>`,
183
- sticky: true, // Headers stick to top (Telegram style)
130
+ sticky: true,
184
131
  }))
185
- .build();
132
+ .build()
186
133
  ```
187
134
 
188
- **Bundle:** 12.3 KB gzipped
189
-
190
135
  Set `sticky: false` for inline headers (iMessage/WhatsApp style).
191
136
 
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
137
+ ### Async Loading
251
138
 
252
139
  ```typescript
253
- import { vlist, withAsync } from 'vlist';
140
+ import { vlist, withAsync } from '@floor/vlist'
254
141
 
255
142
  const list = vlist({
256
143
  container: '#list',
257
144
  item: {
258
145
  height: 64,
259
- template: (item) => {
260
- if (!item) return `<div class="loading">Loading...</div>`;
261
- return `<div>${item.name}</div>`;
262
- },
146
+ template: (item) => item
147
+ ? `<div>${item.name}</div>`
148
+ : `<div class="placeholder">Loadingâ€Ķ</div>`,
263
149
  },
264
150
  })
265
151
  .use(withAsync({
266
152
  adapter: {
267
153
  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
- };
154
+ const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
155
+ const data = await res.json()
156
+ return { items: data.items, total: data.total, hasMore: data.hasMore }
275
157
  },
276
158
  },
277
- loading: {
278
- cancelThreshold: 15, // Cancel loads when scrolling fast (pixels/ms)
279
- },
280
159
  }))
281
- .build();
160
+ .build()
282
161
  ```
283
162
 
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.
163
+ ### More Patterns
287
164
 
288
- ### Page-Level Scrolling
165
+ | Pattern | Key options |
166
+ |---------|------------|
167
+ | **Chat UI** | `reverse: true` + `withSections({ sticky: false })` |
168
+ | **Horizontal carousel** | `direction: 'horizontal'`, `item.width` |
169
+ | **Page-level scroll** | `withPage()` |
170
+ | **1M+ items** | `withScale()` — auto-compresses scroll space |
171
+ | **Wrap navigation** | `scroll: { wrap: true }` |
172
+ | **Variable heights** | `item: { height: (index) => heights[index] }` |
289
173
 
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
- ```
174
+ See **[vlist.dev](https://vlist.dev)** for live demos of each.
304
175
 
305
- **Bundle:** 8.6 KB gzipped
306
-
307
- Perfect for blog posts, infinite scroll feeds, and full-page lists.
308
-
309
- ### Horizontal Carousel
176
+ ## API
310
177
 
311
178
  ```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();
179
+ const list = vlist(config).use(...plugins).build()
327
180
  ```
328
181
 
329
- **Bundle:** 8.6 KB gzipped
182
+ ### Data
330
183
 
331
- ### Selection & Navigation
184
+ | Method | Description |
185
+ |--------|-------------|
186
+ | `list.setItems(items)` | Replace all items |
187
+ | `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
188
+ | `list.prependItems(items)` | Add to start (preserves scroll position) |
189
+ | `list.updateItem(id, partial)` | Update a single item |
190
+ | `list.removeItem(id)` | Remove by ID |
191
+ | `list.reload()` | Re-fetch from adapter (async) |
332
192
 
333
- ```typescript
334
- import { vlist, withSelection } from 'vlist';
193
+ ### Navigation
335
194
 
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
- ```
360
-
361
- **Bundle:** 10.0 KB gzipped
362
-
363
- Supports `mode: 'single'` or `'multiple'` with keyboard navigation (Arrow keys, Home, End, Space, Enter).
364
-
365
- ### Variable Heights (Chat Messages)
366
-
367
- ```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();
386
- ```
387
-
388
- **Bundle:** 10.9 KB gzipped
389
-
390
- Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search.
391
-
392
- ## API Reference
393
-
394
- ### Core Methods
395
-
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
- ```
195
+ | Method | Description |
196
+ |--------|-------------|
197
+ | `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \| `'center'` \| `'end'`) |
198
+ | `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |
199
+ | `list.scrollToItem(id, align?)` | Scroll to item by ID |
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 |
433
205
 
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));
667
-
668
- // Restore on return
669
- const snapshot = JSON.parse(sessionStorage.getItem('scroll'));
670
- list.restoreScroll(snapshot);
671
- ```
239
+ ### Properties
672
240
 
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';
707
-
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
- ```
253
+ ## Plugin Configuration
731
254
 
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)
799
-
800
- ```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)
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 |
830
277
 
831
278
  ```bash
832
- npm install @floor/vlist vlist-svelte
279
+ npm install @floor/vlist vlist-react # or vlist-vue / vlist-svelte
833
280
  ```
834
281
 
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
- />
856
- ```
857
-
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 |
913
-
914
- **Traditional virtual lists:** 20-23 KB minimum (all features bundled regardless of usage)
915
-
916
- ### Memory Efficiency
917
-
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)
295
+ | Configuration | Gzipped |
296
+ |---------------|---------|
297
+ | Base only | 7.7 KB |
298
+ | + Grid | 11.7 KB |
299
+ | + Sections | 12.3 KB |
300
+ | + Async | 13.5 KB |
301
+ | All plugins | ~16 KB |
923
302
 
924
- ### Benchmarks
925
-
926
- See [benchmarks](https://vlist.dev/benchmarks/) for detailed performance comparisons.
927
-
928
- ## Browser Support
929
-
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
935
-
936
- Relies on:
937
- - `IntersectionObserver` (widely supported)
938
- - `ResizeObserver` (polyfill available if needed)
939
- - CSS `transform` (universal support)
303
+ With 100K items: **~26 DOM nodes** in the document (visible + overscan) instead of 100,000.
940
304
 
941
305
  ## TypeScript
942
306
 
943
- Fully typed with comprehensive TypeScript definitions:
307
+ Fully typed. Generic over your item type:
944
308
 
945
309
  ```typescript
946
- import { vlist, withGrid, type VList, type VListConfig } from 'vlist';
310
+ import { vlist, withGrid, type VList } from '@floor/vlist'
947
311
 
948
- interface Photo {
949
- id: number;
950
- url: string;
951
- title: string;
952
- }
312
+ interface Photo { id: number; url: string; title: string }
953
313
 
954
- const config: VListConfig<Photo> = {
314
+ const list: VList<Photo> = vlist<Photo>({
955
315
  container: '#gallery',
956
316
  items: photos,
957
317
  item: {
958
318
  height: 200,
959
- template: (photo: Photo) => `<img src="${photo.url}" />`,
319
+ template: (photo) => `<img src="${photo.url}" />`,
960
320
  },
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
321
  })
995
322
  .use(withGrid({ columns: 4 }))
996
- .use(withSections({ ... }))
997
- .use(withSelection({ mode: 'single' }))
998
- .build();
323
+ .build()
999
324
  ```
1000
325
 
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
326
  ## Contributing
1020
327
 
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
328
+ 1. Fork → branch → make changes → add tests → pull request
329
+ 2. Run `bun test` (1739 tests) and `bun run build` before submitting
1028
330
 
1029
331
  ## License
1030
332
 
1031
- MIT License - see [LICENSE](LICENSE) for details
333
+ [MIT](LICENSE)
1032
334
 
1033
335
  ## Links
1034
336
 
1035
- - **Documentation & Examples:** [vlist.dev](https://vlist.dev)
337
+ - **Docs & Examples:** [vlist.dev](https://vlist.dev)
1036
338
  - **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
1037
339
  - **NPM:** [@floor/vlist](https://www.npmjs.com/package/@floor/vlist)
1038
340
  - **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
1039
341
 
1040
342
  ---
1041
343
 
1042
- **Built by [Floor IO](https://floor.io)** with âĪïļ for the web community
344
+ Built by [Floor IO](https://floor.io)