vue-virtual-flow 0.0.8 → 0.1.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +99 -12
  3. package/README.vi.md +91 -10
  4. package/dist/components/ChatVirtualScroll.d.cts +29 -2
  5. package/dist/components/ChatVirtualScroll.d.ts +29 -2
  6. package/dist/components/DynamicVirtualScroll.d.cts +3 -3
  7. package/dist/components/DynamicVirtualScroll.d.ts +3 -3
  8. package/dist/components/ShortMediaFeed.d.cts +2 -2
  9. package/dist/components/ShortMediaFeed.d.ts +2 -2
  10. package/dist/components/VirtualCarousel.d.cts +15 -2
  11. package/dist/components/VirtualCarousel.d.ts +15 -2
  12. package/dist/components/VirtualScroll.d.cts +3 -3
  13. package/dist/components/VirtualScroll.d.ts +3 -3
  14. package/dist/components/WindowDynamicVirtualScroll.d.cts +3 -3
  15. package/dist/components/WindowDynamicVirtualScroll.d.ts +3 -3
  16. package/dist/components/WindowGirdVirtualScroll.d.cts +3 -3
  17. package/dist/components/WindowGirdVirtualScroll.d.ts +3 -3
  18. package/dist/composables/useCarouselAutoplay.d.cts +15 -0
  19. package/dist/composables/useCarouselAutoplay.d.ts +15 -0
  20. package/dist/composables/useCarouselInteraction.d.cts +40 -0
  21. package/dist/composables/useCarouselInteraction.d.ts +40 -0
  22. package/dist/composables/useScrollActivity.d.cts +28 -0
  23. package/dist/composables/useScrollActivity.d.ts +28 -0
  24. package/dist/index.d.cts +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/types.d.cts +46 -0
  27. package/dist/types.d.ts +46 -0
  28. package/dist/utils/carouselLayout.d.cts +8 -0
  29. package/dist/utils/carouselLayout.d.ts +8 -0
  30. package/dist/utils/carouselMotion.d.cts +5 -0
  31. package/dist/utils/carouselMotion.d.ts +5 -0
  32. package/dist/utils/keyedSizeIndex.d.cts +19 -0
  33. package/dist/utils/keyedSizeIndex.d.ts +19 -0
  34. package/dist/vue-virtual-flow.cjs +1 -1
  35. package/dist/vue-virtual-flow.css +1 -1
  36. package/dist/vue-virtual-flow.js +2193 -1510
  37. package/package.json +3 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-14
