vue-virtual-flow 0.0.4 → 0.0.6

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
@@ -22,6 +22,7 @@ sets responsive without prescribing how an item should look.
22
22
  - [ChatVirtualScroll](#chatvirtualscroll)
23
23
  - [ShortMediaFeed](#shortmediafeed)
24
24
  - [VirtualCarousel](#virtualcarousel)
25
+ - [SkeletonLoader](#skeletonloader)
25
26
  - [Shared recipes](#shared-recipes)
26
27
  - [Viewport height](#viewport-height)
27
28
  - [Stable item keys](#stable-item-keys)
@@ -44,10 +45,19 @@ Import the library stylesheet once in your application entry point:
44
45
  import 'vue-virtual-flow/style.css'
45
46
  ```
46
47
 
47
- The package requires Vue `^3.4.0` and Node.js `>=18`.
48
+ | Dependency | Requirement |
49
+ | --- | --- |
50
+ | Vue | `^3.4.0` |
51
+ | Node.js | `>=18` |
48
52
 
49
53
  ## Quick start
50
54
 
55
+ | Registration | Usage |
56
+ | --- | --- |
57
+ | Local imports | `import { VirtualList, SkeletonLoader } from 'vue-virtual-flow'` |
58
+ | Global plugin | `createApp(App).use(VueVirtualScroll)` |
59
+ | Shared styles | `import 'vue-virtual-flow/style.css'` |
60
+
51
61
  Import components locally:
52
62
 
53
63
  ```vue
@@ -85,7 +95,7 @@ createApp(App).use(VueVirtualScroll).mount('#app')
85
95
 
86
96
  The plugin registers `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
87
97
  `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`, `ChatVirtualScroll`,
88
- `ShortMediaFeed`, and `VirtualCarousel`.
98
+ `ShortMediaFeed`, `VirtualCarousel`, and `SkeletonLoader`.
89
99
 
90
100
  ## Choose a component
91
101
 
@@ -99,9 +109,22 @@ The plugin registers `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
99
109
  | `ChatVirtualScroll` | Messages are appended at the bottom and older history is prepended. | Component container | Variable, measured automatically |
100
110
  | `ShortMediaFeed` | One full-height item should snap into view at a time. | Component container | One viewport per item |
101
111
  | `VirtualCarousel` | Several horizontal slides should snap and virtualize. | Component container | Calculated from container width |
112
+ | `SkeletonLoader` | You need a placeholder while content loads. | None | Configurable width and height |
102
113
 
103
114
  ## Component guides
104
115
 
116
+ Events use Vue template listeners; action methods are called through a component
117
+ ref. The tables below use `list`, `chat`, `feed`, or `carousel` as the ref name.
118
+
119
+ | Task | Usage |
120
+ | --- | --- |
121
+ | Declare a typed ref | `const list = ref<VirtualScrollExpose>()` |
122
+ | Attach the ref to the component | `<VirtualScroll ref="list" ... />` |
123
+ | Call after mount in a script | `list.value?.scrollToIndex(10)` |
124
+ | Call from a template event | `@click="list?.scrollToIndex(10)"` |
125
+ | Select scroll behavior | Pass `{ behavior: 'smooth' }` to `scrollTo` / `scrollToIndex`, or `'smooth'` to `scrollToTop`, `scrollToBottom`, `next`, or `previous`. |
126
+ | Align a list item | `scrollToIndex(index, { align: 'start' })` / `'center'` / `'end'` |
127
+
105
128
  ### VirtualList and DynamicVirtualScroll
106
129
 
107
130
  `VirtualList` is an alias of `DynamicVirtualScroll`. Both names render the same
@@ -199,19 +222,19 @@ to items that have not been measured.
199
222
 
200
223
  #### Events
201
224
 
202
- | Event | Payload | When it fires |
203
- | --- | --- | --- |
204
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. `endIndex` is exclusive. |
205
- | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. |
206
- | `refresh` | none | After pulling past the refresh threshold and releasing. |
225
+ | Event | Payload | When it fires | Usage |
226
+ | --- | --- | --- | --- |
227
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. `endIndex` is exclusive. | `@scroll="onScroll"` |
228
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
229
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
207
230
 
208
231
  #### Exposed API
209
232
 
210
- | Method | Description |
211
- | --- | --- |
212
- | `scrollTo(position, options?)` | Scrolls to a pixel offset inside the list. |
213
- | `scrollToIndex(index, { align?, behavior? })` | Scrolls to a zero-based index. `align` is `start`, `center`, or `end`. |
214
- | `scrollToTop(behavior?)` | Scrolls to the start of the list. |
233
+ | Method / state | Description | Template example |
234
+ | --- | --- | --- |
235
+ | `scrollTo(position, options?)` | Scrolls to a pixel offset inside the list. | `list?.scrollTo(200, { behavior: 'smooth' })` |
236
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
237
+ | `scrollToTop(behavior?)` | Scrolls to the start of the list. | `list?.scrollToTop()` |
215
238
 
216
239
  Long-distance smooth jumps automatically become immediate jumps because
217
240
  unmeasured rows can change the target offset while scrolling.
@@ -285,13 +308,30 @@ The rendered row must stay at `itemSize`. If content can wrap or resize, use
285
308
  | `refreshing` | `boolean` | `false` | Keeps the refresh indicator open during async work. |
286
309
  | `pullRefreshThreshold` | `number` | `64` | Pull distance required to emit `refresh`, in pixels. |
287
310
 
288
- #### Slots, events, and exposed API
311
+ #### Slots
289
312
 
290
- - Slots: `default({ item, index })`, `empty`, `loading`, and
291
- `refresh({ pullDistance, progress, refreshing })`.
292
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more`, and
293
- `refresh`.
294
- - Methods: `scrollTo`, `scrollToIndex`, and `scrollToTop`.
313
+ | Slot | Scope | Description |
314
+ | --- | --- | --- |
315
+ | `default` | `{ item, index }` | Renders each mounted row. |
316
+ | `empty` | none | Rendered when `items` is empty and `loading` is false. |
317
+ | `loading` | none | Loading row; defaults to “Loading more…”. |
318
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Pull-to-refresh indicator. `progress` is clamped from `0` to `1`. |
319
+
320
+ #### Events
321
+
322
+ | Event | Payload | When it fires | Usage |
323
+ | --- | --- | --- | --- |
324
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. `endIndex` is exclusive. | `@scroll="onScroll"` |
325
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
326
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
327
+
328
+ #### Exposed API
329
+
330
+ | Method / state | Description | Template example |
331
+ | --- | --- | --- |
332
+ | `scrollTo(position, options?)` | Scrolls to a pixel offset inside the list. | `list?.scrollTo(200, { behavior: 'smooth' })` |
333
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based item index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
334
+ | `scrollToTop(behavior?)` | Scrolls to the start of the list. | `list?.scrollToTop()` |
295
335
 
296
336
  ### WindowDynamicVirtualScroll
297
337
 
@@ -355,14 +395,30 @@ const list = ref<VirtualScrollExpose>()
355
395
  | `refreshing` | `boolean` | `false` | Keeps the refresh indicator open during async work. |
356
396
  | `pullRefreshThreshold` | `number` | `64` | Pull distance required to emit `refresh`, in pixels. |
357
397
 
358
- #### Slots, events, and exposed API
398
+ #### Slots
359
399
 
360
- - Slots: `default({ item, index })`, `empty`, `loading`, and
361
- `refresh({ pullDistance, progress, refreshing })`.
362
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more`, and
363
- `refresh`. `scrollTop` is relative to the component start.
364
- - Methods: `scrollTo`, `scrollToIndex`, and `scrollToTop`. They scroll the
365
- browser window; `scrollTo` accepts an offset relative to the list.
400
+ | Slot | Scope | Description |
401
+ | --- | --- | --- |
402
+ | `default` | `{ item, index }` | Renders each mounted row. |
403
+ | `empty` | none | Rendered when `items` is empty and `loading` is false. |
404
+ | `loading` | none | Loading row; defaults to “Loading more…”. |
405
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Pull-to-refresh indicator. `progress` is clamped from `0` to `1`. |
406
+
407
+ #### Events
408
+
409
+ | Event | Payload | When it fires | Usage |
410
+ | --- | --- | --- | --- |
411
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On window scroll. `scrollTop` is relative to the component start. `endIndex` is exclusive. | `@scroll="onScroll"` |
412
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
413
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
414
+
415
+ #### Exposed API
416
+
417
+ | Method / state | Description | Template example |
418
+ | --- | --- | --- |
419
+ | `scrollTo(position, options?)` | Scrolls the window to a pixel offset relative to the list start. | `list?.scrollTo(200, { behavior: 'smooth' })` |
420
+ | `scrollToIndex(index, options?)` | Scrolls the window to a zero-based item index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
421
+ | `scrollToTop(behavior?)` | Scrolls the window to the component start. | `list?.scrollToTop()` |
366
422
 
367
423
  ### WindowGirdVirtualScroll
368
424
 
@@ -370,6 +426,8 @@ Use `WindowGirdVirtualScroll` for a multi-column grid whose cards have a known,
370
426
  fixed height and whose viewport is the browser window. Virtualization happens
371
427
  by complete rows, so every visible column remains mounted together.
372
428
 
429
+ #### Example
430
+
373
431
  ```vue
374
432
  <script setup lang="ts">
375
433
  import { WindowGirdVirtualScroll } from 'vue-virtual-flow'
@@ -399,11 +457,49 @@ const products = Array.from({ length: 10_000 }, (_, id) => ({
399
457
  </template>
400
458
  ```
401
459
 
402
- `itemSize` is the height of one grid row. Content that exceeds it is clipped.
403
- `columns` defaults to `2`, `gap` defaults to `0`, and `overscan` defaults to
404
- one extra row before and after the visible range. The remaining list props,
405
- events, named slots, and exposed methods match `WindowDynamicVirtualScroll`.
406
- The default slot also receives `rowIndex` and `columnIndex`.
460
+ #### Props
461
+
462
+ | Prop | Type | Default | Description |
463
+ | --- | --- | --- | --- |
464
+ | `items` | `readonly T[]` | required | Data passed to the default slot. |
465
+ | `itemSize` | `number` | required | Fixed height of each grid row in pixels. Overflow is clipped. |
466
+ | `columns` | `number` | `2` | Number of columns in each row. |
467
+ | `gap` | `number` | `0` | Spacing between rows and columns in pixels. |
468
+ | `overscan` | `number` | `1` | Extra rows mounted before and after the visible range. |
469
+ | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key`, or index | Stable identity for each item. |
470
+ | `ariaLabel` | `string` | `"Window virtual grid"` | Accessible label for the grid. |
471
+ | `hasMore` | `boolean` | `false` | Indicates that another page can be loaded. |
472
+ | `loading` | `boolean` | `false` | Prevents duplicate requests and shows the loading row. |
473
+ | `loadingItemSize` | `number` | `itemSize` | Reserved loading-row height in pixels. |
474
+ | `loadMoreThreshold` | `number` | `200` | Distance from the window bottom to the grid end that triggers `load-more`, in pixels. |
475
+ | `pullToRefresh` | `boolean` | `false` | Enables refresh while the page is at `window.scrollY = 0`. |
476
+ | `refreshing` | `boolean` | `false` | Keeps the refresh indicator open during async work. |
477
+ | `pullRefreshThreshold` | `number` | `64` | Pull distance required to emit `refresh`, in pixels. |
478
+
479
+ #### Slots
480
+
481
+ | Slot | Scope | Description |
482
+ | --- | --- | --- |
483
+ | `default` | `{ item, index, rowIndex, columnIndex }` | Renders each mounted cell. |
484
+ | `empty` | none | Rendered when `items` is empty and `loading` is false. |
485
+ | `loading` | none | Loading row; defaults to “Loading more…”. |
486
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Pull-to-refresh indicator. `progress` is clamped from `0` to `1`. |
487
+
488
+ #### Events
489
+
490
+ | Event | Payload | When it fires | Usage |
491
+ | --- | --- | --- | --- |
492
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On window scroll. `scrollTop` is relative to the component start. `endIndex` is exclusive. | `@scroll="onScroll"` |
493
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
494
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
495
+
496
+ #### Exposed API
497
+
498
+ | Method / state | Description | Template example |
499
+ | --- | --- | --- |
500
+ | `scrollTo(position, options?)` | Scrolls the window to a pixel offset relative to the list start. | `list?.scrollTo(200, { behavior: 'smooth' })` |
501
+ | `scrollToIndex(index, options?)` | Scrolls the window to the row containing a zero-based item index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
502
+ | `scrollToTop(behavior?)` | Scrolls the window to the component start. | `list?.scrollToTop()` |
407
503
 
408
504
  ### ChatVirtualScroll
409
505
 
@@ -475,6 +571,7 @@ height added above the viewport and restores the reader's position.
475
571
  | `estimatedItemSize` | `number` | `48` | Initial average message height in pixels. |
476
572
  | `height` | `number \| string` | `400` | Viewport height. A number is pixels; `"fill"` maps to `100%`. |
477
573
  | `overscan` | `number` | `5` | Extra messages mounted before and after the visible range. |
574
+ | `size` | `number` | unset | Optional total render budget including overscan, e.g. `:size="12"` for mobile. Trims overscan first; always renders enough messages to fill the viewport, even if this exceeds `size`. Does not add messages beyond `overscan`. Positive values are floored (minimum 1); non-positive or non-finite values disable the budget. |
478
575
  | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key`, or index | Stable identity; especially important when prepending. |
479
576
  | `ariaLabel` | `string` | `"Chat messages"` | Accessible label for the live log. |
480
577
  | `stickToBottom` | `boolean` | `true` | Follows appended messages while currently near the bottom. |
@@ -494,21 +591,21 @@ height added above the viewport and restores the reader's position.
494
591
 
495
592
  #### Events
496
593
 
497
- | Event | Payload | When it fires |
498
- | --- | --- | --- |
499
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. |
500
- | `load-older` | none | Near the top when `hasOlder` is true and `loadingOlder` is false. |
501
- | `bottom-change` | `boolean` | When the viewport enters or leaves the bottom threshold. |
594
+ | Event | Payload | When it fires | Usage |
595
+ | --- | --- | --- | --- |
596
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. | `@scroll="onScroll"` |
597
+ | `load-older` | none | Near the top when `hasOlder` is true and `loadingOlder` is false. | `@load-older="loadOlder"` |
598
+ | `bottom-change` | `boolean` | When the viewport enters or leaves the bottom threshold. | `@bottom-change="onBottomChange"` |
502
599
 
503
600
  #### Exposed API
504
601
 
505
- | Member | Description |
506
- | --- | --- |
507
- | `isAtBottom` | Readonly boolean indicating whether the user is near the bottom. |
508
- | `scrollTo(position, options?)` | Scrolls to a pixel offset. |
509
- | `scrollToIndex(index, { align?, behavior? })` | Scrolls to a zero-based message index. |
510
- | `scrollToTop(behavior?)` | Scrolls to the oldest loaded message. |
511
- | `scrollToBottom(behavior?)` | Scrolls to the newest loaded message. |
602
+ | Method / state | Description | Template example |
603
+ | --- | --- | --- |
604
+ | `isAtBottom` | Readonly boolean indicating whether the user is near the bottom. | `chat?.isAtBottom` |
605
+ | `scrollTo(position, options?)` | Scrolls to a pixel offset. | `chat?.scrollTo(200, { behavior: 'smooth' })` |
606
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based message index. | `chat?.scrollToIndex(10, { behavior: 'smooth' })` |
607
+ | `scrollToTop(behavior?)` | Scrolls to the oldest loaded message. | `chat?.scrollToTop()` |
608
+ | `scrollToBottom(behavior?)` | Scrolls to the newest loaded message. | `chat?.scrollToBottom('smooth')` |
512
609
 
513
610
  ### ShortMediaFeed
514
611
 
@@ -576,23 +673,39 @@ keep persistent playback state in the parent.
576
673
  | `loading` | `boolean` | `false` | Prevents duplicate `load-more` requests. |
577
674
  | `loadMoreThreshold` | `number` | `2` | Remaining item count that triggers `load-more`. |
578
675
 
579
- #### Slots and events
676
+ #### Slots
677
+
678
+ | Slot | Scope | Description |
679
+ | --- | --- | --- |
680
+ | `default` | `{ item, index, active }` | Renders a mounted item; `active` marks the snapped item. |
681
+ | `empty` | none | Rendered when `items` is empty. |
682
+
683
+ #### Events
580
684
 
581
- | Kind | Name | Payload/scope | Description |
685
+ | Event | Payload | When it fires | Usage |
582
686
  | --- | --- | --- | --- |
583
- | Slot | `default` | `{ item, index, active }` | Renders a mounted item; `active` marks the snapped item. |
584
- | Slot | `empty` | none | Rendered when `items` is empty. |
585
- | Event | `update:activeIndex` | `number` | Supports `v-model:active-index`. |
586
- | Event | `change` | `{ index, item }` | The active item changes. |
587
- | Event | `reach-start` | none | Navigation changes to the first item. |
588
- | Event | `reach-end` | none | Navigation changes to the last item. |
589
- | Event | `load-more` | none | Remaining items reach `loadMoreThreshold`. |
687
+ | `update:activeIndex` | `number` | Supports `v-model:active-index`. | `v-model:active-index="activeIndex"` |
688
+ | `change` | `{ index, item }` | The active item changes. | `@change="onChange"` |
689
+ | `reach-start` | none | Navigation changes to the first item. | `@reach-start="onReachStart"` |
690
+ | `reach-end` | none | Navigation changes to the last item. | `@reach-end="onReachEnd"` |
691
+ | `load-more` | none | Remaining items reach `loadMoreThreshold`. | `@load-more="loadMore"` |
590
692
 
591
- #### Exposed API and keyboard controls
693
+ #### Exposed API
694
+
695
+ | Method / state | Description | Template example |
696
+ | --- | --- | --- |
697
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based item index using standard `ScrollToOptions`. | `feed?.scrollToIndex(10, { behavior: 'smooth' })` |
592
698
 
593
- `scrollToIndex(index, options?)` scrolls to an item using standard
594
- `ScrollToOptions`. The viewport supports Arrow Up/Down, Page Up/Down, Home,
595
- and End.
699
+ #### Keyboard controls
700
+
701
+ These keys work while the viewport is focused:
702
+
703
+ | Key | Action |
704
+ | --- | --- |
705
+ | `ArrowDown`, `PageDown` | Moves to the next item. |
706
+ | `ArrowUp`, `PageUp` | Moves to the previous item. |
707
+ | `Home` | Moves to the first position. |
708
+ | `End` | Moves to the last item. |
596
709
 
597
710
  For an unbounded feed, page data with `load-more` and keep a reasonable item
598
711
  window in the parent. Virtualization limits mounted DOM and media elements,
@@ -676,31 +789,74 @@ five slides are normally mounted in the middle of the collection.
676
789
  | `autoplayDelay` | `number` | `3000` | Delay in milliseconds between automatic transitions; values below `1` are clamped and non-finite values use the default. |
677
790
  | `autoplayLoop` | `boolean` | `true` | Returns to the first slide when reaching the end. |
678
791
  | `pauseOnHover` | `boolean` | `true` | Pauses autoplay when the carousel is hovered. |
792
+ | `responsive` | `ResponsiveBreakpoint[]` | `undefined` | Overrides slide count, scroll step, and gap at container-width breakpoints. |
679
793
 
680
- #### Slots and events
794
+ #### Responsive configuration
681
795
 
682
- | Kind | Name | Payload/scope | Description |
796
+ The largest `breakpoint` not exceeding the carousel container width is used.
797
+ If no breakpoint matches, the base props apply. Omitted fields fall back to
798
+ the base props, not to another breakpoint.
799
+
800
+ | Field | Type | Default | Description |
683
801
  | --- | --- | --- | --- |
684
- | Slot | `default` | `{ item, index, active, visible }` | Renders each mounted slide. |
685
- | Slot | `empty` | none | Rendered when `items` is empty. |
686
- | Event | `update:activeIndex` | `number` | Supports `v-model:active-index`. |
687
- | Event | `change` | `{ index, item }` | The first visible slide changes. |
688
- | Event | `reach-start` | none | Navigation changes to the first position. |
689
- | Event | `reach-end` | none | Navigation changes to the final valid position. |
690
- | Event | `load-more` | none | Remaining slides reach `loadMoreThreshold`. |
802
+ | `breakpoint` | `number` | required | Minimum container width in pixels. |
803
+ | `slidesPerView` | `number` | `slidesPerView` | Slides visible at this breakpoint. |
804
+ | `slidesToScroll` | `number` | `slidesToScroll` | Slides moved by `next()` and `previous()`. |
805
+ | `gap` | `number` | `gap` | Space between slides in pixels. |
806
+
807
+ ```vue
808
+ <VirtualCarousel
809
+ :items="products"
810
+ :slides-per-view="1"
811
+ :responsive="[
812
+ { breakpoint: 640, slidesPerView: 2, gap: 12 },
813
+ { breakpoint: 1024, slidesPerView: 3, slidesToScroll: 2, gap: 16 },
814
+ ]"
815
+ >
816
+ <template #default="{ item }"><ProductCard :product="item" /></template>
817
+ </VirtualCarousel>
818
+ ```
819
+
820
+ #### Slots
821
+
822
+ | Slot | Scope | Description |
823
+ | --- | --- | --- |
824
+ | `default` | `{ item, index, active, visible }` | Renders each mounted slide. |
825
+ | `empty` | none | Rendered when `items` is empty. |
826
+
827
+ #### Events
828
+
829
+ | Event | Payload | When it fires | Usage |
830
+ | --- | --- | --- | --- |
831
+ | `update:activeIndex` | `number` | Supports `v-model:active-index`. | `v-model:active-index="activeIndex"` |
832
+ | `change` | `{ index, item }` | The first visible slide changes. | `@change="onChange"` |
833
+ | `reach-start` | none | Navigation changes to the first position. | `@reach-start="onReachStart"` |
834
+ | `reach-end` | none | Navigation changes to the final valid position. | `@reach-end="onReachEnd"` |
835
+ | `load-more` | none | Remaining slides reach `loadMoreThreshold`. | `@load-more="loadMore"` |
836
+
837
+ #### Exposed API
838
+
839
+ | Method / state | Description | Template example |
840
+ | --- | --- | --- |
841
+ | `next(behavior?)` | Advances by `slidesToScroll`; defaults to smooth scrolling. | `carousel?.next()` |
842
+ | `previous(behavior?)` | Moves back by `slidesToScroll`. | `carousel?.previous()` |
843
+ | `scrollToIndex(index, options?)` | Makes the index the first visible slide. | `carousel?.scrollToIndex(10, { behavior: 'smooth' })` |
844
+ | `startAutoplay()` | Starts or restarts the timer when `autoplay` is true and items are available. | `carousel?.startAutoplay()` |
845
+ | `stopAutoplay()` | Clears the current timer; set `autoplay=false` to keep automatic progression disabled. | `carousel?.stopAutoplay()` |
691
846
 
692
- #### Exposed API and keyboard controls
847
+ #### Keyboard controls
693
848
 
694
- | Method | Description |
849
+ These keys work while the viewport is focused:
850
+
851
+ | Key | Action |
695
852
  | --- | --- |
696
- | `next(behavior?)` | Advances by `slidesToScroll`; defaults to smooth scrolling. |
697
- | `previous(behavior?)` | Moves back by `slidesToScroll`. |
698
- | `scrollToIndex(index, options?)` | Makes the index the first visible slide. |
699
- | `startAutoplay()` | Starts or restarts automatic slide progression. |
700
- | `stopAutoplay()` | Stops automatic slide progression. |
853
+ | `ArrowRight`, `PageDown` | Advances by `slidesToScroll`, equivalent to `next()`. |
854
+ | `ArrowLeft`, `PageUp` | Moves back by `slidesToScroll`, equivalent to `previous()`. |
855
+ | `Home` | Moves to the first position. |
856
+ | `End` | Moves to the final valid first-slide position. |
701
857
 
702
- The viewport supports Arrow Left/Right, Page Up/Down, Home, and End. A
703
- long-distance smooth jump becomes immediate so virtualized gaps are not shown.
858
+ A long-distance smooth jump becomes immediate so virtualized gaps are not
859
+ shown.
704
860
 
705
861
  #### Autoplay example
706
862
 
@@ -733,17 +889,134 @@ const products = ref(loadProducts())
733
889
  </template>
734
890
  ```
735
891
 
736
- Autoplay also starts when an initially empty `items` collection receives data.
737
- `stopAutoplay()` pauses the timer and `startAutoplay()` resumes it while the
738
- `autoplay` prop remains enabled. Changes to `autoplayDelay`, `autoplayLoop`, and
739
- `pauseOnHover` take effect at runtime.
892
+ | Action / condition | Result |
893
+ | --- | --- |
894
+ | `autoplay=true` | Starts automatically, including when an initially empty `items` collection receives data. |
895
+ | `stopAutoplay()` | Clears the current timer. Later navigation or pointer events may restart it when `autoplay` remains true. |
896
+ | `startAutoplay()` | Starts or restarts the timer when `autoplay` is true and items are available. |
897
+ | `autoplay=false` | Disables automatic progression until the prop is enabled again. |
898
+ | `autoplayLoop` | At the end, true returns to the start; false stops the timer. |
899
+ | `pauseOnHover` | When true, hovering pauses the timer; leaving resumes it if autoplay is enabled. |
900
+ | Change autoplay settings | `autoplayDelay`, `autoplayLoop`, and `pauseOnHover` take effect at runtime. |
901
+
902
+ ### SkeletonLoader
903
+
904
+ `SkeletonLoader` renders a placeholder with a shimmer animation. Use it on its
905
+ own or inside a list's `loading` slot. It stays visible while mounted; control
906
+ standalone visibility with `v-if`, or let the list's `loading` prop control the
907
+ slot.
908
+
909
+ #### Example
910
+
911
+ Import the library stylesheet once, as shown below or in your application entry
912
+ point. Set `loading` to false when your data request finishes.
913
+
914
+ ```vue
915
+ <script setup lang="ts">
916
+ import { ref } from 'vue'
917
+ import { SkeletonLoader } from 'vue-virtual-flow'
918
+ import 'vue-virtual-flow/style.css'
919
+
920
+ const loading = ref(true)
921
+ </script>
922
+
923
+ <template>
924
+ <SkeletonLoader
925
+ v-if="loading"
926
+ width="100%"
927
+ :height="120"
928
+ aria-label="Loading content…"
929
+ />
930
+ <p v-else>Content is ready.</p>
931
+ </template>
932
+ ```
933
+
934
+ #### Props
935
+
936
+ | Prop | Type | Default | Description |
937
+ | --- | --- | --- | --- |
938
+ | `width` | `number \| string` | `'100%'` | Width in pixels for numbers, or a CSS length such as `'60%'` or `'12rem'`. |
939
+ | `height` | `number \| string` | `16` | Height in pixels for numbers, or a CSS length. |
940
+ | `variant` | `'rectangle' \| 'rounded' \| 'circle'` | `'rounded'` | Placeholder shape. Use equal width and height for a circle. |
941
+ | `animated` | `boolean` | `true` | Enables the shimmer animation. |
942
+ | `ariaLabel` | `string` | `'Loading…'` | Accessible label for the placeholder's `role="status"`. |
943
+
944
+ ```vue
945
+ <SkeletonLoader variant="circle" :width="48" :height="48" />
946
+ <SkeletonLoader variant="rectangle" :height="180" />
947
+ <SkeletonLoader width="60%" :height="20" :animated="false" />
948
+ ```
949
+
950
+ The animation automatically stops when the user prefers reduced motion. When
951
+ composing several decorative skeletons under a shared loading message, pass
952
+ `aria-hidden="true"` to each skeleton to avoid repeated accessible labels.
953
+
954
+ #### Slots, events, and exposed API
955
+
956
+ | API | Support |
957
+ | --- | --- |
958
+ | Slots | None. The component renders a single placeholder. |
959
+ | Events | No custom events. |
960
+ | Exposed methods | None. Control the component with props and `v-if`. |
961
+
962
+ #### Actions
963
+
964
+ | Action | Usage |
965
+ | --- | --- |
966
+ | Show or hide | `<SkeletonLoader v-if="loading" />` |
967
+ | Use a circle | `<SkeletonLoader variant="circle" :width="48" :height="48" />` |
968
+ | Disable shimmer | `<SkeletonLoader :animated="false" />` |
969
+ | Set the accessible label | `aria-label="Loading…"` |
970
+ | Hide a decorative skeleton from screen readers | `aria-hidden="true"` |
971
+
972
+ #### Use in a loading slot
973
+
974
+ Import `SkeletonLoader` alongside `VirtualList`. With your existing `items` and
975
+ `loading` state, replace the loading message with a skeleton:
976
+
977
+ ```vue
978
+ <VirtualList :items="items" :loading="loading" :loading-item-size="64">
979
+ <template #default="{ item }"><FeedCard :item="item" /></template>
980
+ <template #loading>
981
+ <SkeletonLoader :height="40" aria-label="Loading the next page…" />
982
+ </template>
983
+ </VirtualList>
984
+ ```
985
+
986
+ | Setting | Usage |
987
+ | --- | --- |
988
+ | Components with a `loading` slot | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`. |
989
+ | List `loading` prop | Set to true to show the slot; false to hide it. |
990
+ | Skeleton `height` prop | Sets the placeholder height, for example `40` pixels. |
991
+ | List `loadingItemSize` prop | Reserve space for the skeleton plus the row's vertical padding. Example: `40px + 1.5rem = 64px` at a 16px root font size. |
992
+
993
+ #### Customize appearance
994
+
995
+ Set these CSS custom properties on the skeleton or a parent element:
996
+
997
+ | CSS custom property | Default | Description |
998
+ | --- | --- | --- |
999
+ | `--skeleton-background` | `#e2e8f0` | Base background color. |
1000
+ | `--skeleton-highlight` | `rgb(255 255 255 / 60%)` | Shimmer highlight color. |
1001
+ | `--skeleton-radius` | `8px` | Corner radius for the `rounded` variant. |
1002
+
1003
+ ```vue
1004
+ <SkeletonLoader
1005
+ :height="80"
1006
+ style="--skeleton-background: #334155; --skeleton-highlight: #475569; --skeleton-radius: 12px"
1007
+ />
1008
+ ```
740
1009
 
741
1010
  ## Shared recipes
742
1011
 
743
1012
  ### Viewport height
744
1013
 
745
- Container-based components accept a number or CSS height string. Numbers are
746
- converted to pixels. `height="fill"` is shorthand for `height="100%"`:
1014
+ | Value / component | Behavior |
1015
+ | --- | --- |
1016
+ | `:height="400"` | Numeric values become pixels. |
1017
+ | `height="60vh"` | CSS height strings are passed through. |
1018
+ | `height="fill"` / `height="100%"` | Uses the parent height. The parent must have an explicit height; development builds warn if this resolves to `0px`. |
1019
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Use the browser window and do not accept a `height` prop. |
747
1020
 
748
1021
  ```vue
749
1022
  <div class="list-container">
@@ -760,16 +1033,14 @@ converted to pixels. `height="fill"` is shorthand for `height="100%"`:
760
1033
  }
761
1034
  ```
762
1035
 
763
- A percentage height only works when its parent has an explicit height.
764
- Development builds warn when it resolves to `0px`.
765
- `WindowDynamicVirtualScroll` and `WindowGirdVirtualScroll` use the browser
766
- window and have no `height` prop.
767
-
768
1036
  ### Stable item keys
769
1037
 
770
- For object items, components try `item.id` and then `item.key`. Primitive items
771
- fall back to their index. Provide `itemKey` when the identifier has another
772
- name:
1038
+ | Configuration / data | Key selection |
1039
+ | --- | --- |
1040
+ | `:item-key="(item, index) => …"` | Uses the function result. |
1041
+ | `item-key="uuid"` | Uses a valid `item.uuid`; otherwise tries `id`, `key`, then the index. |
1042
+ | Object without an explicit `itemKey` | Tries `item.id`, then `item.key`, then the index. |
1043
+ | Primitive without a key function | Falls back to the index. |
773
1044
 
774
1045
  ```vue
775
1046
  <VirtualList :items="users" item-key="uuid" />
@@ -786,8 +1057,11 @@ variable-height item.
786
1057
 
787
1058
  ### Load more
788
1059
 
789
- List components use a pixel distance threshold. `ShortMediaFeed` and
790
- `VirtualCarousel` use a remaining-item threshold.
1060
+ | Component | `loadMoreThreshold` | Event |
1061
+ | --- | --- | --- |
1062
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Distance to the end in pixels. | `load-more` |
1063
+ | `ShortMediaFeed` | Number of items after the active item. | `load-more` |
1064
+ | `VirtualCarousel` | Number of slides after the visible group. | `load-more` |
791
1065
 
792
1066
  ```vue
793
1067
  <script setup lang="ts">
@@ -819,15 +1093,20 @@ async function loadMore() {
819
1093
  </template>
820
1094
  ```
821
1095
 
822
- The event fires at most once for the current item count. Appending items allows
823
- the next request. Keep `loading` true for the entire request and set `hasMore`
824
- to false after the final page.
1096
+ | Step / condition | Action / result |
1097
+ | --- | --- |
1098
+ | Receive `load-more` | Start the next request only when `loading` is false and `hasMore` is true. |
1099
+ | Request in progress | Keep `loading=true` for the entire request. |
1100
+ | Receive the next page | Append its items. The event normally fires once per current item count; increasing that count permits another request. |
1101
+ | Request finished | Set `loading=false`; use `finally` when requests may fail. |
1102
+ | Final page | Set `hasMore=false` to stop further requests. |
825
1103
 
826
1104
  ### Pull to refresh
827
1105
 
828
- Pull-to-refresh is available on `VirtualList`, `DynamicVirtualScroll`,
829
- `VirtualScroll`, `WindowDynamicVirtualScroll`, and
830
- `WindowGirdVirtualScroll`:
1106
+ | Component | Gesture starts at |
1107
+ | --- | --- |
1108
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll` | The top of the component container. |
1109
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | `window.scrollY = 0` |
831
1110
 
832
1111
  ```vue
833
1112
  <VirtualList
@@ -844,21 +1123,51 @@ Pull-to-refresh is available on `VirtualList`, `DynamicVirtualScroll`,
844
1123
  </VirtualList>
845
1124
  ```
846
1125
 
847
- The single-touch gesture begins only at the top. Set `refreshing` to true while
848
- the async refresh runs, then return it to false to close the indicator.
1126
+ | Step / state | Action / result |
1127
+ | --- | --- |
1128
+ | Enable the gesture | `pull-to-refresh` |
1129
+ | Pull down with one finger at the top | Updates the `refresh` slot scope: `pullDistance`, `progress`, and `refreshing`. |
1130
+ | Release after reaching `pullRefreshThreshold` | Emits `refresh` without a payload. |
1131
+ | Handle `refresh` | Set `refreshing=true` while fetching fresh data to keep the indicator open. |
1132
+ | Finish refreshing | Set `refreshing=false` to close the indicator. |
849
1133
 
850
1134
  ## TypeScript exports
851
1135
 
1136
+ | Type | Purpose |
1137
+ | --- | --- |
1138
+ | `ChatVirtualScrollExpose` | Chat ref API and `isAtBottom` state. |
1139
+ | `ChatVirtualScrollProps<T>` | Chat component props. |
1140
+ | `DynamicVirtualScrollProps<T>` | Variable-height list props. |
1141
+ | `ItemKey<T>` | Property name or function used to identify an item. |
1142
+ | `ResponsiveBreakpoint` | Carousel container breakpoint and optional layout overrides. |
1143
+ | `ScrollAlignment` | `'start'`, `'center'`, or `'end'` for list index scrolling. |
1144
+ | `ShortMediaFeedChangeEvent<T>` | Feed `change` payload: `{ index, item }`. |
1145
+ | `ShortMediaFeedExpose` | Feed ref API. |
1146
+ | `ShortMediaFeedProps<T>` | Feed props. |
1147
+ | `SkeletonLoaderProps` | Skeleton size, shape, animation, and accessible label. |
1148
+ | `VirtualCarouselChangeEvent<T>` | Carousel `change` payload: `{ index, item }`. |
1149
+ | `VirtualCarouselExpose` | Carousel navigation and autoplay ref API. |
1150
+ | `VirtualCarouselProps<T>` | Carousel props, including responsive settings. |
1151
+ | `VirtualListExpose` | Alias of `VirtualScrollExpose`. |
1152
+ | `VirtualListProps<T>` | Alias of `DynamicVirtualScrollProps<T>`. |
1153
+ | `VirtualScrollEvent` | Scroll payload: `{ scrollTop, startIndex, endIndex }`; `endIndex` is exclusive. |
1154
+ | `VirtualScrollExpose` | Shared ref API for fixed, dynamic, and window lists/grids. |
1155
+ | `VirtualScrollProps<T>` | Fixed-height list props. |
1156
+ | `WindowDynamicVirtualScrollProps<T>` | Window-based variable-height list props. |
1157
+ | `WindowGirdVirtualScrollProps<T>` | Window-based fixed-row grid props. |
1158
+
852
1159
  ```ts
853
1160
  import type {
854
1161
  ChatVirtualScrollExpose,
855
1162
  ChatVirtualScrollProps,
856
1163
  DynamicVirtualScrollProps,
857
1164
  ItemKey,
1165
+ ResponsiveBreakpoint,
858
1166
  ScrollAlignment,
859
1167
  ShortMediaFeedChangeEvent,
860
1168
  ShortMediaFeedExpose,
861
1169
  ShortMediaFeedProps,
1170
+ SkeletonLoaderProps,
862
1171
  VirtualCarouselChangeEvent,
863
1172
  VirtualCarouselExpose,
864
1173
  VirtualCarouselProps,
@@ -874,6 +1183,22 @@ import type {
874
1183
 
875
1184
  ## Development and release
876
1185
 
1186
+ | Command / lifecycle | Action |
1187
+ | --- | --- |
1188
+ | `bun install` | Install dependencies. |
1189
+ | `bunx playwright install chromium firefox webkit` | Install browsers for smoke tests. |
1190
+ | `bun run dev` | Start the playground. |
1191
+ | `bun run typecheck` | Check TypeScript and Vue types. |
1192
+ | `bun run test` | Run unit tests. |
1193
+ | `bun run test:watch` | Run unit tests in watch mode. |
1194
+ | `bun run build` | Typecheck and build the package with type declarations. |
1195
+ | `bun run typecheck:package` | Check ESM and CommonJS consumer types after building. |
1196
+ | `bun run lint:package` | Validate package exports and type packaging after building. |
1197
+ | `bun run test:browser` | Run Playwright browser tests. |
1198
+ | `bun run check` | Run all checks: unit tests, build, consumer types, package lint, and browser tests. |
1199
+ | `npm pack --dry-run` | Inspect the files that will be published. |
1200
+ | `prepublishOnly` | Automatically runs `bun run check` before publishing. |
1201
+
877
1202
  ```bash
878
1203
  bun install
879
1204
  bunx playwright install chromium firefox webkit