11
+
12
+ ### Added
13
+ - **VirtualCarousel**: Add configurable mouse dragging, momentum, controlled snapping, intrinsic `autoWidth` slides, RTL support, continuous looping, bounded keep-alive caching, and pinned slide instances
14
+ - **ChatVirtualScroll**: Add per-message size estimates, scroll activity state, visible-range events, key-based scrolling, logical position restoration, pinned/focused message retention, forced bottom-follow rules, and configurable synchronous measurement
15
+ - **ChatVirtualScroll**: Add an accessible new-message counter with a customizable slot and localized built-in label
16
+ - **ChatVirtualScroll**: Add `itemsVersion` and `refresh()` for explicitly reconciling arbitrary in-place mutations without an always-on deep watcher
17
+
18
+ ### Fixed
19
+ - **ChatVirtualScroll**: Anchor native scrolling at the bottom so prepended history is immediately reachable during touch scrolling, without rebasing the scroll position
20
+ - **ChatVirtualScroll**: Preserve the reading position when messages below the viewport resize or new messages arrive during a gesture
21
+ - **ChatVirtualScroll**: Keep the new-message control inside the viewport instead of scrolling it away with message content
22
+ - **ChatVirtualScroll**: Defer measurement corrections during wheel, keyboard, touch, smooth-scroll, and scrollbar-drag activity so native momentum is not interrupted
23
+ - **ChatVirtualScroll**: Use stable identity fallbacks instead of array indexes and reject duplicate keys before they can corrupt measurements or anchors
24
+
25
+ ### Improved
26
+ - **VirtualCarousel**: Extract autoplay, interaction, layout, and motion responsibilities and add cross-browser regression coverage for dragging, looping, RTL, auto-width, and cached state
27
+ - **ChatVirtualScroll**: Measure previously unseen messages before paint and update append, prepend, measurement, offset, and key operations through an incremental keyed index
28
+ - **ChatVirtualScroll**: Remove the duplicate measured-height map and split input activity from layout/measurement logic
29
+ - Cover cold variable-height scrolling, native Chromium touch input, new-message overlay geometry, and prepend/resize anchoring across Chromium, Firefox, and WebKit
30
+
31
+ ### Changed
32
+ - The chat's internal DOM `scrollTop` is now negative above the bottom. Exposed scrolling methods and `scroll` event offsets remain measured from the top; use those APIs instead of accessing the native offset directly
33
+ - **ChatVirtualScroll**: Add a `.vue-chat-virtual-scroll__root` layout wrapper while keeping inherited attributes on the scroll viewport
34
+ - **ChatVirtualScroll**: Duplicate keys now fail fast; every message must have a unique stable key
35
+
36
+ ## [0.0.7] - 2026-09-29
37
+
38
+ ### Fixed
39
+ - **ChatVirtualScroll**: Preserve visible message DOM and scroll position when older messages are prepended
40
+ - **ChatVirtualScroll**: Defer native scroll corrections during touch gestures and momentum while visually compensating for measured height changes
41
+ - **ChatVirtualScroll**: Prevent gaps at the top and double scroll adjustments near the bottom when message heights shrink
42
+
43
+ ### Improved
44
+ - Reserve image dimensions in the chat playground to reduce layout shifts while media loads
45
+ - Added regression coverage for touch scrolling, prepend anchoring, height changes, imperative scrolling, and cleanup across unit and browser tests
46
+
47
+ ## [0.0.6] - 2026-09-29
48
+
49
+ ### Added
50
+ - **ChatVirtualScroll**: Added an optional `size` prop to limit mounted messages including overscan, while always keeping enough messages to fill the viewport
51
+
52
+ ### Improved
53
+ - Added English and Vietnamese documentation for `size` and configured the chat playground with a render budget of 12 messages
54
+ - Added regression tests for render budgets, reactive updates, variable message heights, append/prepend behavior, and scrolling up in a portrait mobile viewport
55
+
56
+ ## [0.0.5] - 2026-09-29
57
+
58
+ ### Added
59
+ - **SkeletonLoader**: Added configurable dimensions, rectangle/rounded/circle variants, shimmer animation, reduced-motion support, and an accessible loading label
60
+ - **Public API**: Exported `SkeletonLoader` and `SkeletonLoaderProps`, and registered the component in the Vue plugin
61
+ - **Playground**: Added skeleton placeholders to the load-more demo
62
+
63
+ ### Improved
64
+ - Added English and Vietnamese SkeletonLoader usage guides
65
+ - Converted API references and usage recipes into tables, including event listeners, ref actions, responsive configuration, autoplay, and release commands
66
+
67
+ ## [0.0.4] - 2026-09-11
68
+
69
+ ### Added
70
+ - **WindowGirdVirtualScroll**: Added fixed-height, multi-column window virtualization with configurable columns, gaps, overscan, loading, pull-to-refresh, and imperative scrolling
71
+ - **Playground**: Added an interactive window grid demo with column controls and direct item navigation
72
+
73
+ ### Fixed
74
+ - **WindowDynamicVirtualScroll**: Reset stale measurements and load-more state when the item key set changes without changing the item count
75
+ - **WindowDynamicVirtualScroll**: Batched window scroll and ResizeObserver updates with `requestAnimationFrame` to reduce redundant work
76
+ - **WindowDynamicVirtualScroll**: Cancelled pending frames and guarded deferred callbacks during component cleanup
77
+
78
+ ### Improved
79
+ - Added regression coverage for window scroll batching, resize compensation, same-size dataset replacement, and lifecycle cleanup
80
+
81
+ ## [0.0.3] - 2026-08-08
82
+
83
+ ### Fixed
84
+ - **All Components**: Smooth scroll animation now works correctly for all distances instead of being force-disabled when scrolling > 3 viewports
85
+ - **VirtualCarousel**: Fixed responsive mode stability when window is resized
86
+ - **VirtualCarousel**: Fixed circular dependency in computed properties causing scroll calculation issues
87
+ - **All Components**: Improved ResizeObserver cleanup to prevent potential memory leaks
88
+
89
+ ### Added
90
+ - **VirtualCarousel**: Exported `ResponsiveBreakpoint` type for TypeScript users
91
+
92
+ ### Improved
93
+ - Better memory management in all components with explicit observer cleanup
94
+ - More consistent scroll behavior respecting user's `behavior` option
95
+
96
+ ## [0.0.2] - Previous release
97
+
98
+ Initial public release with core functionality.
package/README.md CHANGED
@@ -551,6 +551,9 @@ async function loadOlder() {
551
551
  </template>
552
552
 
553
553
  <template #loadingOlder>Loading older messages…</template>
554
+ <template #newItems="{ count, scrollToBottom }">
555
+ <button @click="scrollToBottom('smooth')">{{ count }} new</button>
556
+ </template>
554
557
  <template #empty>No messages.</template>
555
558
  </ChatVirtualScroll>
556
559
 
@@ -563,12 +566,19 @@ async function loadOlder() {
563
566
  Prepend older data to the same keyed collection. The scroll container is anchored
564
567
  at the bottom, so adding or measuring older messages above the reader does not
565
568
  need a native scroll correction. Previously unseen messages are measured before
566
- paint, and cached height updates use an incremental size index.
567
-
568
- Use the exposed methods and the `scroll` event for positions measured from the
569
- top. The internal DOM scroller uses negative `scrollTop` values (zero at the
570
- bottom); code that directly reads or writes that DOM property must account for
571
- this. Message DOM order remains oldest to newest.
569
+ paint, and cached height updates use an incremental keyed tree. Normal append,
570
+ prepend, measurement, offset and key lookups are `O(log n)` per changed message;
571
+ arbitrary replacement, removal or reorder falls back to rebuilding the index.
572
+ Replace the `items` array for arbitrary edits. If application constraints require
573
+ mutating the same array instance, increment `itemsVersion` (or call `refresh()`)
574
+ after a same-length removal/reorder so the component can reconcile without an
575
+ always-on `O(n)` deep watcher.
576
+
577
+ Use the exposed methods, `getScrollPosition()`, and the `scroll` event for
578
+ positive positions measured from the top. The internal DOM scroller uses
579
+ negative `scrollTop` values (zero at the bottom) to preserve native momentum
580
+ while history is prepended; consumers should not read or write that native
581
+ property directly. Message DOM order remains oldest to newest.
572
582
 
573
583
  For smooth media conversations, reserve image/video dimensions with `width`,
574
584
  `height`, or `aspect-ratio`, use an estimate close to your message heights, and
@@ -581,11 +591,13 @@ virtualization cannot hide network latency once that boundary is reached.
581
591
  | Prop | Type | Default | Description |
582
592
  | --- | --- | --- | --- |
583
593
  | `items` | `readonly T[]` | required | Messages ordered from oldest to newest. |
594
+ | `itemsVersion` | `PropertyKey` | unset | Increment after an arbitrary in-place mutation that keeps the same array reference/length. Immutable array replacement does not need it. |
584
595
  | `estimatedItemSize` | `number` | `48` | Initial average message height in pixels. |
596
+ | `estimateItemSize` | `(item, index) => number` | unset | Per-message estimate for cold rows, useful when text and media have very different heights. |
585
597
  | `height` | `number \| string` | `400` | Viewport height. A number is pixels; `"fill"` maps to `100%`. |
586
598
  | `overscan` | `number` | `5` | Extra messages mounted before and after the visible range. |
587
599
  | `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. |
588
- | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key`, or index | Stable identity; especially important when prepending. |
600
+ | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key`, or item identity | Stable unique identity; especially important when prepending. Duplicate keys fail fast instead of corrupting anchors. |
589
601
  | `ariaLabel` | `string` | `"Chat messages"` | Accessible label for the live log. |
590
602
  | `stickToBottom` | `boolean` | `true` | Follows appended messages while currently near the bottom. |
591
603
  | `bottomThreshold` | `number` | `80` | Distance still considered “at bottom”, in pixels. |
@@ -593,6 +605,19 @@ virtualization cannot hide network latency once that boundary is reached.
593
605
  | `hasOlder` | `boolean` | `false` | Indicates that older history is available. |
594
606
  | `loadingOlder` | `boolean` | `false` | Prevents duplicate requests and shows the top indicator. |
595
607
  | `loadOlderThreshold` | `number` | `120` | Distance from the top that triggers `load-older`, in pixels. |
608
+ | `scrollEndDelay` | `number` | `150` | Idle fallback in milliseconds for browsers without reliable `scrollend`. |
609
+ | `syncMeasurement` | `boolean` | `true` | Measure new rows before paint. Set false to avoid synchronous layout reads and rely on `ResizeObserver`. |
610
+ | `pinnedKeys` | `readonly PropertyKey[]` | `[]` | Messages kept mounted outside the visible range to preserve local state. |
611
+ | `maxPinnedItems` | `number` | `20` | Maximum number of explicit pinned keys retained. |
612
+ | `keepFocusedItem` | `boolean` | `true` | Keeps the message containing keyboard focus mounted while off-screen. |
613
+ | `showNewItemsIndicator` | `boolean` | `true` | Shows the built-in new-message button while reading history. |
614
+ | `newItemsLabel` | `(count) => string` | English label | Localizes the built-in new-message button without replacing the slot. |
615
+ | `forceStickToBottom` | `(item, index) => boolean` | unset | Force-follows selected appended messages, commonly messages sent by the local user. |
616
+ | `contain` | `boolean` | `true` | Enables layout/paint containment. Disable for overlays that intentionally escape without `Teleport`. |
617
+
618
+ Pinned and keyboard-focused messages are intentionally additional to the
619
+ `size` render budget. Keep `pinnedKeys` small; the `maxPinnedItems` guard does
620
+ not cap the currently focused message.
596
621
 
597
622
  #### Slots
598
623
 
@@ -601,6 +626,7 @@ virtualization cannot hide network latency once that boundary is reached.
601
626
  | `default` | `{ item, index }` | Renders each mounted message. |
602
627
  | `empty` | none | Rendered when there are no messages. |
603
628
  | `loadingOlder` | none | Sticky top status; defaults to “Loading older messages…”. |
629
+ | `newItems` | `{ count, scrollToBottom }` | Customizes the floating new-message control. |
604
630
 
605
631
  #### Events
606
632
 
@@ -609,14 +635,23 @@ virtualization cannot hide network latency once that boundary is reached.
609
635
  | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. | `@scroll="onScroll"` |
610
636
  | `load-older` | none | Near the top when `hasOlder` is true and `loadingOlder` is false. | `@load-older="loadOlder"` |
611
637
  | `bottom-change` | `boolean` | When the viewport enters or leaves the bottom threshold. | `@bottom-change="onBottomChange"` |
638
+ | `scrolling-change` | `boolean` | When user-driven scrolling starts or settles. | `@scrolling-change="onScrollingChange"` |
639
+ | `new-items-change` | `number` | When the number of appended messages unseen at the bottom changes. | `@new-items-change="onNewItemsChange"` |
640
+ | `visible-change` | `{ scrollTop, startIndex, endIndex }` | When the actual visible range changes, excluding overscan; useful for read tracking. | `@visible-change="markVisibleAsRead"` |
612
641
 
613
642
  #### Exposed API
614
643
 
615
644
  | Method / state | Description | Template example |
616
645
  | --- | --- | --- |
617
646
  | `isAtBottom` | Readonly boolean indicating whether the user is near the bottom. | `chat?.isAtBottom` |
647
+ | `isScrolling` | Readonly boolean indicating an active user scroll gesture. | `chat?.isScrolling` |
648
+ | `newItemsCount` | Number of appended messages received while reading history. | `chat?.newItemsCount` |
649
+ | `clearNewItems()` | Clears the new-message count without moving the viewport. | `chat?.clearNewItems()` |
650
+ | `refresh()` | Reconciles keys and measurements after mutating the same `items` array in place. | `chat?.refresh()` |
651
+ | `getScrollPosition()` | Returns the positive logical pixel offset from the list start. | `chat?.getScrollPosition()` |
618
652
  | `scrollTo(position, options?)` | Scrolls to a pixel offset. | `chat?.scrollTo(200, { behavior: 'smooth' })` |
619
653
  | `scrollToIndex(index, options?)` | Scrolls to a zero-based message index. | `chat?.scrollToIndex(10, { behavior: 'smooth' })` |
654
+ | `scrollToKey(key, options?)` | Scrolls to a message by its stable key. | `chat?.scrollToKey(messageId, { align: 'center' })` |
620
655
  | `scrollToTop(behavior?)` | Scrolls to the oldest loaded message. | `chat?.scrollToTop()` |
621
656
  | `scrollToBottom(behavior?)` | Scrolls to the newest loaded message. | `chat?.scrollToBottom('smooth')` |
622
657
 
@@ -720,8 +755,11 @@ These keys work while the viewport is focused:
720
755
  | `Home` | Moves to the first position. |
721
756
  | `End` | Moves to the last item. |
722
757
 
723
- For an unbounded feed, page data with `load-more` and keep a reasonable item
724
- window in the parent. Virtualization limits mounted DOM and media elements,
758
+ The viewport also converts mouse-wheel input into slide transitions. The wheel
759
+ handler in the component is enabled by default; nothing extra is required from
760
+ the parent. For touch input, native scroll-snap continues to handle swipes.
761
+
762
+ For an unbounded feed, page data with `load-more` and handle parents. Virtualization limits mounted DOM and media elements,
725
763
  but it does not remove objects from `items`.
726
764
 
727
765
  ### VirtualCarousel
@@ -802,8 +840,56 @@ five slides are normally mounted in the middle of the collection.
802
840
  | `autoplayDelay` | `number` | `3000` | Delay in milliseconds between automatic transitions; values below `1` are clamped and non-finite values use the default. |
803
841
  | `autoplayLoop` | `boolean` | `true` | Returns to the first slide when reaching the end. |
804
842
  | `pauseOnHover` | `boolean` | `true` | Pauses autoplay when the carousel is hovered. |
843
+ | `mouseDrag` | `boolean` | `true` | Enables click-and-drag navigation with the primary mouse button. |
844
+ | `dragThreshold` | `number` | `8` | Pointer movement in pixels required to recognize and commit a drag. |
845
+ | `momentum` | `boolean` | `true` | Projects release velocity and decelerates into the destination. |
846
+ | `settleDuration` | `number` | `250` | Base duration in milliseconds for drag/loop settling; `0` makes it instant. Native API scrolling still uses its `behavior` argument. |
847
+ | `nativeSnap` | `boolean` | `true` | Uses native CSS snap for wheel/API scrolling. Set to `false` inside another snap container to settle with the motion controller instead. Loop always uses controlled settling. |
848
+ | `autoWidth` | `boolean` | `false` | Measures each slide's intrinsic width; set the width of the slot content. Overrides `slidesPerView` for sizing. |
849
+ | `estimatedItemWidth` | `number` | `240` | Estimated width in pixels for unmounted slides in `autoWidth` mode. |
850
+ | `rtl` | `boolean` | `false` | Right-to-left layout, pointer movement, native scrolling, and arrow keys. |
851
+ | `loop` | `boolean` | `false` | Continuous looping in either direction, separate from `autoplayLoop`. Disabled when all items fit in the viewport. |
852
+ | `keepAlive` | `boolean` | `false` | Retains recently viewed slide instances and their local state in a bounded cache. |
853
+ | `maxCachedItems` | `number` | `20` | Maximum additional offscreen cached/pinned slides. Visible/buffered slides and one focused slide are not counted. |
854
+ | `pinnedIndexes` | `readonly number[]` | `[]` | Slide indices to retain even with `keepAlive=false`; consumes the offscreen cache budget. |
805
855
  | `responsive` | `ResponsiveBreakpoint[]` | `undefined` | Overrides slide count, scroll step, and gap at container-width breakpoints. |
806
856
 
857
+ Touch gestures use directional locking: vertical gestures continue scrolling the
858
+ page, while horizontal gestures move the carousel and snap to the nearest slide.
859
+ While dragging, the track moves with compositor-friendly `translate3d` and the
860
+ selected `activeIndex` remains unchanged until drag settling finishes. The render
861
+ window and `inViewport` update only when slides enter or leave view. A fast release
862
+ continues with the release velocity and decelerates to rest, with momentum limited
863
+ to two extra slides. Holding before release disables this momentum. A deliberate
864
+ horizontal drag of at least `dragThreshold` pixels commits one slide even when it does not cross the
865
+ slide midpoint.
866
+
867
+ Buttons and links keep their normal click behavior. Form fields and
868
+ `data-carousel-no-drag` descendants do not start dragging; form fields also keep
869
+ their own arrow-key behavior. Wheel/trackpad input interrupts an active animation
870
+ at its displayed position.
871
+
872
+ `autoWidth` uses `ResizeObserver`, cumulative offsets, and binary search. Widths
873
+ of slides not yet mounted are estimates, so distant jumps may be refined when
874
+ those slides are first measured. Late measurements are applied after dragging
875
+ finishes, preserving the selected slide. Give content a resolvable width, for
876
+ example `<ProductCard :style="{ width: item.width + 'px' }" />`.
877
+
878
+ `keepAlive` retains local input/component state until LRU eviction. Only settled
879
+ views update cache recency; intermediate frames do not evict the destination's
880
+ state. Cached offscreen slides are hidden and inert. Use `pinnedIndexes` to retain
881
+ playing media explicitly; pause media with the slot's `inViewport` flag. Persistent
882
+ state should still be stored outside slides by a stable `itemKey`.
883
+
884
+ `loop` repositions each keyed slide around the current cycle without creating
885
+ duplicate component instances. `autoplayLoop` only rewinds a non-looping carousel.
886
+ Loop mode uses the same motion controller to settle wheel input after it stops,
887
+ instead of relying on native snap points that change during cycle recentering.
888
+ The demo disables incremental loading while continuous loop is enabled so that
889
+ the collection's boundaries remain fixed. The component still supports
890
+ `load-more`; changing data or geometry cancels the current transition and realigns
891
+ the selected index.
892
+
807
893
  #### Responsive configuration
808
894
 
809
895
  The largest `breakpoint` not exceeding the carousel container width is used.
@@ -834,7 +920,7 @@ the base props, not to another breakpoint.
834
920
 
835
921
  | Slot | Scope | Description |
836
922
  | --- | --- | --- |
837
- | `default` | `{ item, index, active, visible }` | Renders each mounted slide. |
923
+ | `default` | `{ item, index, active, visible, inViewport }` | `active`/`visible` describe the selected group; `inViewport` reflects actual overlap during motion. |
838
924
  | `empty` | none | Rendered when `items` is empty. |
839
925
 
840
926
  #### Events
@@ -1210,6 +1296,7 @@ import type {
1210
1296
  | `bun run test:browser` | Run Playwright browser tests. |
1211
1297
  | `bun run check` | Run all checks: unit tests, build, consumer types, package lint, and browser tests. |
1212
1298
  | `npm pack --dry-run` | Inspect the files that will be published. |
1299
+ | `npm publish --access public` | Publish the already-versioned public package after the release commit is ready. |
1213
1300
  | `prepublishOnly` | Automatically runs `bun run check` before publishing. |
1214
1301
 
1215
1302
  ```bash
@@ -1221,8 +1308,8 @@ bun run check
1221
1308
 
1222
1309
  The playground includes fixed-height, variable-height, window, chat,
1223
1310
  load-more, short-media, and carousel demos. Before publishing, also run
1224
- `npm pack --dry-run`. GitHub releases are published through
1225
- `.github/workflows/publish.yml`.
1311
+ `npm pack --dry-run`. Commit the release, publish it with
1312
+ `npm publish --access public`, then create and push the matching Git tag.
1226
1313
 
1227
1314
  ## Contributing
1228
1315
 
package/README.vi.md CHANGED
@@ -551,6 +551,9 @@ async function loadOlder() {
551
551
  </template>
552
552
 
553
553
  <template #loadingOlder>Đang tải tin nhắn cũ…</template>
554
+ <template #newItems="{ count, scrollToBottom }">
555
+ <button @click="scrollToBottom('smooth')">{{ count }} tin mới</button>
556
+ </template>
554
557
  <template #empty>Chưa có tin nhắn.</template>
555
558
  </ChatVirtualScroll>
556
559
 
@@ -563,12 +566,18 @@ async function loadOlder() {
563
566
  Hãy thêm dữ liệu cũ vào đầu cùng một collection có key ổn định. Vùng cuộn lấy
564
567
  đáy làm mốc nên việc thêm hoặc đo tin nhắn phía trên người đọc không cần sửa vị
565
568
  trí cuộn native. Tin nhắn xuất hiện lần đầu được đo trước khi vẽ; các thay đổi chiều cao
566
- được cập nhật riêng trong bảng chỉ mục.
567
-
568
- Dùng các method qua ref và sự kiện `scroll` để thao tác với vị trí tính từ đầu
569
- danh sách. Phần tử cuộn DOM bên trong dùng `scrollTop` âm (bằng 0 ở đáy); code
570
- đọc hoặc ghi trực tiếp thuộc tính DOM này cần tính đến khác biệt đó. Thứ tự DOM
571
- của tin nhắn vẫn từ cũ đến mới.
569
+ được cập nhật trong cây chỉ mục theo key. Append, prepend, đo chiều cao, tìm
570
+ offset và key thông thường có độ phức tạp `O(log n)` cho mỗi tin thay đổi; thay
571
+ thế, xóa hoặc sắp xếp tùy ý sẽ dựng lại index.
572
+ Nên thay cả array `items` khi sửa dữ liệu tùy ý. Nếu ứng dụng bắt buộc mutate
573
+ cùng một array, hãy tăng `itemsVersion` (hoặc gọi `refresh()`) sau thao tác xóa/
574
+ sắp xếp không đổi độ dài. Cách này giữ được hiệu năng mà không cần deep watcher
575
+ `O(n)` chạy sau mọi tin nhắn mới.
576
+
577
+ Dùng các method qua ref, `getScrollPosition()` và sự kiện `scroll` để thao tác
578
+ với offset dương tính từ đầu danh sách. Phần tử DOM bên trong dùng `scrollTop`
579
+ âm (bằng 0 ở đáy) nhằm giữ momentum native khi prepend; code sử dụng component
580
+ không nên đọc hoặc ghi trực tiếp thuộc tính này. Thứ tự DOM vẫn từ cũ đến mới.
572
581
 
573
582
  Để chat có ảnh/video cuộn mượt, hãy dành sẵn kích thước bằng `width`, `height`
574
583
  hoặc `aspect-ratio`, đặt chiều cao ước lượng gần thực tế và tránh giới hạn `size`
@@ -581,11 +590,13 @@ kế tiếp trước khi người dùng chạm tin cũ nhất đã có; virtuali
581
590
  | Prop | Kiểu | Mặc định | Mô tả |
582
591
  | --- | --- | --- | --- |
583
592
  | `items` | `readonly T[]` | bắt buộc | Tin nhắn được sắp từ cũ nhất tới mới nhất. |
593
+ | `itemsVersion` | `PropertyKey` | không đặt | Tăng giá trị sau mutation tại chỗ tùy ý không đổi reference/độ dài. Không cần dùng khi thay array theo kiểu immutable. |
584
594
  | `estimatedItemSize` | `number` | `48` | Chiều cao trung bình ban đầu của tin nhắn, tính bằng pixel. |
595
+ | `estimateItemSize` | `(item, index) => number` | không đặt | Ước lượng riêng cho từng tin chưa đo, hữu ích khi text và media chênh lệch chiều cao lớn. |
585
596
  | `height` | `number \| string` | `400` | Chiều cao viewport. Số được hiểu là pixel; `"fill"` tương đương `100%`. |
586
597
  | `overscan` | `number` | `5` | Số tin nhắn mount thêm trước và sau vùng đang hiển thị. |
587
598
  | `size` | `number` | không đặt | Giới hạn tổng số tin nhắn render, bao gồm overscan, ví dụ `:size="12"` cho mobile. Giảm overscan trước; luôn render đủ vùng nhìn dù phải vượt `size`. Không tăng số tin nhắn vượt phạm vi `overscan`. Số dương được làm tròn xuống (tối thiểu 1); số không dương hoặc không hữu hạn sẽ tắt giới hạn. |
588
- | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định, đặc biệt quan trọng khi thêm vào đầu. |
599
+ | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc identity của item | Định danh ổn định và duy nhất, đặc biệt quan trọng khi thêm vào đầu. Key trùng sẽ báo lỗi ngay thay vì làm hỏng anchor. |
589
600
  | `ariaLabel` | `string` | `"Chat messages"` | Nhãn hỗ trợ khả năng tiếp cận cho live log. |
590
601
  | `stickToBottom` | `boolean` | `true` | Bám theo tin nhắn mới nếu hiện tại đang gần cuối. |
591
602
  | `bottomThreshold` | `number` | `80` | Khoảng cách vẫn được xem là “ở cuối”, tính bằng pixel. |
@@ -593,6 +604,18 @@ kế tiếp trước khi người dùng chạm tin cũ nhất đã có; virtuali
593
604
  | `hasOlder` | `boolean` | `false` | Cho biết vẫn còn lịch sử cũ. |
594
605
  | `loadingOlder` | `boolean` | `false` | Ngăn request trùng và hiển thị indicator phía trên. |
595
606
  | `loadOlderThreshold` | `number` | `120` | Khoảng cách tới đầu để emit `load-older`, tính bằng pixel. |
607
+ | `scrollEndDelay` | `number` | `150` | Thời gian chờ fallback tính bằng ms khi trình duyệt không có `scrollend` ổn định. |
608
+ | `syncMeasurement` | `boolean` | `true` | Đo hàng mới trước paint. Đặt false để tránh đọc layout đồng bộ và chỉ dùng `ResizeObserver`. |
609
+ | `pinnedKeys` | `readonly PropertyKey[]` | `[]` | Các tin nhắn luôn được mount để giữ state cục bộ. |
610
+ | `maxPinnedItems` | `number` | `20` | Số key pin tối đa được giữ lại. |
611
+ | `keepFocusedItem` | `boolean` | `true` | Giữ DOM của tin nhắn đang chứa keyboard focus dù ra ngoài viewport. |
612
+ | `showNewItemsIndicator` | `boolean` | `true` | Hiển thị nút tin nhắn mới khi người dùng đang đọc lịch sử. |
613
+ | `newItemsLabel` | `(count) => string` | nhãn tiếng Anh | Bản địa hóa nút tin nhắn mới mặc định mà không cần thay slot. |
614
+ | `forceStickToBottom` | `(item, index) => boolean` | không đặt | Luôn bám đáy với tin append được chọn, thường dùng cho tin do người dùng hiện tại gửi. |
615
+ | `contain` | `boolean` | `true` | Bật containment layout/paint; tắt nếu overlay không dùng `Teleport` cần tràn ra ngoài. |
616
+
617
+ Tin nhắn được pin và tin đang chứa keyboard focus được render thêm ngoài giới
618
+ hạn `size`. Nên giữ `pinnedKeys` nhỏ; `maxPinnedItems` không giới hạn tin đang focus.
596
619
 
597
620
  #### Slots
598
621
 
@@ -601,6 +624,7 @@ kế tiếp trước khi người dùng chạm tin cũ nhất đã có; virtuali
601
624
  | `default` | `{ item, index }` | Render từng tin nhắn đang được mount. |
602
625
  | `empty` | không có | Render khi chưa có tin nhắn. |
603
626
  | `loadingOlder` | không có | Status sticky phía trên; mặc định “Loading older messages…”. |
627
+ | `newItems` | `{ count, scrollToBottom }` | Tùy chỉnh nút tin nhắn mới nổi phía dưới. |
604
628
 
605
629
  #### Events
606
630
 
@@ -609,14 +633,23 @@ kế tiếp trước khi người dùng chạm tin cũ nhất đã có; virtuali
609
633
  | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. | `@scroll="onScroll"` |
610
634
  | `load-older` | không có | Gần đầu khi `hasOlder` là true và `loadingOlder` là false. | `@load-older="loadOlder"` |
611
635
  | `bottom-change` | `boolean` | Khi viewport đi vào hoặc rời ngưỡng ở cuối. | `@bottom-change="onBottomChange"` |
636
+ | `scrolling-change` | `boolean` | Khi thao tác cuộn của người dùng bắt đầu hoặc dừng. | `@scrolling-change="onScrollingChange"` |
637
+ | `new-items-change` | `number` | Khi số tin append chưa được xem ở đáy thay đổi. | `@new-items-change="onNewItemsChange"` |
638
+ | `visible-change` | `{ scrollTop, startIndex, endIndex }` | Khi vùng thực sự nhìn thấy thay đổi, không tính overscan; dùng để theo dõi đã đọc. | `@visible-change="markVisibleAsRead"` |
612
639
 
613
640
  #### API qua ref
614
641
 
615
642
  | Method / trạng thái | Mô tả | Ví dụ trong template |
616
643
  | --- | --- | --- |
617
644
  | `isAtBottom` | Boolean readonly cho biết người dùng có đang gần cuối hay không. | `chat?.isAtBottom` |
645
+ | `isScrolling` | Boolean readonly cho biết người dùng đang thực hiện thao tác cuộn. | `chat?.isScrolling` |
646
+ | `newItemsCount` | Số tin append nhận được trong lúc đang đọc lịch sử. | `chat?.newItemsCount` |
647
+ | `clearNewItems()` | Xóa bộ đếm tin mới mà không đổi vị trí cuộn. | `chat?.clearNewItems()` |
648
+ | `refresh()` | Đồng bộ lại key và measurement sau khi mutate tại chỗ cùng một array `items`. | `chat?.refresh()` |
649
+ | `getScrollPosition()` | Trả về offset pixel logic dương tính từ đầu danh sách. | `chat?.getScrollPosition()` |
618
650
  | `scrollTo(position, options?)` | Cuộn tới offset pixel. | `chat?.scrollTo(200, { behavior: 'smooth' })` |
619
651
  | `scrollToIndex(index, options?)` | Cuộn tới index tin nhắn bắt đầu từ 0. | `chat?.scrollToIndex(10, { behavior: 'smooth' })` |
652
+ | `scrollToKey(key, options?)` | Cuộn tới tin nhắn theo key ổn định. | `chat?.scrollToKey(messageId, { align: 'center' })` |
620
653
  | `scrollToTop(behavior?)` | Cuộn tới tin nhắn cũ nhất đã tải. | `chat?.scrollToTop()` |
621
654
  | `scrollToBottom(behavior?)` | Cuộn tới tin nhắn mới nhất đã tải. | `chat?.scrollToBottom('smooth')` |
622
655
 
@@ -720,6 +753,10 @@ Các phím sau hoạt động khi viewport có focus:
720
753
  | `Home` | Chuyển tới vị trí đầu. |
721
754
  | `End` | Chuyển tới item cuối. |
722
755
 
756
+ Viewport còn chuyển cả input bánh xe chuột thành chuyển slide. Handler wheel đã
757
+ bật sẵn trong component, parent không phải làm gì thêm. Còn touch để trackpad
758
+ snap gốc của trình duyệt xử lý.
759
+
723
760
  Với feed không giới hạn, hãy tải theo trang bằng `load-more` và chỉ giữ một cửa
724
761
  sổ dữ liệu hợp lý ở component cha. Virtualization giới hạn DOM và media element
725
762
  đang mount, nhưng không xóa object khỏi `items`.
@@ -801,8 +838,51 @@ thường tối đa năm slide được mount ở giữa collection.
801
838
  | `autoplayDelay` | `number` | `3000` | Thời gian chờ giữa các lần tự động chuyển, tính bằng mili giây; giá trị dưới `1` được giới hạn và giá trị không hữu hạn dùng mặc định. |
802
839
  | `autoplayLoop` | `boolean` | `true` | Quay lại slide đầu tiên khi đến cuối. |
803
840
  | `pauseOnHover` | `boolean` | `true` | Tạm dừng autoplay khi con trỏ nằm trên carousel. |
841
+ | `mouseDrag` | `boolean` | `true` | Cho phép giữ và kéo bằng nút chuột trái để chuyển slide trên máy tính. |
842
+ | `dragThreshold` | `number` | `8` | Khoảng di chuyển con trỏ để nhận diện và commit thao tác kéo, tính bằng pixel. |
843
+ | `momentum` | `boolean` | `true` | Chiếu vận tốc lúc thả thành quán tính rồi giảm tốc về slide đích. |
844
+ | `settleDuration` | `number` | `250` | Thời lượng nền của animation drag/loop; `0` chuyển tức thì. Cuộn native qua API vẫn dùng tham số `behavior`. |
845
+ | `nativeSnap` | `boolean` | `true` | Dùng CSS snap cho wheel/API. Đặt `false` khi lồng trong container có snap để căn slide bằng bộ điều khiển chuyển động. Loop luôn dùng cơ chế này. |
846
+ | `autoWidth` | `boolean` | `false` | Đo chiều rộng riêng của từng slide; nội dung slot cần có chiều rộng xác định. Thay thế `slidesPerView` khi tính chiều rộng. |
847
+ | `estimatedItemWidth` | `number` | `240` | Chiều rộng ước lượng cho slide chưa được mount khi bật `autoWidth`. |
848
+ | `rtl` | `boolean` | `false` | Layout phải sang trái, bao gồm drag, native scroll và phím mũi tên. |
849
+ | `loop` | `boolean` | `false` | Loop liên tục theo hai hướng, độc lập với `autoplayLoop`. Tự tắt khi toàn bộ item vừa viewport. |
850
+ | `keepAlive` | `boolean` | `false` | Giữ instance và state cục bộ của slide đã xem trong cache giới hạn. |
851
+ | `maxCachedItems` | `number` | `20` | Số slide cache/pin ngoài vùng render tối đa; không tính slide đang hiển thị/buffer và một slide có focus. |
852
+ | `pinnedIndexes` | `readonly number[]` | `[]` | Giữ các index này mounted ngay cả khi `keepAlive=false`; dùng ngân sách cache ngoài vùng render. |
804
853
  | `responsive` | `ResponsiveBreakpoint[]` | `undefined` | Ghi đè số slide hiển thị, bước cuộn và khoảng cách theo breakpoint chiều rộng container. |
805
854
 
855
+ Thao tác cảm ứng có khóa hướng: vuốt dọc tiếp tục cuộn trang, còn vuốt ngang
856
+ di chuyển carousel và tự căn vào slide gần nhất. Trong lúc kéo, track di chuyển
857
+ bằng `translate3d` và `activeIndex` được giữ nguyên tới khi animation drag kết thúc.
858
+ Vùng render và `inViewport` chỉ cập nhật khi slide đi vào hoặc ra khỏi vùng nhìn.
859
+ Khi thả nhanh, track tiếp tục theo vận tốc lúc thả rồi
860
+ giảm tốc về 0, với quán tính giới hạn tối đa hai slide thêm. Giữ tay trước khi
861
+ thả sẽ bỏ qua quán tính. Kéo ngang từ ngưỡng `dragThreshold` sẽ chuyển một slide ngay cả
862
+ khi chưa đi qua nửa chiều rộng slide.
863
+
864
+ Button và link giữ hành vi click bình thường. Input và phần tử có
865
+ `data-carousel-no-drag` không bắt đầu drag; input giữ hành vi phím mũi tên riêng.
866
+ Wheel/trackpad ngắt animation tại vị trí đang hiển thị.
867
+
868
+ `autoWidth` dùng `ResizeObserver`, vị trí cộng dồn và binary search. Slide chưa
869
+ mount dùng chiều rộng ước lượng, nên bước nhảy xa có thể được tinh chỉnh sau khi
870
+ đo. Kết quả đo muộn được áp dụng khi drag kết thúc để giữ slide đang chọn. Nội dung
871
+ slot cần có chiều rộng rõ ràng, ví dụ `<ProductCard :style="{ width: item.width + 'px' }" />`.
872
+
873
+ `keepAlive` giữ state của input/component tới khi bị loại khỏi cache LRU. Chỉ nhóm
874
+ slide khi chuyển động dừng mới cập nhật độ gần đây; frame trung gian không đẩy state
875
+ của slide đích ra khỏi cache. Slide cache ngoài vùng render bị ẩn và inert.
876
+ `pinnedIndexes` giữ media đang chạy; dùng `inViewport` để quyết định pause media.
877
+ State cần tồn tại lâu dài vẫn nên lưu bên ngoài theo `itemKey` ổn định.
878
+
879
+ `loop` di chuyển các slide có key quanh chu kỳ hiện tại, không tạo instance trùng.
880
+ `autoplayLoop` chỉ quay về đầu khi không bật loop liên tục. Demo tắt tải thêm khi
881
+ bật loop để biên collection cố định. Loop dùng cùng bộ điều khiển chuyển động để
882
+ căn slide sau khi wheel dừng, tránh native snap bám theo node vừa đổi chu kỳ.
883
+ Component vẫn hỗ trợ `load-more`. Thay đổi
884
+ dữ liệu hoặc layout sẽ hủy chuyển động hiện tại và căn lại index đang chọn.
885
+
806
886
  #### Cấu hình responsive
807
887
 
808
888
  Component chọn `breakpoint` lớn nhất không vượt quá chiều rộng container của
@@ -833,7 +913,7 @@ Trường bị bỏ qua dùng prop gốc, không kế thừa từ breakpoint kh
833
913
 
834
914
  | Slot | Scope | Mô tả |
835
915
  | --- | --- | --- |
836
- | `default` | `{ item, index, active, visible }` | Render từng slide đang mount. |
916
+ | `default` | `{ item, index, active, visible, inViewport }` | `active`/`visible` theo nhóm slide đã chọn; `inViewport` theo vùng nhìn thực tế trong lúc di chuyển. |
837
917
  | `empty` | không có | Render khi `items` rỗng. |
838
918
 
839
919
  #### Events
@@ -1208,6 +1288,7 @@ import type {
1208
1288
  | `bun run test:browser` | Chạy test trình duyệt bằng Playwright. |
1209
1289
  | `bun run check` | Chạy toàn bộ: unit test, build, type phía dùng package, lint package và test trình duyệt. |
1210
1290
  | `npm pack --dry-run` | Kiểm tra các file sẽ được publish. |
1291
+ | `npm publish --access public` | Publish package public đã được cập nhật version sau khi release commit sẵn sàng. |
1211
1292
  | `prepublishOnly` | Tự chạy `bun run check` trước khi publish. |
1212
1293
 
1213
1294
  ```bash
@@ -1219,8 +1300,8 @@ bun run check
1219
1300
 
1220
1301
  Playground có các demo riêng cho chiều cao cố định, chiều cao động, cuộn theo
1221
1302
  cửa sổ, chat, load-more, short-media và carousel. Trước khi publish, hãy chạy
1222
- thêm `npm pack --dry-run`. GitHub Release được publish qua
1223
- `.github/workflows/publish.yml`.
1303
+ thêm `npm pack --dry-run`. Sau đó commit release, chạy
1304
+ `npm publish --access public`, rồi tạo và push Git tag tương ứng.
1224
1305
 
1225
1306
  ## Đóng góp
1226
1307
 
@@ -4,9 +4,15 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
4
4
  readonly onScroll?: ((event: VirtualScrollEvent) => any) | undefined;
5
5
  readonly onLoadOlder?: (() => any) | undefined;
6
6
  readonly onBottomChange?: ((isAtBottom: boolean) => any) | undefined;
7
- } & import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, never>, "onScroll" | "onLoadOlder" | "onBottomChange"> & {
7
+ readonly onScrollingChange?: ((isScrolling: boolean) => any) | undefined;
8
+ readonly onNewItemsChange?: ((count: number) => any) | undefined;
9
+ readonly onVisibleChange?: ((event: VirtualScrollEvent) => any) | undefined;
10
+ } & import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, never>, "onScroll" | "onLoadOlder" | "onBottomChange" | "onScrollingChange" | "onNewItemsChange" | "onVisibleChange"> & {
8
11
  items: readonly T[];
12
+ /** Increment after mutating/reordering the same array instance in place. */
13
+ itemsVersion?: PropertyKey;
9
14
  estimatedItemSize?: number;
15
+ estimateItemSize?: (item: T, index: number) => number;
10
16
  height?: number | string;
11
17
  overscan?: number;
12
18
  /** Render budget including overscan; always keeps the viewport filled. */
@@ -19,13 +25,30 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
19
25
  hasOlder?: boolean;
20
26
  loadingOlder?: boolean;
21
27
  loadOlderThreshold?: number;
28
+ scrollEndDelay?: number;
29
+ syncMeasurement?: boolean;
30
+ pinnedKeys?: readonly PropertyKey[];
31
+ maxPinnedItems?: number;
32
+ keepFocusedItem?: boolean;
33
+ showNewItemsIndicator?: boolean;
34
+ newItemsLabel?: (count: number) => string;
35
+ forceStickToBottom?: (item: T, index: number) => boolean;
36
+ contain?: boolean;
22
37
  } & Partial<{}>> & import('vue').PublicProps;
23
38
  expose(exposed: import('vue').ShallowUnwrapRef<{
24
39
  isAtBottom: import('vue').Ref<boolean, boolean>;
40
+ isScrolling: import('vue').Ref<boolean, boolean>;
41
+ newItemsCount: import('vue').Ref<number, number>;
42
+ clearNewItems: () => void;
43
+ refresh: () => void;
44
+ getScrollPosition: () => number;
25
45
  scrollTo: (position: number, options?: ScrollToOptions) => void;
26
46
  scrollToIndex: (index: number, options?: ScrollToOptions & {
27
47
  align?: ScrollAlignment;
28
48
  }) => void;
49
+ scrollToKey: (key: PropertyKey, options?: ScrollToOptions & {
50
+ align?: ScrollAlignment;
51
+ }) => void;
29
52
  scrollToTop: (behavior?: ScrollBehavior) => void;
30
53
  scrollToBottom: (behavior?: ScrollBehavior) => void;
31
54
  }>): void;
@@ -37,8 +60,12 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
37
60
  }): unknown;
38
61
  empty?(): unknown;
39
62
  loadingOlder?(): unknown;
63
+ newItems?(props: {
64
+ count: number;
65
+ scrollToBottom: (behavior?: ScrollBehavior) => void;
66
+ }): unknown;
40
67
  };
41
- emit: ((evt: "scroll", event: VirtualScrollEvent) => void) & ((evt: "loadOlder") => void) & ((evt: "bottomChange", isAtBottom: boolean) => void);
68
+ emit: ((evt: "scroll", event: VirtualScrollEvent) => void) & ((evt: "loadOlder") => void) & ((evt: "bottomChange", isAtBottom: boolean) => void) & ((evt: "scrollingChange", isScrolling: boolean) => void) & ((evt: "newItemsChange", count: number) => void) & ((evt: "visibleChange", event: VirtualScrollEvent) => void);
42
69
  }>) => import('vue').VNode & {
43
70
  __ctx?: Awaited<typeof __VLS_setup>;
44
71
  };
@@ -4,9 +4,15 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
4
4
  readonly onScroll?: ((event: VirtualScrollEvent) => any) | undefined;
5
5
  readonly onLoadOlder?: (() => any) | undefined;
6
6
  readonly onBottomChange?: ((isAtBottom: boolean) => any) | undefined;
7
- } & import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, never>, "onScroll" | "onLoadOlder" | "onBottomChange"> & {
7
+ readonly onScrollingChange?: ((isScrolling: boolean) => any) | undefined;
8
+ readonly onNewItemsChange?: ((count: number) => any) | undefined;
9
+ readonly onVisibleChange?: ((event: VirtualScrollEvent) => any) | undefined;
10
+ } & import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, never>, "onScroll" | "onLoadOlder" | "onBottomChange" | "onScrollingChange" | "onNewItemsChange" | "onVisibleChange"> & {
8
11
  items: readonly T[];
12
+ /** Increment after mutating/reordering the same array instance in place. */
13
+ itemsVersion?: PropertyKey;
9
14
  estimatedItemSize?: number;
15
+ estimateItemSize?: (item: T, index: number) => number;
10
16
  height?: number | string;
11
17
  overscan?: number;
12
18
  /** Render budget including overscan; always keeps the viewport filled. */
@@ -19,13 +25,30 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
19
25
  hasOlder?: boolean;
20
26
  loadingOlder?: boolean;
21
27
  loadOlderThreshold?: number;
28
+ scrollEndDelay?: number;
29
+ syncMeasurement?: boolean;
30
+ pinnedKeys?: readonly PropertyKey[];
31
+ maxPinnedItems?: number;
32
+ keepFocusedItem?: boolean;
33
+ showNewItemsIndicator?: boolean;
34
+ newItemsLabel?: (count: number) => string;
35
+ forceStickToBottom?: (item: T, index: number) => boolean;
36
+ contain?: boolean;
22
37
  } & Partial<{}>> & import('vue').PublicProps;
23
38
  expose(exposed: import('vue').ShallowUnwrapRef<{
24
39
  isAtBottom: import('vue').Ref<boolean, boolean>;
40
+ isScrolling: import('vue').Ref<boolean, boolean>;
41
+ newItemsCount: import('vue').Ref<number, number>;
42
+ clearNewItems: () => void;
43
+ refresh: () => void;
44
+ getScrollPosition: () => number;
25
45
  scrollTo: (position: number, options?: ScrollToOptions) => void;
26
46
  scrollToIndex: (index: number, options?: ScrollToOptions & {
27
47
  align?: ScrollAlignment;
28
48
  }) => void;
49
+ scrollToKey: (key: PropertyKey, options?: ScrollToOptions & {
50
+ align?: ScrollAlignment;
51
+ }) => void;
29
52
  scrollToTop: (behavior?: ScrollBehavior) => void;
30
53
  scrollToBottom: (behavior?: ScrollBehavior) => void;
31
54
  }>): void;
@@ -37,8 +60,12 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
37
60
  }): unknown;
38
61
  empty?(): unknown;
39
62
  loadingOlder?(): unknown;
63
+ newItems?(props: {
64
+ count: number;
65
+ scrollToBottom: (behavior?: ScrollBehavior) => void;
66
+ }): unknown;
40
67
  };
41
- emit: ((evt: "scroll", event: VirtualScrollEvent) => void) & ((evt: "loadOlder") => void) & ((evt: "bottomChange", isAtBottom: boolean) => void);
68
+ emit: ((evt: "scroll", event: VirtualScrollEvent) => void) & ((evt: "loadOlder") => void) & ((evt: "bottomChange", isAtBottom: boolean) => void) & ((evt: "scrollingChange", isScrolling: boolean) => void) & ((evt: "newItemsChange", count: number) => void) & ((evt: "visibleChange", event: VirtualScrollEvent) => void);
42
69
  }>) => import('vue').VNode & {
43
70
  __ctx?: Awaited<typeof __VLS_setup>;
44
71
  };