Full Code of Akryum/vue-virtual-scroller for AI

master 5741944941d7 cached
103 files
227.9 KB
64.0k tokens
75 symbols
1 requests
Download .txt
Showing preview only (253K chars total). Download the full file or copy to clipboard to get everything.
Repository: Akryum/vue-virtual-scroller
Branch: master
Commit: 5741944941d7
Files: 103
Total size: 227.9 KB

Directory structure:
gitextract_9c2q8070/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   ├── config.yml
│   │   └── feature-request.yml
│   └── workflows/
│       ├── continuous-publish.yml
│       ├── pr-title.yml
│       ├── release-notes.yml
│       └── test.yml
├── .gitignore
├── .node-version
├── CHANGELOG.md
├── README.md
├── SKILLS-GENERATION.md
├── docs/
│   ├── .vitepress/
│   │   ├── components/
│   │   │   └── demos/
│   │   │       ├── ChatStreamDocDemo.vue
│   │   │       ├── DemoShell.vue
│   │   │       ├── DynamicScrollerDocDemo.vue
│   │   │       ├── GridDocDemo.vue
│   │   │       ├── HorizontalDocDemo.vue
│   │   │       ├── RecycleScrollerDocDemo.vue
│   │   │       ├── SimpleListDocDemo.vue
│   │   │       ├── TestChatDocDemo.vue
│   │   │       └── demo-data.ts
│   │   ├── config.mts
│   │   └── theme/
│   │       ├── index.ts
│   │       └── style.css
│   ├── demos/
│   │   ├── chat.md
│   │   ├── dynamic-scroller.md
│   │   ├── grid.md
│   │   ├── horizontal.md
│   │   ├── index.md
│   │   ├── recycle-scroller.md
│   │   ├── simple-list.md
│   │   └── test-chat.md
│   ├── guide/
│   │   ├── ai-skills.md
│   │   ├── dynamic-scroller-item.md
│   │   ├── dynamic-scroller.md
│   │   ├── id-state.md
│   │   ├── index.md
│   │   ├── recycle-scroller.md
│   │   └── use-recycle-scroller.md
│   └── index.md
├── eslint.config.mjs
├── netlify.toml
├── package.json
├── packages/
│   ├── demo/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── index.html
│   │   ├── src/
│   │   │   ├── App.vue
│   │   │   ├── components/
│   │   │   │   ├── ChatDemo.vue
│   │   │   │   ├── DynamicScrollerDemo.vue
│   │   │   │   ├── GridDemo.vue
│   │   │   │   ├── Home.vue
│   │   │   │   ├── HorizontalDemo.vue
│   │   │   │   ├── Person.vue
│   │   │   │   ├── RecycleScrollerDemo.vue
│   │   │   │   ├── SimpleList.vue
│   │   │   │   └── TestChat.vue
│   │   │   ├── data.js
│   │   │   ├── main.js
│   │   │   └── router.js
│   │   └── vite.config.js
│   └── vue-virtual-scroller/
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       ├── skills/
│       │   └── vue-virtual-scroller/
│       │       ├── SKILL.md
│       │       └── references/
│       │           ├── dynamic-scroller-item.md
│       │           ├── dynamic-scroller.md
│       │           ├── index.md
│       │           ├── installation-and-setup.md
│       │           ├── recycle-scroller.md
│       │           └── use-recycle-scroller.md
│       ├── src/
│       │   ├── components/
│       │   │   ├── DynamicScroller.spec.ts
│       │   │   ├── DynamicScroller.vue
│       │   │   ├── DynamicScrollerItem.spec.ts
│       │   │   ├── DynamicScrollerItem.vue
│       │   │   ├── ItemView.spec.ts
│       │   │   ├── ItemView.vue
│       │   │   ├── RecycleScroller.spec.ts
│       │   │   ├── RecycleScroller.vue
│       │   │   └── ResizeObserver.vue
│       │   ├── composables/
│       │   │   ├── useDynamicScroller.spec.ts
│       │   │   ├── useDynamicScroller.ts
│       │   │   ├── useDynamicScrollerItem.ts
│       │   │   ├── useIdState.spec.ts
│       │   │   ├── useIdState.ts
│       │   │   ├── useRecycleScroller.spec.ts
│       │   │   └── useRecycleScroller.ts
│       │   ├── config.ts
│       │   ├── directives/
│       │   │   └── observeVisibility.ts
│       │   ├── index.spec.ts
│       │   ├── index.ts
│       │   ├── scrollparent.spec.ts
│       │   ├── scrollparent.ts
│       │   ├── shims-vue.d.ts
│       │   ├── types.ts
│       │   ├── utils.spec.ts
│       │   └── utils.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.json
│       └── vite.config.ts
└── pnpm-workspace.yaml

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: Akryum


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: 🐞 Bug report
description: Report an issue with vue-virtual-scroller
labels: [to triage]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report!
  - type: textarea
    id: bug-description
    attributes:
      label: Describe the bug
      description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
      placeholder: Bug description
    validations:
      required: true
  - type: textarea
    id: reproduction
    attributes:
      label: Reproduction
      description: Please provide a link to [StackBlitz](https://stackblitz.com/fork/vue). A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "need repro" label. If no reproduction is provided after 3 days, it will be closed.
      placeholder: Reproduction
    validations:
      required: true
  - type: textarea
    id: system-info
    attributes:
      label: System Info
      description: Output of `npx envinfo --system --npmPackages '{vue,vue-virtual-scroller,vite,@vitejs/*}' --binaries --browsers`
      render: shell
      placeholder: System, Binaries, Browsers
    validations:
      required: true
  - type: dropdown
    id: package-manager
    attributes:
      label: Used Package Manager
      description: Select the used package manager
      options:
        - npm
        - yarn
        - pnpm
    validations:
      required: true
  - type: checkboxes
    id: checkboxes
    attributes:
      label: Validations
      description: Before submitting the issue, please make sure you do the following
      options:
        # - label: Follow our [Code of Conduct](https://github.com/histoire-dev/histoire/blob/main/CODE_OF_CONDUCT.md)
        #   required: true
        # - label: Read the [Contributing Guidelines](https://github.com/histoire-dev/histoire/blob/main/CONTRIBUTING.md).
        #   required: true
        - label: Read the [docs](https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md).
          required: true
        - label: Check that there isn't [already an issue](https://github.com/Akryum/vue-virtual-scroller/issues) that reports the same bug to avoid creating a duplicate.
          required: true
        - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/Akryum/vue-virtual-scroller/discussions).
          required: true
        - label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.
          required: true


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Questions & Discussions
    url: https://github.com/Akryum/vue-virtual-scroller/discussions
    about: Use GitHub discussions for message-board style questions and discussions.


================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.yml
================================================
name: 🚀 New feature proposal
description: Propose a new feature to be added to vue-virtual-scroller
labels: ['enhancement: to triage']
body:
  - type: markdown
    attributes:
      value: |
        Thanks for your interest in the project and taking the time to fill out this feature report!
  - type: textarea
    id: feature-description
    attributes:
      label: Clear and concise description of the problem
      description: 'As a developer using vue-virtual-scroller I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!'
    validations:
      required: true
  - type: textarea
    id: suggested-solution
    attributes:
      label: Suggested solution
      description: We could provide following implementation...
    validations:
      required: true
  - type: textarea
    id: alternative
    attributes:
      label: Alternative
      description: Clear and concise description of any alternative solutions or features you've considered.
  - type: textarea
    id: additional-context
    attributes:
      label: Additional context
      description: Any other context or screenshots about the feature request here.
  - type: checkboxes
    id: checkboxes
    attributes:
      label: Validations
      description: Before submitting the issue, please make sure you do the following
      options:
        # - label: Follow our [Code of Conduct](https://github.com/histoire-dev/histoire/blob/main/CODE_OF_CONDUCT.md)
        #   required: true
        # - label: Read the [Contributing Guidelines](https://github.com/histoire-dev/histoire/blob/main/CONTRIBUTING.md).
        #   required: true
        - label: Read the [docs](https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md).
          required: true
        - label: Check that there isn't [already an issue](https://github.com/Akryum/vue-virtual-scroller/issues) that reports the same bug to avoid creating a duplicate.
          required: true


================================================
FILE: .github/workflows/continuous-publish.yml
================================================
name: Publish Any Commit
on: [push, pull_request]

jobs:
  auto-publish:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - run: corepack enable
      - uses: actions/setup-node@v4
        with:
          node-version: latest
          cache: pnpm

      - name: Install dependencies
        run: pnpm install

      - name: Build
        run: pnpm build

      - run: pnpx pkg-pr-new publish './packages/*'


================================================
FILE: .github/workflows/pr-title.yml
================================================
name: Check PR title

on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize

jobs:
  check-title:
    runs-on: ubuntu-latest
    steps:
      # Please look up the latest version from
      # https://github.com/amannn/action-semantic-pull-request/releases
      - uses: amannn/action-semantic-pull-request@v3.4.2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release-notes.yml
================================================
name: Create release

on:
  push:
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

jobs:
  build:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@master
        with:
          fetch-depth: 0 # Fetch all tags

      - name: Create Release for Tag
        id: release_tag
        uses: Akryum/release-tag@conventional
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          preset: angular # Use conventional-changelog preset


================================================
FILE: .github/workflows/test.yml
================================================
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - run: corepack enable
      - uses: actions/setup-node@v4
        with:
          node-version: latest
          cache: pnpm

      - name: Install dependencies
        run: pnpm install

      - name: Build
        run: pnpm build

      - name: ESLint
        run: pnpm lint


================================================
FILE: .gitignore
================================================
node_modules/
.temp/
.cache/
dist/
.eslintcache
docs/.vitepress/cache
docs/.vitepress/dist


================================================
FILE: .node-version
================================================
25.8.0


================================================
FILE: CHANGELOG.md
================================================
## v2.0.0-beta.10

[compare changes](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.9...v2.0.0-beta.10)

### 🩹 Fixes

- Empty slot ([5791945](https://github.com/Akryum/vue-virtual-scroller/commit/5791945))

### 🏡 Chore

- Changelog ([2ae2195](https://github.com/Akryum/vue-virtual-scroller/commit/2ae2195))

### ❤️ Contributors

- Guillaume Chau ([@Akryum](http://github.com/Akryum))

## v2.0.0-beta.9

[compare changes](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.8...v2.0.0-beta.9)

### 🚀 Enhancements

- Items ref ([#789](https://github.com/Akryum/vue-virtual-scroller/pull/789))
- New `disableTransform` prop to use top/left instead of translate ([#138](https://github.com/Akryum/vue-virtual-scroller/pull/138))
- Typescript / composition rewrite, new docs ([dff69e8](https://github.com/Akryum/vue-virtual-scroller/commit/dff69e8))
- AI skills ([8e58315](https://github.com/Akryum/vue-virtual-scroller/commit/8e58315))

### 🩹 Fixes

- Index lost, fix #783 ([#784](https://github.com/Akryum/vue-virtual-scroller/pull/784), [#783](https://github.com/Akryum/vue-virtual-scroller/issues/783))
- Avoid rendering when slot is unused ([#787](https://github.com/Akryum/vue-virtual-scroller/pull/787))
- Rewrite view (re-)assignment logic ([#743](https://github.com/Akryum/vue-virtual-scroller/pull/743))
- **RecycleScroller:** Introduce an item wrapper to reduce re-render ([#742](https://github.com/Akryum/vue-virtual-scroller/pull/742))
- Flicker issue in ios when scrolling up ([#864](https://github.com/Akryum/vue-virtual-scroller/pull/864))
- Hide view to avoid overlap when position is set to -9999px; ([#837](https://github.com/Akryum/vue-virtual-scroller/pull/837))
- Prevent empty gaps on fast scrolling, fix (#863, #882) ([#890](https://github.com/Akryum/vue-virtual-scroller/pull/890), [#863](https://github.com/Akryum/vue-virtual-scroller/issues/863), [#882](https://github.com/Akryum/vue-virtual-scroller/issues/882))
- Recycle scroller visible gaps computation ([5940707](https://github.com/Akryum/vue-virtual-scroller/commit/5940707))
- Small improvements ([013279c](https://github.com/Akryum/vue-virtual-scroller/commit/013279c))
- Build ([8efdef2](https://github.com/Akryum/vue-virtual-scroller/commit/8efdef2))

### 💅 Refactors

- Esm only ([4ffe378](https://github.com/Akryum/vue-virtual-scroller/commit/4ffe378))

### 📖 Documentation

- Fix dead link ([c6a3320](https://github.com/Akryum/vue-virtual-scroller/commit/c6a3320))
- Update readmes ([828c184](https://github.com/Akryum/vue-virtual-scroller/commit/828c184))

### 🏡 Chore

- Fix cherrypick of rewrite ([c9ccc34](https://github.com/Akryum/vue-virtual-scroller/commit/c9ccc34))
- Update lockfile ([0f2e362](https://github.com/Akryum/vue-virtual-scroller/commit/0f2e362))
- Update pnpm + pin pnpm in package.json ([#885](https://github.com/Akryum/vue-virtual-scroller/pull/885))
- Add pkg.pr.new ([#886](https://github.com/Akryum/vue-virtual-scroller/pull/886))
- Add test workflow ([#887](https://github.com/Akryum/vue-virtual-scroller/pull/887))
- Update pnpm and refresh lockfile ([47efc94](https://github.com/Akryum/vue-virtual-scroller/commit/47efc94))

### ✅ Tests

- **lint:** Update eslint and use antfu config ([61b9919](https://github.com/Akryum/vue-virtual-scroller/commit/61b9919))
- **lint:** Fix ([1e2e8e0](https://github.com/Akryum/vue-virtual-scroller/commit/1e2e8e0))

### ❤️ Contributors

- Guillaume Chau ([@Akryum](http://github.com/Akryum))
- Ferflores507 ([@ferflores507](http://github.com/ferflores507))
- KaygNas <597857074@QQ.COM>
- Hobywhan ([@hobywhan](http://github.com/hobywhan))
- Wan Zulsarhan Wan Shaari <zulsarhan.shaari@gmail.com>
- Tatsuyuki Ishi ([@ishitatsuyuki](http://github.com/ishitatsuyuki))
- AousAnwar ([@AousAnwar](http://github.com/AousAnwar))
- Alex Liu ([@Mini-ghost](http://github.com/Mini-ghost))
- Reynaldiaznan123 <reynaldiaznan450@gmail.com>
- Vito ([@liu-lihao](http://github.com/liu-lihao))


# [2.0.0-beta.8](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2023-02-06)



### Bug Fixes

* borderBoxSize not available in older browsers ([8f90971](https://github.com/Akryum/vue-virtual-scroller/commit/8f9097138d2f90ece8348141ac320c47ff7ab64a))



# [2.0.0-beta.7](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.6...v2.0.0-beta.7) (2022-12-14)


### Bug Fixes

* items not updating if new object reference, fix [#690](https://github.com/Akryum/vue-virtual-scroller/issues/690) ([5b5df8c](https://github.com/Akryum/vue-virtual-scroller/commit/5b5df8cdc231f989e7fc6d6677d02e9ef695d1b9))



# [2.0.0-beta.6](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2022-12-14)


### Bug Fixes

* keyField issue for class instances, fix [#770](https://github.com/Akryum/vue-virtual-scroller/issues/770) ([#771](https://github.com/Akryum/vue-virtual-scroller/issues/771)) ([1559ca8](https://github.com/Akryum/vue-virtual-scroller/commit/1559ca87e9195b6a1c5bada13de7f7b755a2fb6c))
* **RecycleScroller:** gridItems is undefined when scrollToItem, fix [#773](https://github.com/Akryum/vue-virtual-scroller/issues/773) ([#761](https://github.com/Akryum/vue-virtual-scroller/issues/761)) ([7c809ad](https://github.com/Akryum/vue-virtual-scroller/commit/7c809ad1d612824867490c7bd5ce2861110412eb))
* sorting views not working, [#772](https://github.com/Akryum/vue-virtual-scroller/issues/772) ([0b199d1](https://github.com/Akryum/vue-virtual-scroller/commit/0b199d14c846ecc00b93f989adbe29961dc68aad))
* view not unused if item no longer present, fix [#774](https://github.com/Akryum/vue-virtual-scroller/issues/774) ([bd51403](https://github.com/Akryum/vue-virtual-scroller/commit/bd514031f537978f0343317bb9cee550c5bfd7ad))
* views not reused correctly ([d5a8d75](https://github.com/Akryum/vue-virtual-scroller/commit/d5a8d759090f9af656865dd98648941fb2c71fa2))


### Features

* allow throttling update calls ([#764](https://github.com/Akryum/vue-virtual-scroller/issues/764)) ([9ba57d7](https://github.com/Akryum/vue-virtual-scroller/commit/9ba57d7d84c06d2ad265a266958292081704f218))



# [2.0.0-beta.5](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2022-12-07)


### Bug Fixes

* duplicate active views ([1ef796b](https://github.com/Akryum/vue-virtual-scroller/commit/1ef796b42143da6d4e74f83b8ac88176128e6d77))
* **DynamicScroller:** gaps caused by DOM reusing not triggering ResizeObserver ([a21e191](https://github.com/Akryum/vue-virtual-scroller/commit/a21e1915d76741a2806abd3a702d450f722879c8))
* inconsistent state on reused view ([a14747d](https://github.com/Akryum/vue-virtual-scroller/commit/a14747d33d75eaf7fe820370436d70e82562939b))
* views map corruption + view not removed from unusedPool ([cef8860](https://github.com/Akryum/vue-virtual-scroller/commit/cef886085c52f62736cf4c404a32f4f4fce6d229))


### Performance Improvements

* unnecessary loop ([86d0d07](https://github.com/Akryum/vue-virtual-scroller/commit/86d0d0776e26542d1b94484ec6ff5410733d3f18))



# [2.0.0-beta.4](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2022-12-06)


### Bug Fixes

* improved dynamic scroller resize observer logic ([40f58b3](https://github.com/Akryum/vue-virtual-scroller/commit/40f58b3e3a411df36c09d59cc3776719f60d93cf))
* item sizes getting 'disabled' resulting in gaps ([55b4ab1](https://github.com/Akryum/vue-virtual-scroller/commit/55b4ab1df1b4998178f2f03a53c112086a2633f2))
* unusing views after non-continuous scroll ([11488b7](https://github.com/Akryum/vue-virtual-scroller/commit/11488b7d8ffdfe1384fe808e4a49c1ba95ad1383))
* views incorrectly unused (proxy identity comparison) ([395bbfb](https://github.com/Akryum/vue-virtual-scroller/commit/395bbfb73588455795ecc5b144281ce5fda042ff))



# [2.0.0-beta.3](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2022-10-18)


### Performance Improvements

* small code changes to maximize performance ([3b4dbf3](https://github.com/Akryum/vue-virtual-scroller/commit/3b4dbf39f480745d53e4bb43217c2b35975e4ab6))


### Reverts

* pass key-field prop [#732](https://github.com/Akryum/vue-virtual-scroller/issues/732), fix [#758](https://github.com/Akryum/vue-virtual-scroller/issues/758) ([8d221e6](https://github.com/Akryum/vue-virtual-scroller/commit/8d221e6978e4924ab125337fc91f6b6de7a1f497))



# [2.0.0-beta.2](https://github.com/Akryum/vue-virtual-scroller/compare/v1.1.1...v2.0.0-beta.2) (2022-10-17)

### Bug Fixes

* fix: height NaN, fix [#757](https://github.com/Akryum/vue-virtual-scroller/issues/757)



# [2.0.0-beta.1](https://github.com/Akryum/vue-virtual-scroller/compare/v1.1.0...v2.0.0-beta.1) (2022-10-15)


### Bug Fixes

* Account for the height of the leading and trailing slots when calculating visible items, fix [#685](https://github.com/Akryum/vue-virtual-scroller/issues/685) ([24ab3ba](https://github.com/Akryum/vue-virtual-scroller/commit/24ab3ba773d5819fcbe29f13eab663d48bce73ca))
* avoid jumping scroll position when upper item size is calculated ([#374](https://github.com/Akryum/vue-virtual-scroller/issues/374)) ([fd58a95](https://github.com/Akryum/vue-virtual-scroller/commit/fd58a95392c98b8e67da66235fcf4cac78ea2fd4))
* clamp endIndex if less items than prerender ([#473](https://github.com/Akryum/vue-virtual-scroller/issues/473)) ([f9124aa](https://github.com/Akryum/vue-virtual-scroller/commit/f9124aa81c36b46df339a5f18e0e832ab6e5a580))
* DynamicScroller should pass its own keyField prop to child RecycleScroller ([#732](https://github.com/Akryum/vue-virtual-scroller/issues/732)) ([9673679](https://github.com/Akryum/vue-virtual-scroller/commit/9673679fc174cd6236fae4e19a9b1a3b625e900e))
* **DynamicScrollerItem:** watch item prop ([#700](https://github.com/Akryum/vue-virtual-scroller/issues/700)) ([4d3b956](https://github.com/Akryum/vue-virtual-scroller/commit/4d3b95651610b8396c8dff66af9267407eab8e72))
* issue with beforeDestroy hook ([#748](https://github.com/Akryum/vue-virtual-scroller/issues/748)) ([59f3f1b](https://github.com/Akryum/vue-virtual-scroller/commit/59f3f1b0aee9ab8ea276fee60e204b6dcc0baceb))
* merge ([c8363b1](https://github.com/Akryum/vue-virtual-scroller/commit/c8363b114f691042dbced3b5b79d2ebd7812f481))
* restore scroll in keep-alive ([#724](https://github.com/Akryum/vue-virtual-scroller/issues/724)) ([5011e06](https://github.com/Akryum/vue-virtual-scroller/commit/5011e06f2aa6ef8afa6ecaad804413e56a542c8d))
* scrollToItem works with pageMode ([#396](https://github.com/Akryum/vue-virtual-scroller/issues/396)) ([c9772bf](https://github.com/Akryum/vue-virtual-scroller/commit/c9772bfb9e87672de1480072c4d5dc8024d1e5d1))
* wrap the callback in requestAnimationFrame, fix [#516](https://github.com/Akryum/vue-virtual-scroller/issues/516) ([#517](https://github.com/Akryum/vue-virtual-scroller/issues/517)) ([6f359ab](https://github.com/Akryum/vue-virtual-scroller/commit/6f359abed6cf5d81a05d3760d6b622153f331f01))


### Features

* add an empty slot ([#398](https://github.com/Akryum/vue-virtual-scroller/issues/398)) ([5c2715c](https://github.com/Akryum/vue-virtual-scroller/commit/5c2715c0a2c52b0c27436baabbf982fcb9861131))
* add skipHover prop to deactive the hover detection ([#752](https://github.com/Akryum/vue-virtual-scroller/issues/752)) ([b613318](https://github.com/Akryum/vue-virtual-scroller/commit/b613318a52d4d8f84bda69f0189f27dd51d0aaff))
* adds configurable list/item tags for semantic html ([#203](https://github.com/Akryum/vue-virtual-scroller/issues/203)) ([3d24dc3](https://github.com/Akryum/vue-virtual-scroller/commit/3d24dc31928ec9eabe74294e5d5b3466109e1bc2))
* custom classes for list wrapper and list items. ([#397](https://github.com/Akryum/vue-virtual-scroller/issues/397)) ([32b285d](https://github.com/Akryum/vue-virtual-scroller/commit/32b285d40667870b65c71dc59b02627f97c67ea4))
* Emit events for scroll to begin and end of list ([#364](https://github.com/Akryum/vue-virtual-scroller/issues/364)) ([2a7bfd4](https://github.com/Akryum/vue-virtual-scroller/commit/2a7bfd45e1ee56e82426a67d9f3f3ba5a7839185))
* gridItems prop ([#27](https://github.com/Akryum/vue-virtual-scroller/issues/27)) ([6339e72](https://github.com/Akryum/vue-virtual-scroller/commit/6339e72693c982805648ae3001b7c2957d8aa39e))
* itemSecondarySize ([43d311c](https://github.com/Akryum/vue-virtual-scroller/commit/43d311c2f336de74da4d0ec705b0a3546eeda153))
* throw error when key field does not exist in item ([#265](https://github.com/Akryum/vue-virtual-scroller/issues/265)) ([c63129f](https://github.com/Akryum/vue-virtual-scroller/commit/c63129fdc8264d25c737db1c2ce2891a9b804705))
* update event provide range of the visible items ([#115](https://github.com/Akryum/vue-virtual-scroller/issues/115)) ([f19af6c](https://github.com/Akryum/vue-virtual-scroller/commit/f19af6c15346ff33e5d3c4b9729b02a73d5fe4df))


### Performance Improvements

* skipHover: don't add event listeners ([6b623b5](https://github.com/Akryum/vue-virtual-scroller/commit/6b623b56e4ab481b1e0cde883682df2cc81edf19))





================================================
FILE: README.md
================================================
# vue-virtual-scroller

[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg) ![npm](https://img.shields.io/npm/dm/vue-virtual-scroller.svg)](https://www.npmjs.com/package/vue-virtual-scroller)
[![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/)

[Documentation](https://vue-virtual-scroller.netlify.app/)

Blazing fast scrolling of any amount of data | [Live demo](https://vue-virtual-scroller-demo.netlify.app/) | [Video demo](https://www.youtube.com/watch?v=Uzq1KQV8f4k)

For Vue 2 support, see [here](https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller)

This package ships ESM only in the current Vue 3 line. Use it with an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.

[💚️ Become a Sponsor](https://github.com/sponsors/Akryum)

## Sponsors

<p align="center">
  <a href="https://guillaume-chau.info/sponsors/" target="_blank">
    <img src='https://akryum.netlify.app/sponsors.svg' alt="sponsors" />
  </a>
</p>


================================================
FILE: SKILLS-GENERATION.md
================================================
# Skills Generation (vue-virtual-scroller)

This file is the canonical process for generating and updating package skills in this repository.

## Scope

This process currently covers one package skill:

- `packages/vue-virtual-scroller/skills/vue-virtual-scroller`

This process does not cover:

- `packages/demo` as a standalone skill target
- internal implementation-only helpers that are not documented as public APIs

## Sources of truth

Always generate skill content from public documentation first, not from memory.

If implementation behavior appears to differ from docs, fix docs first, then regenerate the skill from the updated docs.

### Primary docs

- `docs/index.md`
- `docs/guide/index.md`
- `docs/guide/recycle-scroller.md`
- `docs/guide/dynamic-scroller.md`
- `docs/guide/dynamic-scroller-item.md`
- `docs/guide/id-state.md`
- `docs/guide/use-recycle-scroller.md`

### Supporting examples

Use these to sharpen examples and workflow guidance, not to invent undocumented API behavior:

- `docs/demos/index.md`
- `docs/demos/recycle-scroller.md`
- `docs/demos/dynamic-scroller.md`
- `docs/demos/chat.md`
- `docs/demos/simple-list.md`
- `docs/demos/horizontal.md`
- `docs/demos/grid.md`
- `docs/demos/test-chat.md`
- `packages/demo/src/**`

### Public-export verification

Use these only to verify package exports, option names, and known docs gaps:

- `packages/vue-virtual-scroller/src/index.ts`
- `packages/vue-virtual-scroller/src/types.ts`
- `packages/vue-virtual-scroller/README.md`

If an exported surface is not documented enough to support skill content, update docs first or explicitly leave that surface out of the generated skill.

Current areas that require extra care:

- `docs/guide/id-state.md` describes `IdState`, while the current package exports `useIdState`
- `useDynamicScroller` and `useDynamicScrollerItem` are exported but do not currently have guide pages
- plugin install options such as `installComponents` and `componentsPrefix` are exported but not fully documented in the guide

Do not silently fill those gaps from source code into the skill. Either document them first or keep them out of the generated skill.

## Output files

Generate one skill folder for the published package:

1. `packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md`
2. `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md`
3. One reference file per documented public surface or recurring workflow

Expected initial reference set for this repo:

- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md`
- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md`
- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md`
- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md`
- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md`

Optional reference files:

- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/id-state.md` only after docs and exports are reconciled
- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/patterns-and-guardrails.md` if the core skill becomes too dense

Do not force an `api-*.md` naming pattern here. This repo is better represented by one file per component, composable, or usage decision.

## Required `SKILL.md` structure

Each generated `SKILL.md` should include:

1. YAML frontmatter:
   - `name`
   - `description` as a single line that clearly mentions Vue virtual scrolling, `RecycleScroller`, `DynamicScroller`, and headless usage so the skill triggers correctly
2. Title and one-line summary
3. A quick decision table for when to use:
   - `RecycleScroller`
   - `DynamicScroller`
   - `DynamicScrollerItem`
   - `useRecycleScroller`
4. Setup snippet that includes:
   - package install
   - ESM-only note
   - CSS import (`vue-virtual-scroller/index.css`)
   - plugin install or direct component import
5. Practical guidance sections for:
   - choosing fixed-size vs variable-size rendering
   - when to switch from `RecycleScroller` to `DynamicScroller`
   - required sizing/CSS constraints
   - performance guardrails and reuse pitfalls
   - common layouts such as chat feeds, grids, and horizontal scrollers
6. References section containing a table with `Topic`, `Description`, and `Reference`
7. Further reading section linking only to shipped reference files and, if needed, stable package-level external URLs

## Required references structure

Each skill should include a `references/` folder with surface-focused reference files. Keep references one level deep from `SKILL.md`.

Mandatory layout:

- `references/index.md`: maps each documented surface and workflow topic to exactly one reference file
- `references/<surface>.md`: one file per documented public surface or focused workflow

Each reference file should:

- start with a short title and one-line scope
- include a short provenance section without referencing repository-local file paths outside the published package
- include sections in this order when possible:
  - `When to use`
  - `Required inputs`
  - `Core props/options`
  - `Events/returns`
  - `Pitfalls`
  - `Example patterns`
- stay grounded in current docs
- focus on user-facing behavior, not internal implementation detail
- avoid chaining into nested references
- avoid bundling unrelated surfaces into one file

## Writing constraints

- Keep guidance practical and tied to current public behavior.
- Prefer decisions and guardrails over generic marketing language.
- Always mention that the package is Vue 3 and ESM-only when setup is discussed.
- Always mention the required CSS import when installation/setup is discussed.
- Do not invent APIs, props, events, or helper functions that are not documented.
- Do not describe Vue 2 usage in the generated skill for this repo.
- Use demo pages to illustrate patterns such as chat streams, grids, horizontal scrolling, and stress-tested append flows.
- Never reference repository-local files outside the published package from `SKILL.md` or `references/*.md`.
- In shipped skill files, only link to other shipped skill files or stable external package URLs.
- If the repo documents AI-agent consumption with `skills-npm`, keep that guidance in the VitePress docs, not in the shipped skill files.
- Do not generate or update `agents/openai.yaml` for this workflow.

## Generation workflow

### 1. Gather context

```bash
rg --files docs packages/vue-virtual-scroller
rg -n "RecycleScroller|DynamicScroller|DynamicScrollerItem|useRecycleScroller|useDynamicScroller|useDynamicScrollerItem|useIdState|installComponents|componentsPrefix|ESM" \
  docs \
  packages/vue-virtual-scroller/src/index.ts \
  packages/vue-virtual-scroller/src/types.ts \
  packages/vue-virtual-scroller/README.md
```

Read the primary docs files first.

Use demos and package exports to check scope and examples.

If docs are missing, outdated, or contradictory, update docs first and use the updated docs as the generation input.

### 2. Decide skill coverage

Start from documented public surfaces only.

For the current repo baseline, the minimum covered surfaces should be:

- installation/setup
- `RecycleScroller`
- `DynamicScroller`
- `DynamicScrollerItem`
- `useRecycleScroller`

Only add `id-state`, `useDynamicScroller`, `useDynamicScrollerItem`, or plugin option references after the docs clearly support them.

### 3. Generate or update the skill

- Regenerate `SKILL.md` using the required structure above.
- Regenerate `references/index.md`.
- Regenerate one reference file per documented surface or workflow topic.
- Ensure `SKILL.md` links to `references/index.md`.
- Keep the top-level skill concise and move detail into `references/*.md`.

### 4. Validate generated skill files

```bash
sed -n '1,260p' packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md
sed -n '1,260p' packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md
rg --files packages/vue-virtual-scroller/skills/vue-virtual-scroller/references
```

Checklist:

- [ ] Frontmatter is valid and the description is specific enough to trigger on virtual scrolling tasks.
- [ ] Setup guidance includes the ESM-only note and the CSS import.
- [ ] The choose-the-right-surface guidance distinguishes `RecycleScroller`, `DynamicScroller`, and headless usage correctly.
- [ ] Fixed-size, variable-size, page mode, and common performance pitfalls are grounded in current docs.
- [ ] `references/index.md` exists and links to every reference file.
- [ ] Each reference file covers one surface or one focused workflow only.
- [ ] No shipped skill file links to repository-local paths outside the package.
- [ ] Any API not fully documented in the guide has been omitted or documented first.

### 5. Record generation metadata

After regeneration, update this document with:

- generation date
- docs/package baseline commit SHA
- version notes if the public package surface changed
- generated artifacts

## Incremental update process

When docs or public exports change, update only impacted skill sections.

```bash
git diff <last_skill_sha>..HEAD -- docs packages/vue-virtual-scroller/src/index.ts packages/vue-virtual-scroller/src/types.ts packages/vue-virtual-scroller/README.md
git diff --name-only <last_skill_sha>..HEAD -- docs packages/vue-virtual-scroller/src/index.ts packages/vue-virtual-scroller/src/types.ts packages/vue-virtual-scroller/README.md
```

Then:

1. Map changed docs or export files to affected skill sections.
2. If exports changed without docs updates, fix docs first unless the skill already omits that surface.
3. Update only the affected `SKILL.md` sections and reference files.
4. Re-run the validation checklist.
5. Refresh metadata below.

## Current generation metadata

- Last generation date: `2026-03-10T14:40:32+01:00`
- Baseline commit SHA: `4ffe378192353c24e474d7541c649613458cf1eb`
- Baseline short SHA: `4ffe378`
- Baseline commit date: `2026-03-10T14:25:42+01:00`
- Baseline commit message: `refactor: esm only`
- Generated artifacts:
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md`
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md`
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md`
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md`
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md`
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md`
  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md`

## Notes

- There is no dedicated generation script in this repository yet.
- Generation is currently a documented manual process with reproducible inspection commands.
- `packages/demo` exists as an example application and validation aid, not as a primary skill output target.
- To make shipped skills consumable through `skills-npm`, the published `vue-virtual-scroller` package must include the `skills/` directory in its packaged files.


================================================
FILE: docs/.vitepress/components/demos/ChatStreamDocDemo.vue
================================================
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue'
import DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'
import DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'
import { avatarStyle, createMessages } from './demo-data'
import DemoShell from './DemoShell.vue'

const scroller = ref<InstanceType<typeof DynamicScroller>>()
const basePool = createMessages(1500, 303)

let nextId = 1
const stream = ref(createMessages(20, 707).map(item => ({ ...item, id: nextId++ })))
const search = ref('')
const streaming = ref(false)

let streamTimer: ReturnType<typeof setInterval> | undefined

const filteredItems = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return stream.value
  return stream.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))
})

function appendBatch(amount = 8) {
  for (let i = 0; i < amount; i++) {
    const template = basePool[(nextId + i) % basePool.length]
    stream.value.push({
      ...template,
      id: nextId++,
      timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
    })
  }
  requestAnimationFrame(() => scroller.value?.scrollToBottom())
}

function startStream() {
  if (streaming.value)
    return
  streaming.value = true
  appendBatch(12)
  streamTimer = setInterval(() => {
    appendBatch(6)
  }, 320)
}

function stopStream() {
  streaming.value = false
  if (streamTimer) {
    clearInterval(streamTimer)
    streamTimer = undefined
  }
}

onBeforeUnmount(stopStream)
</script>

<template>
  <DemoShell
    title="Chat stream"
    description="Ported from the streaming chat demo. New rows are pushed continuously and the view auto-scrolls to bottom."
  >
    <template #toolbar>
      <button
        v-if="!streaming"
        class="demo-button"
        @click="startStream"
      >
        Start stream
      </button>
      <button
        v-else
        class="demo-button secondary"
        @click="stopStream"
      >
        Stop stream
      </button>

      <button
        class="demo-button secondary"
        @click="appendBatch(20)"
      >
        +20 messages
      </button>

      <label class="demo-chip">
        Filter
        <input
          v-model="search"
          type="text"
          placeholder="Search"
        >
      </label>

      <span class="demo-chip">Rows: {{ filteredItems.length }}</span>
    </template>

    <DynamicScroller
      ref="scroller"
      class="demo-viewport"
      :items="filteredItems"
      :min-item-size="62"
    >
      <template #before>
        <div class="demo-notice">
          The Stream demo appends data in real time while preserving smooth scrolling.
        </div>
      </template>

      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[item.message]"
          class="demo-message-row"
        >
          <div
            class="demo-avatar"
            :style="avatarStyle(item.hue)"
          >
            {{ item.initials }}
          </div>

          <div class="demo-chat-bubble">
            <strong>{{ item.user }}</strong>
            <div class="demo-message-body">
              {{ item.message }}
            </div>
          </div>

          <small class="demo-message-meta">#{{ index }} · {{ item.timestamp }}</small>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/DemoShell.vue
================================================
<script setup lang="ts">
defineProps<{
  title: string
  description: string
}>()
</script>

<template>
  <section class="demo-shell">
    <header class="demo-shell__header">
      <h3 class="demo-shell__title">
        {{ title }}
      </h3>
      <p class="demo-shell__description">
        {{ description }}
      </p>
    </header>
    <div class="demo-shell__toolbar">
      <slot name="toolbar" />
    </div>
    <div class="demo-shell__viewport">
      <slot />
    </div>
  </section>
</template>


================================================
FILE: docs/.vitepress/components/demos/DynamicScrollerDocDemo.vue
================================================
<script setup lang="ts">
import { computed, ref } from 'vue'
import DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'
import DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'
import { avatarStyle, createMessages, mutateMessage } from './demo-data'
import DemoShell from './DemoShell.vue'

const search = ref('')
const messages = ref(createMessages(600, 101))
const minItemSize = ref(68)

const visibleStart = ref(0)
const visibleEnd = ref(0)

const filteredMessages = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return messages.value
  return messages.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))
})

function randomizeMessage(index: number) {
  const row = filteredMessages.value[index]
  if (!row)
    return
  mutateMessage(row, Date.now() % 997)
}

function onUpdate(_viewStart: number, _viewEnd: number, start: number, end: number) {
  visibleStart.value = start
  visibleEnd.value = end
}
</script>

<template>
  <DemoShell
    title="DynamicScroller: unknown heights"
    description="Ported from the dynamic messages demo. Each row recalculates as content changes."
  >
    <template #toolbar>
      <label class="demo-chip">
        Filter
        <input
          v-model="search"
          type="text"
          placeholder="Type keyword"
        >
      </label>

      <label class="demo-chip">
        Min row size
        <input
          v-model.number="minItemSize"
          type="range"
          min="40"
          max="120"
          step="2"
        >
        {{ minItemSize }}px
      </label>

      <span class="demo-chip">Matches: {{ filteredMessages.length }}</span>
      <span class="demo-chip">Visible: {{ visibleStart }}-{{ visibleEnd }}</span>
    </template>

    <DynamicScroller
      class="demo-viewport"
      :items="filteredMessages"
      :min-item-size="minItemSize"
      :emit-update="true"
      @update="onUpdate"
    >
      <template #before>
        <div class="demo-notice">
          Click any message to mutate text and trigger a dynamic size recalculation.
        </div>
      </template>

      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[item.message]"
          class="demo-message-row"
          @click="randomizeMessage(index)"
        >
          <div
            class="demo-avatar"
            :style="avatarStyle(item.hue)"
          >
            {{ item.initials }}
          </div>

          <div>
            <div class="demo-message-body">
              {{ item.message }}
            </div>
            <small class="demo-message-meta">{{ item.user }}</small>
          </div>

          <small class="demo-message-meta">{{ item.timestamp }}</small>
        </DynamicScrollerItem>
      </template>

      <template #after>
        <div class="demo-notice">
          End of list.
        </div>
      </template>
    </DynamicScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/GridDocDemo.vue
================================================
<script setup lang="ts">
import type { Person } from './demo-data'
import { computed, ref } from 'vue'
import RecycleScroller from '../../../../packages/vue-virtual-scroller/src/components/RecycleScroller.vue'
import { createPeopleRows, gradientAt } from './demo-data'
import DemoShell from './DemoShell.vue'

interface GridCard extends Person {
  id: number
}

const scroller = ref<InstanceType<typeof RecycleScroller>>()
const gridItems = ref(5)
const scrollTo = ref(300)

const rawRows = createPeopleRows(2500, false, 111)

const cards = computed<GridCard[]>(() =>
  rawRows
    .filter(row => row.type === 'person')
    .map((row) => {
      const person = row.value as Person
      return {
        id: row.id,
        ...person,
      }
    }),
)

function jump() {
  const target = Math.min(Math.max(0, scrollTo.value), cards.value.length - 1)
  scroller.value?.scrollToItem(target)
}
</script>

<template>
  <DemoShell
    title="Grid mode"
    description="Ported from the grid demo. RecycleScroller composes fixed-size cards in rows for large image-like layouts."
  >
    <template #toolbar>
      <label class="demo-chip">
        Items / row
        <input
          v-model.number="gridItems"
          type="range"
          min="2"
          max="10"
        >
        {{ gridItems }}
      </label>

      <label class="demo-chip">
        Scroll to
        <input
          v-model.number="scrollTo"
          type="number"
          min="0"
          :max="cards.length"
        >
      </label>

      <button
        class="demo-button"
        @click="jump"
      >
        Jump
      </button>

      <span class="demo-chip">Cards: {{ cards.length }}</span>
    </template>

    <RecycleScroller
      ref="scroller"
      class="demo-viewport"
      :items="cards"
      :item-size="166"
      :grid-items="gridItems"
      :item-secondary-size="176"
    >
      <template #default="{ item, index }">
        <article class="demo-grid-card" :style="{ background: gradientAt(index) }">
          <small>#{{ index }}</small>
          <strong>{{ item.initials }}</strong>
          <span>{{ item.name }}</span>
        </article>
      </template>
    </RecycleScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/HorizontalDocDemo.vue
================================================
<script setup lang="ts">
import { computed, ref } from 'vue'
import DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'
import DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'
import { avatarStyle, createMessages } from './demo-data'
import DemoShell from './DemoShell.vue'

const search = ref('')
const rows = ref(createMessages(500, 909))

const filteredRows = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return rows.value
  return rows.value.filter(row => row.message.toLowerCase().includes(term) || row.user.toLowerCase().includes(term))
})

function cardWidth(message: string) {
  return Math.max(180, Math.min(440, Math.round(message.length * 0.95)))
}
</script>

<template>
  <DemoShell
    title="Horizontal dynamic"
    description="Ported from the horizontal demo. Unknown widths are measured dynamically while scrolling on the x-axis."
  >
    <template #toolbar>
      <label class="demo-chip">
        Filter
        <input
          v-model="search"
          type="text"
          placeholder="Search text"
        >
      </label>

      <span class="demo-chip">Cards: {{ filteredRows.length }}</span>
      <span class="demo-chip">Tip: Shift + wheel for horizontal scroll</span>
    </template>

    <DynamicScroller
      class="demo-viewport demo-horizontal-track"
      :items="filteredRows"
      :min-item-size="180"
      direction="horizontal"
    >
      <template #before>
        <div class="demo-notice">
          Width is content-driven and recalculated per card.
        </div>
      </template>

      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[item.message]"
          :style="{ width: `${cardWidth(item.message)}px` }"
          class="demo-horizontal-card"
        >
          <div class="demo-avatar" :style="avatarStyle(item.hue)">
            {{ item.initials }}
          </div>
          <div class="demo-message-body">
            {{ item.message }}
          </div>
          <small class="demo-message-meta">{{ item.user }} · #{{ index }}</small>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/RecycleScrollerDocDemo.vue
================================================
<script setup lang="ts">
import type { Person, PersonRow } from './demo-data'
import { computed, onMounted, ref, watch } from 'vue'
import RecycleScroller from '../../../../packages/vue-virtual-scroller/src/components/RecycleScroller.vue'
import { avatarStyle, createPeopleRows } from './demo-data'
import DemoShell from './DemoShell.vue'

const scroller = ref<InstanceType<typeof RecycleScroller>>()
const count = ref(8000)
const withLetters = ref(true)
const buffer = ref(240)
const scrollTo = ref(180)
const rows = ref<PersonRow[]>([])

const visibleStart = ref(0)
const visibleEnd = ref(0)

const itemSize = computed(() => (withLetters.value ? null : 74))

function regenerate() {
  rows.value = createPeopleRows(Math.max(50, count.value), withLetters.value, 17)
}

function addPeople(amount = 100) {
  count.value = Math.min(50000, count.value + amount)
}

function jump() {
  const target = Math.min(Math.max(0, scrollTo.value), rows.value.length - 1)
  scroller.value?.scrollToItem(target)
}

function toggleLetterSize(row: PersonRow) {
  if (row.type === 'letter') {
    row.height = row.height === 96 ? 136 : 96
  }
}

function personOf(row: PersonRow) {
  return row.value as Person
}

function onUpdate(_viewStart: number, _viewEnd: number, start: number, end: number) {
  visibleStart.value = start
  visibleEnd.value = end
}

watch([count, withLetters], regenerate)
onMounted(regenerate)
</script>

<template>
  <DemoShell
    title="RecycleScroller: Large list, variable height"
    description="Ported from the classic RecycleScroller demo with modern controls and cleaner visual feedback."
  >
    <template #toolbar>
      <label class="demo-chip">
        Items
        <input
          v-model.number="count"
          type="number"
          min="50"
          max="50000"
        >
      </label>

      <label class="demo-chip">
        Variable height
        <input
          v-model="withLetters"
          type="checkbox"
        >
      </label>

      <label class="demo-chip">
        Buffer
        <input
          v-model.number="buffer"
          type="range"
          min="100"
          max="1800"
          step="20"
        >
        {{ buffer }}px
      </label>

      <label class="demo-chip">
        Scroll to
        <input
          v-model.number="scrollTo"
          type="number"
          min="0"
          :max="rows.length"
        >
      </label>

      <button
        class="demo-button secondary"
        @click="addPeople(500)"
      >
        +500
      </button>

      <button
        class="demo-button"
        @click="jump"
      >
        Jump
      </button>

      <span class="demo-chip">Visible: {{ visibleStart }}-{{ visibleEnd }}</span>
    </template>

    <RecycleScroller
      ref="scroller"
      class="demo-viewport"
      :items="rows"
      :item-size="itemSize"
      :buffer="buffer"
      key-field="id"
      size-field="height"
      :emit-update="true"
      @update="onUpdate"
    >
      <template #default="{ item, index }">
        <div
          v-if="item.type === 'letter'"
          class="demo-letter-row"
          :style="{ height: `${item.height}px` }"
          @click="toggleLetterSize(item)"
        >
          <strong>{{ item.value }}</strong>
          <span>Segment {{ index }}</span>
        </div>

        <div
          v-else
          class="demo-person-row"
          :style="{ height: `${item.height}px` }"
        >
          <div
            class="demo-avatar"
            :style="avatarStyle(personOf(item).hue)"
          >
            {{ personOf(item).initials }}
          </div>

          <div>
            <div>{{ personOf(item).name }}</div>
            <small class="demo-message-meta">Click letter rows to toggle heights</small>
          </div>

          <small class="demo-message-meta">#{{ index }}</small>
        </div>
      </template>
    </RecycleScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/SimpleListDocDemo.vue
================================================
<script setup lang="ts">
import { computed, ref } from 'vue'
import DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'
import DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'
import RecycleScroller from '../../../../packages/vue-virtual-scroller/src/components/RecycleScroller.vue'
import { createSimpleStrings } from './demo-data'
import DemoShell from './DemoShell.vue'

const useDynamic = ref(true)
const search = ref('')
const rows = ref(createSimpleStrings(4000, 505))

const filteredRows = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return rows.value
  return rows.value.filter(item => item.toLowerCase().includes(term))
})
</script>

<template>
  <DemoShell
    title="Simple list"
    description="Ported from the simple-list demo. Switch between DynamicScroller and RecycleScroller with a single control."
  >
    <template #toolbar>
      <label class="demo-chip">
        Filter
        <input
          v-model="search"
          type="text"
          placeholder="Find sentence"
        >
      </label>

      <label class="demo-chip">
        Dynamic mode
        <input
          v-model="useDynamic"
          type="checkbox"
        >
      </label>

      <span class="demo-chip">Rows: {{ filteredRows.length }}</span>
    </template>

    <DynamicScroller
      v-if="useDynamic"
      class="demo-viewport"
      :items="filteredRows"
      :min-item-size="58"
    >
      <template #before>
        <div class="demo-notice">
          Dynamic mode handles variable sentence height.
        </div>
      </template>
      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :index="index"
          :size-dependencies="[item]"
          class="demo-message-row"
        >
          <div class="demo-avatar" :style="{ background: 'linear-gradient(145deg, #4a7c59, #234f35)' }">
            {{ String(index + 1).slice(-2).padStart(2, '0') }}
          </div>
          <div class="demo-message-body">
            {{ item }}
          </div>
          <small class="demo-message-meta">dynamic</small>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>

    <RecycleScroller
      v-else
      class="demo-viewport"
      :items="filteredRows"
      :item-size="58"
    >
      <template #default="{ item, index }">
        <div class="demo-message-row">
          <div class="demo-avatar" :style="{ background: 'linear-gradient(145deg, #7b2cbf, #3c096c)' }">
            {{ String(index + 1).slice(-2).padStart(2, '0') }}
          </div>
          <div class="demo-message-body">
            {{ item }}
          </div>
          <small class="demo-message-meta">fixed</small>
        </div>
      </template>
    </RecycleScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/TestChatDocDemo.vue
================================================
<script setup lang="ts">
import { ref } from 'vue'
import DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'
import DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'
import { createSimpleStrings } from './demo-data'
import DemoShell from './DemoShell.vue'

const pool = createSimpleStrings(1200, 1303)
const scroller = ref<InstanceType<typeof DynamicScroller>>()
const rows = ref<{ id: number, text: string }[]>([])

let nextId = 1

function addItems(count = 1) {
  for (let i = 0; i < count; i++) {
    rows.value.push({
      id: nextId,
      text: pool[nextId % pool.length],
    })
    nextId++
  }
  requestAnimationFrame(() => scroller.value?.scrollToBottom())
}
</script>

<template>
  <DemoShell
    title="Test chat append"
    description="Ported from test-chat. This stress test appends many rows and keeps the viewport pinned to the latest messages."
  >
    <template #toolbar>
      <button class="demo-button" @click="addItems(1)">
        +1
      </button>
      <button class="demo-button" @click="addItems(5)">
        +5
      </button>
      <button class="demo-button" @click="addItems(20)">
        +20
      </button>
      <button class="demo-button" @click="addItems(80)">
        +80
      </button>
      <span class="demo-chip">Messages: {{ rows.length }}</span>
    </template>

    <DynamicScroller
      ref="scroller"
      class="demo-viewport"
      :items="rows"
      :min-item-size="48"
      @resize="scroller?.scrollToBottom()"
    >
      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[item.text]"
          class="demo-message-row"
        >
          <div class="demo-avatar" :style="{ background: 'linear-gradient(145deg, #2f7a52, #14532d)' }">
            {{ String((index % 99) + 1).padStart(2, '0') }}
          </div>
          <div class="demo-chat-bubble">
            <div class="demo-message-body">
              {{ item.text }}
            </div>
          </div>
          <small class="demo-message-meta">#{{ item.id }}</small>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </DemoShell>
</template>


================================================
FILE: docs/.vitepress/components/demos/demo-data.ts
================================================
export interface PersonRow {
  id: number
  index: number
  type: 'person' | 'letter'
  value: string | Person
  height: number
}

export interface Person {
  name: string
  initials: string
  hue: number
}

export interface MessageRow {
  id: number
  user: string
  initials: string
  hue: number
  message: string
  timestamp: string
}

const FIRST_NAMES = [
  'Avery',
  'Riley',
  'Jordan',
  'Quinn',
  'Morgan',
  'Rowan',
  'Sage',
  'Parker',
  'Casey',
  'Reese',
  'Dakota',
  'Alex',
  'Jamie',
  'Taylor',
  'Harper',
  'Mika',
  'Noa',
  'Arden',
  'River',
  'Kai',
]

const LAST_NAMES = [
  'Anderson',
  'Bennett',
  'Carter',
  'Diaz',
  'Edwards',
  'Fletcher',
  'Garcia',
  'Hughes',
  'Ingram',
  'Johnson',
  'Khan',
  'Lopez',
  'Miller',
  'Nguyen',
  'Ortiz',
  'Patel',
  'Quincy',
  'Rivera',
  'Sato',
  'Turner',
]

const WORDS = [
  'virtual',
  'scrolling',
  'profile',
  'buffer',
  'dynamic',
  'render',
  'smooth',
  'window',
  'active',
  'message',
  'compute',
  'layout',
  'viewport',
  'recycle',
  'velocity',
  'index',
  'height',
  'width',
  'visibility',
  'performant',
  'batch',
  'stream',
  'queue',
  'resize',
  'observe',
  'compose',
  'discover',
  'cluster',
  'card',
  'slot',
]

function createRng(seed = 1) {
  let value = seed >>> 0
  return () => {
    value = (Math.imul(1664525, value) + 1013904223) >>> 0
    return value / 4294967296
  }
}

function pick<T>(rng: () => number, values: T[]) {
  return values[Math.floor(rng() * values.length)]
}

function capitalize(text: string) {
  return text.charAt(0).toUpperCase() + text.slice(1)
}

function sentence(rng: () => number, minWords = 8, maxWords = 20) {
  const length = minWords + Math.floor(rng() * (maxWords - minWords + 1))
  const parts: string[] = []
  for (let i = 0; i < length; i++) {
    parts.push(pick(rng, WORDS))
  }
  return `${capitalize(parts.join(' '))}.`
}

function initialsFromName(name: string) {
  const parts = name.split(' ')
  return `${parts[0]?.charAt(0) ?? ''}${parts[1]?.charAt(0) ?? ''}`.toUpperCase()
}

function hueFromText(text: string, salt = 0) {
  let hash = salt
  for (let i = 0; i < text.length; i++) {
    hash = (hash * 31 + text.charCodeAt(i)) % 360
  }
  return (hash + 360) % 360
}

export function avatarStyle(hue: number) {
  return {
    background: `linear-gradient(145deg, hsl(${hue} 68% 44%), hsl(${(hue + 32) % 360} 70% 36%))`,
  }
}

export function createPeopleRows(count: number, withLetters = true, seed = 42) {
  const rng = createRng(seed)
  const byLetter = new Map<string, Person[]>()

  for (let i = 0; i < count; i++) {
    const name = `${pick(rng, FIRST_NAMES)} ${pick(rng, LAST_NAMES)}`
    const letter = name.charAt(0).toLowerCase()
    const person: Person = {
      name,
      initials: initialsFromName(name),
      hue: hueFromText(name),
    }
    const bucket = byLetter.get(letter) ?? []
    bucket.push(person)
    byLetter.set(letter, bucket)
  }

  const rows: PersonRow[] = []
  const letters = 'abcdefghijklmnopqrstuvwxyz'.split('')
  let index = 0
  let id = 1

  for (const letter of letters) {
    const bucket = (byLetter.get(letter) ?? []).sort((a, b) => a.name.localeCompare(b.name))
    if (!bucket.length)
      continue

    if (withLetters) {
      rows.push({
        id: id++,
        index: index++,
        type: 'letter',
        value: letter,
        height: 96,
      })
    }

    for (const person of bucket) {
      rows.push({
        id: id++,
        index: index++,
        type: 'person',
        value: person,
        height: 74,
      })
    }
  }

  return rows
}

export function createMessages(count: number, seed = 99) {
  const rng = createRng(seed)
  const list: MessageRow[] = []

  for (let i = 0; i < count; i++) {
    const user = `${pick(rng, FIRST_NAMES)} ${pick(rng, LAST_NAMES)}`
    const timestamp = `${String(8 + Math.floor((i % 720) / 60)).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}`
    list.push({
      id: i + 1,
      user,
      initials: initialsFromName(user),
      hue: hueFromText(user, i),
      message: `${sentence(rng)} ${rng() > 0.5 ? sentence(rng, 4, 11) : ''}`.trim(),
      timestamp,
    })
  }

  return list
}

export function mutateMessage(row: MessageRow, seed = 1234) {
  const rng = createRng(seed + row.id)
  row.message = `${sentence(rng, 5, 14)} ${sentence(rng, 8, 18)}`
}

export function createSimpleStrings(count: number, seed = 7) {
  const rng = createRng(seed)
  const list: string[] = []
  for (let i = 0; i < count; i++) {
    list.push(`${sentence(rng, 5, 14)} ${rng() > 0.6 ? sentence(rng, 4, 10) : ''}`.trim())
  }
  return list
}

const GRADIENTS = [
  'linear-gradient(145deg, #57cc99, #2d6a4f)',
  'linear-gradient(145deg, #ff8fa3, #7b2cbf)',
  'linear-gradient(145deg, #56cfe1, #4361ee)',
  'linear-gradient(145deg, #ffd166, #f77f00)',
  'linear-gradient(145deg, #52b788, #1b4332)',
  'linear-gradient(145deg, #f8961e, #f94144)',
]

export function gradientAt(index: number) {
  return GRADIENTS[index % GRADIENTS.length]
}


================================================
FILE: docs/.vitepress/config.mts
================================================
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'Vue Virtual Scroller',
  description: 'Blazing fast scrolling of any amount of data',

  themeConfig: {
    nav: [
      { text: 'Guide', link: '/guide/' },
      { text: 'Demos', link: '/demos/' },
      {
        text: 'Links',
        items: [
          { text: 'Live Demo', link: 'https://vue-virtual-scroller-demo.netlify.app/' },
          { text: 'GitHub', link: 'https://github.com/Akryum/vue-virtual-scroller' },
          { text: 'Changelog', link: 'https://github.com/Akryum/vue-virtual-scroller/blob/master/CHANGELOG.md' },
        ],
      },
    ],

    sidebar: {
      '/guide/': [
        {
          text: 'Introduction',
          items: [
            { text: 'Getting Started', link: '/guide/' },
          ],
        },
        {
          text: 'Components',
          items: [
            { text: 'RecycleScroller', link: '/guide/recycle-scroller' },
            { text: 'DynamicScroller', link: '/guide/dynamic-scroller' },
            { text: 'DynamicScrollerItem', link: '/guide/dynamic-scroller-item' },
          ],
        },
        {
          text: 'Utilities',
          items: [
            { text: 'IdState', link: '/guide/id-state' },
            { text: 'Headless (useRecycleScroller)', link: '/guide/use-recycle-scroller' },
          ],
        },
        { text: 'AI & Skills', link: '/guide/ai-skills' },
      ],
      '/demos/': [
        {
          text: 'Demos',
          items: [
            { text: 'Overview', link: '/demos/' },
            { text: 'RecycleScroller', link: '/demos/recycle-scroller' },
            { text: 'DynamicScroller', link: '/demos/dynamic-scroller' },
            { text: 'Chat Stream', link: '/demos/chat' },
            { text: 'Simple List', link: '/demos/simple-list' },
            { text: 'Horizontal', link: '/demos/horizontal' },
            { text: 'Grid', link: '/demos/grid' },
            { text: 'Test Chat', link: '/demos/test-chat' },
          ],
        },
      ],
    },

    socialLinks: [
      { icon: 'github', link: 'https://github.com/Akryum/vue-virtual-scroller' },
    ],

    footer: {
      message: 'Released under the MIT License.',
      copyright: 'Copyright Akryum',
    },

    search: {
      provider: 'local',
    },
  },
})


================================================
FILE: docs/.vitepress/theme/index.ts
================================================
import DefaultTheme from 'vitepress/theme'
import './style.css'

export default {
  extends: DefaultTheme,
}


================================================
FILE: docs/.vitepress/theme/style.css
================================================
:root {
  --demo-bg: linear-gradient(145deg, #f4f8f2 0%, #edf7f6 48%, #f8f2e9 100%);
  --demo-surface: rgba(255, 255, 255, 0.86);
  --demo-border: rgba(47, 73, 59, 0.16);
  --demo-accent: #2f7a52;
  --demo-accent-soft: rgba(47, 122, 82, 0.12);
  --demo-text: #173224;
  --demo-muted: #4c6a5c;
  --demo-shadow: 0 12px 35px rgba(13, 42, 29, 0.12);
}

.demo-shell {
  border: 1px solid var(--demo-border);
  border-radius: 18px;
  background: var(--demo-bg);
  box-shadow: var(--demo-shadow);
  overflow: hidden;
  margin: 18px 0 26px;
}

.demo-shell__header {
  padding: 18px 20px;
  border-bottom: 1px solid var(--demo-border);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.55));
}

.demo-shell__title {
  margin: 0;
  color: var(--demo-text);
  font-family: 'Avenir Next', 'Helvetica Neue', 'Segoe UI', sans-serif;
  font-size: 1.05rem;
  letter-spacing: 0.01em;
}

.demo-shell__description {
  margin: 6px 0 0;
  color: var(--demo-muted);
  font-size: 0.92rem;
  line-height: 1.45;
}

.demo-shell__toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--demo-border);
  background: rgba(255, 255, 255, 0.52);
}

.demo-shell__viewport {
  padding: 16px;
}

.demo-chip {
  display: inline-flex;
  align-items: center;
  height: 34px;
  gap: 8px;
  border-radius: 999px;
  padding: 0 12px;
  color: var(--demo-text);
  background: var(--demo-surface);
  border: 1px solid var(--demo-border);
  box-shadow: 0 3px 10px rgba(13, 42, 29, 0.06);
  font-size: 0.84rem;
}

.demo-chip input,
.demo-chip button,
.demo-chip select {
  border: 0;
  outline: 0;
  background: transparent;
  color: inherit;
  font: inherit;
}

.demo-chip input[type="number"],
.demo-chip input[type="text"] {
  width: 78px;
}

.demo-chip input[type="range"] {
  width: 110px;
}

.demo-button {
  border: 1px solid color-mix(in srgb, var(--demo-accent) 70%, #fff 30%);
  background: linear-gradient(180deg, #4ba774, #2f7a52);
  color: #fff;
  border-radius: 999px;
  padding: 4px 12px;
  cursor: pointer;
  font-size: 0.82rem;
  font-weight: 600;
}

.demo-button.secondary {
  background: #fff;
  color: var(--demo-accent);
  border-color: rgba(47, 122, 82, 0.35);
}

.demo-viewport {
  height: 560px;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--demo-border);
  overflow: hidden;
}

.demo-person-row {
  min-height: 74px;
  display: grid;
  grid-template-columns: 52px 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 10px 16px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}

.demo-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: #fff;
  font-size: 0.92rem;
  font-weight: 700;
}

.demo-letter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  background: linear-gradient(90deg, rgba(47, 122, 82, 0.16), rgba(47, 122, 82, 0.04));
  border-bottom: 1px solid rgba(47, 122, 82, 0.14);
  cursor: pointer;
}

.demo-letter-row strong {
  font-size: 1.35rem;
  text-transform: uppercase;
  color: #214935;
}

.demo-message-row {
  display: grid;
  grid-template-columns: 42px 1fr auto;
  gap: 10px;
  align-items: flex-start;
  padding: 10px 14px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  cursor: pointer;
}

.demo-message-body {
  color: #1f352a;
  line-height: 1.4;
}

.demo-message-meta {
  color: #6a7f74;
  font-size: 0.78rem;
  white-space: nowrap;
}

.demo-notice {
  margin: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px dashed rgba(47, 122, 82, 0.35);
  color: #2e5c43;
  background: var(--demo-accent-soft);
}

.demo-grid-card {
  height: 100%;
  border-radius: 14px;
  border: 1px solid rgba(0, 0, 0, 0.08);
  color: #fff;
  padding: 12px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}

.demo-horizontal-track {
  height: 320px;
}

.demo-horizontal-card {
  height: 100%;
  border-right: 1px solid rgba(0, 0, 0, 0.08);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.5));
}

.demo-chat-bubble {
  border-radius: 14px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(0, 0, 0, 0.06);
}

@media (max-width: 760px) {
  .demo-shell__toolbar {
    gap: 8px;
    padding: 10px;
  }

  .demo-chip {
    width: 100%;
    justify-content: space-between;
  }

  .demo-viewport {
    height: 470px;
  }
}


================================================
FILE: docs/demos/chat.md
================================================
<script setup>
import ChatStreamDocDemo from '../.vitepress/components/demos/ChatStreamDocDemo.vue'
</script>

# Chat Stream Demo

Use this demo for chat, logs, and live feeds that continuously append data.

What to try:

- Start/stop the stream and observe scroll stability.
- Append large batches (`+20 messages`) to validate throughput.
- Apply filters while data is growing.
- Confirm the list stays pinned near the latest items.

<ChatStreamDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import { createMessages } from '../.vitepress/components/demos/demo-data'

const scroller = ref<InstanceType<typeof DynamicScroller>>()
const basePool = createMessages(1500, 303)

let nextId = 1
const stream = ref(createMessages(20, 707).map(item => ({ ...item, id: nextId++ })))
const search = ref('')
const streaming = ref(false)

let streamTimer: ReturnType<typeof setInterval> | undefined

const filteredItems = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return stream.value
  return stream.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))
})

function appendBatch(amount = 8) {
  for (let i = 0; i < amount; i++) {
    const template = basePool[(nextId + i) % basePool.length]
    stream.value.push({
      ...template,
      id: nextId++,
      timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
    })
  }
  requestAnimationFrame(() => scroller.value?.scrollToBottom())
}

function startStream() {
  if (streaming.value)
    return
  streaming.value = true
  appendBatch(12)
  streamTimer = setInterval(() => {
    appendBatch(6)
  }, 320)
}

function stopStream() {
  streaming.value = false
  if (streamTimer) {
    clearInterval(streamTimer)
    streamTimer = undefined
  }
}

onBeforeUnmount(stopStream)
</script>

<template>
  <DynamicScroller
    ref="scroller"
    :items="filteredItems"
    :min-item-size="62"
  >
    <template #default="{ item, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[item.message]"
      >
        <strong>{{ item.user }}</strong>
        <p>{{ item.message }}</p>
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>
```


================================================
FILE: docs/demos/dynamic-scroller.md
================================================
<script setup>
import DynamicScrollerDocDemo from '../.vitepress/components/demos/DynamicScrollerDocDemo.vue'
</script>

# DynamicScroller Demo

Use this demo when item height is not known ahead of time.

What to try:

- Filter the list to verify virtualization still behaves correctly.
- Click messages to mutate content and trigger automatic re-measurement.
- Adjust `Min row size` to see first-render tradeoffs.
- Watch the visible range to understand viewport updates.

<DynamicScrollerDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import { createMessages, mutateMessage } from '../.vitepress/components/demos/demo-data'

const search = ref('')
const messages = ref(createMessages(600, 101))
const minItemSize = ref(68)

const filteredMessages = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return messages.value
  return messages.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))
})

function randomizeMessage(index: number) {
  const row = filteredMessages.value[index]
  if (!row)
    return
  mutateMessage(row, Date.now() % 997)
}
</script>

<template>
  <DynamicScroller
    :items="filteredMessages"
    :min-item-size="minItemSize"
  >
    <template #default="{ item, index, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[item.message]"
        @click="randomizeMessage(index)"
      >
        <strong>{{ item.user }}</strong>
        <p>{{ item.message }}</p>
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>
```


================================================
FILE: docs/demos/grid.md
================================================
<script setup>
import GridDocDemo from '../.vitepress/components/demos/GridDocDemo.vue'
</script>

# Grid Demo

Use this demo for card galleries and catalog layouts.

What to try:

- Change `Items / row` to test responsiveness.
- Jump to deep indexes with `Scroll to`.
- Validate performance with thousands of cards.

<GridDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import type { Person } from '../.vitepress/components/demos/demo-data'
import { computed, ref } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { createPeopleRows } from '../.vitepress/components/demos/demo-data'

interface GridCard extends Person {
  id: number
}

const scroller = ref<InstanceType<typeof RecycleScroller>>()
const gridItems = ref(5)
const scrollTo = ref(300)

const rawRows = createPeopleRows(2500, false, 111)

const cards = computed<GridCard[]>(() =>
  rawRows
    .filter(row => row.type === 'person')
    .map((row) => {
      const person = row.value as Person
      return {
        id: row.id,
        ...person,
      }
    }),
)

function jump() {
  const target = Math.min(Math.max(0, scrollTo.value), cards.value.length - 1)
  scroller.value?.scrollToItem(target)
}
</script>

<template>
  <RecycleScroller
    ref="scroller"
    :items="cards"
    :item-size="166"
    :grid-items="gridItems"
    :item-secondary-size="176"
  >
    <template #default="{ item }">
      <article>
        <strong>{{ item.initials }}</strong>
        <span>{{ item.name }}</span>
      </article>
    </template>
  </RecycleScroller>
</template>
```


================================================
FILE: docs/demos/horizontal.md
================================================
<script setup>
import HorizontalDocDemo from '../.vitepress/components/demos/HorizontalDocDemo.vue'
</script>

# Horizontal Demo

Use this demo for horizontally scrolling lists with dynamic item width.

What to try:

- Scroll horizontally with trackpad or Shift + mouse wheel.
- Filter cards and verify smooth reflow.
- Inspect how variable-width content stays virtualized.

<HorizontalDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import { createMessages } from '../.vitepress/components/demos/demo-data'

const search = ref('')
const rows = ref(createMessages(500, 909))

const filteredRows = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return rows.value
  return rows.value.filter(row => row.message.toLowerCase().includes(term) || row.user.toLowerCase().includes(term))
})

function cardWidth(message: string) {
  return Math.max(180, Math.min(440, Math.round(message.length * 0.95)))
}
</script>

<template>
  <DynamicScroller
    :items="filteredRows"
    :min-item-size="180"
    direction="horizontal"
  >
    <template #default="{ item, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[item.message]"
        :style="{ width: `${cardWidth(item.message)}px` }"
      >
        {{ item.message }}
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>
```


================================================
FILE: docs/demos/index.md
================================================
# Demos

Interactive demos for common real-world use cases.

## Pick a demo

- [RecycleScroller demo](./recycle-scroller) — fixed or variable-size rows with large datasets.
- [DynamicScroller demo](./dynamic-scroller) — unknown row heights with live size recalculation.
- [Chat stream demo](./chat) — append-only feeds with auto-scroll to bottom.
- [Simple list demo](./simple-list) — compare `DynamicScroller` and `RecycleScroller` quickly.
- [Horizontal demo](./horizontal) — dynamic-size cards in horizontal direction.
- [Grid demo](./grid) — multi-column virtualized layouts.
- [Test chat demo](./test-chat) — stress-test frequent insertions and bottom pinning.


================================================
FILE: docs/demos/recycle-scroller.md
================================================
<script setup>
import RecycleScrollerDocDemo from '../.vitepress/components/demos/RecycleScrollerDocDemo.vue'
</script>

# RecycleScroller Demo

Use this demo when your list items have known sizes, or when sizes can be provided by data.

What to try:

- Change `Items` to simulate very large datasets.
- Toggle `Variable height` and click letter rows to see size updates.
- Tune `Buffer` to understand render-ahead behavior.
- Use `Jump` to test `scrollToItem`.

<RecycleScrollerDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { createPeopleRows } from '../.vitepress/components/demos/demo-data'

const count = ref(8000)
const withLetters = ref(true)
const buffer = ref(240)
const rows = ref([])

const itemSize = computed(() => (withLetters.value ? null : 74))

function regenerate() {
  rows.value = createPeopleRows(Math.max(50, count.value), withLetters.value, 17)
}

function toggleLetterSize(row: any) {
  if (row.type === 'letter') {
    row.height = row.height === 96 ? 136 : 96
  }
}

watch([count, withLetters], regenerate)
onMounted(regenerate)
</script>

<template>
  <RecycleScroller
    :items="rows"
    :item-size="itemSize"
    :buffer="buffer"
    key-field="id"
    size-field="height"
  >
    <template #default="{ item, index }">
      <div
        v-if="item.type === 'letter'"
        :style="{ height: `${item.height}px` }"
        @click="toggleLetterSize(item)"
      >
        <strong>{{ item.value }}</strong> ({{ index }})
      </div>
      <div
        v-else
        :style="{ height: `${item.height}px` }"
      >
        {{ item.value.name }}
      </div>
    </template>
  </RecycleScroller>
</template>
```


================================================
FILE: docs/demos/simple-list.md
================================================
<script setup>
import SimpleListDocDemo from '../.vitepress/components/demos/SimpleListDocDemo.vue'
</script>

# Simple List Demo

Use this demo to compare dynamic and fixed-size strategies on the same dataset.

What to try:

- Toggle `Dynamic mode` on/off to compare behavior.
- Filter the list and compare how both modes respond.
- Use this as a reference when deciding between `DynamicScroller` and `RecycleScroller`.

<SimpleListDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DynamicScroller, DynamicScrollerItem, RecycleScroller } from 'vue-virtual-scroller'
import { createSimpleStrings } from '../.vitepress/components/demos/demo-data'

const useDynamic = ref(true)
const search = ref('')
const rows = ref(createSimpleStrings(4000, 505))

const filteredRows = computed(() => {
  const term = search.value.trim().toLowerCase()
  if (!term)
    return rows.value
  return rows.value.filter(item => item.toLowerCase().includes(term))
})
</script>

<template>
  <DynamicScroller
    v-if="useDynamic"
    :items="filteredRows"
    :min-item-size="58"
  >
    <template #default="{ item, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[item]"
      >
        {{ item }}
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>

  <RecycleScroller
    v-else
    :items="filteredRows"
    :item-size="58"
  >
    <template #default="{ item }">
      {{ item }}
    </template>
  </RecycleScroller>
</template>
```


================================================
FILE: docs/demos/test-chat.md
================================================
<script setup>
import TestChatDocDemo from '../.vitepress/components/demos/TestChatDocDemo.vue'
</script>

# Test Chat Demo

Use this demo to test append-heavy timelines and quick burst updates.

What to try:

- Add rows in different batch sizes (`+1`, `+5`, `+20`, `+80`).
- Confirm auto-scroll behavior under repeated inserts.
- Use it as a sanity check for real-time message UIs.

<TestChatDocDemo />


## Relevant source code

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import { createSimpleStrings } from '../.vitepress/components/demos/demo-data'

const pool = createSimpleStrings(1200, 1303)
const scroller = ref<InstanceType<typeof DynamicScroller>>()
const rows = ref<{ id: number, text: string }[]>([])

let nextId = 1

function addItems(count = 1) {
  for (let i = 0; i < count; i++) {
    rows.value.push({
      id: nextId,
      text: pool[nextId % pool.length],
    })
    nextId++
  }
  requestAnimationFrame(() => scroller.value?.scrollToBottom())
}
</script>

<template>
  <DynamicScroller
    ref="scroller"
    :items="rows"
    :min-item-size="48"
    @resize="scroller?.scrollToBottom()"
  >
    <template #default="{ item, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[item.text]"
      >
        {{ item.text }}
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>
```


================================================
FILE: docs/guide/ai-skills.md
================================================
# AI & Skills

If you use AI coding agents, `vue-virtual-scroller` ships a package skill that can be discovered from the installed npm package.

## One-off usage with `npx skills-npm`

After installing `vue-virtual-scroller` in your project:

```bash
pnpm add vue-virtual-scroller@next
npx skills-npm
```

This lets supported coding agents discover the skill that ships inside the package.

## Repeatable setup

If you want skill links to refresh automatically after installs:

```bash
npm i -D skills-npm
```

Add a `prepare` script in your project:

```json
{
  "scripts": {
    "prepare": "skills-npm"
  }
}
```

## Useful options

- `--source <source>` chooses `package.json` or `node_modules`
- `--cwd <cwd>` targets a specific workspace root
- `--recursive` scans monorepos
- `--dry-run` previews the generated links
- `--yes` skips prompts

For more control, create a `skills-npm.config.ts` file in your consumer project.

Learn more about `skills-npm` [here](https://github.com/antfu/skills-npm#skills-npm).

## Notes

- Run `skills-npm` from the consumer project root, not from this package repository.
- Generated links are typically local setup artifacts. Add `skills/npm-*` to `.gitignore` if you do not want them committed.
- The published `vue-virtual-scroller` package includes its `skills/` directory so discovery tools can find the shipped skill.


================================================
FILE: docs/guide/dynamic-scroller-item.md
================================================
# DynamicScrollerItem

The component that should wrap all the items in a [DynamicScroller](./dynamic-scroller) to handle size computations.

## Props

| Prop | Default | Description |
|------|---------|-------------|
| `item` (required) | — | The item rendered in the scroller. |
| `active` (required) | — | Is the holding view active in RecycleScroller. Will prevent unnecessary size recomputation. |
| `sizeDependencies` | — | Values that can affect the size of the item. This prop will be watched and if one value changes, the size will be recomputed. Recommended instead of `watchData`. |
| `watchData` | `false` | Deeply watch `item` for changes to re-calculate the size (not recommended, can impact performance). |
| `tag` | `'div'` | Element used to render the component. |
| `emitResize` | `false` | Emit the `resize` event each time the size is recomputed (can impact performance). |

## Events

| Event | Description |
|-------|-------------|
| `resize` | Emitted each time the size is recomputed, only if `emitResize` prop is `true`. |


================================================
FILE: docs/guide/dynamic-scroller.md
================================================
# DynamicScroller

This works just like the [RecycleScroller](./recycle-scroller), but it can render items with unknown sizes!

## Basic usage

```vue
<script>
export default {
  props: {
    items: Array,
  },
}
</script>

<template>
  <DynamicScroller
    :items="items"
    :min-item-size="54"
    class="scroller"
  >
    <template #default="{ item, index, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[
          item.message,
        ]"
        :data-index="index"
      >
        <div class="avatar">
          <img
            :key="item.avatar"
            :src="item.avatar"
            alt="avatar"
            class="image"
          >
        </div>
        <div class="text">
          {{ item.message }}
        </div>
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>

<style scoped>
.scroller {
  height: 100%;
}
</style>
```

## Important notes

- `minItemSize` is required for the initial render of items.
- `DynamicScroller` won't detect size changes on its own, but you can put values that can affect the item size with `size-dependencies` on [DynamicScrollerItem](./dynamic-scroller-item).
- You don't need to have a `size` field on the items.

## Props

Extends all the [RecycleScroller props](./recycle-scroller#props).

::: tip
It's not recommended to change the `sizeField` prop since all the size management is done internally.
:::

## Events

Extends all the [RecycleScroller events](./recycle-scroller#events).

## Default scoped slot props

Extends all the [RecycleScroller scoped slot props](./recycle-scroller#default-scoped-slot-props).

## Other slots

Extends all the [RecycleScroller other slots](./recycle-scroller#other-slots).


================================================
FILE: docs/guide/id-state.md
================================================
# IdState

This is a convenience mixin that can replace `data` in components being rendered in a [RecycleScroller](./recycle-scroller).

## Why is this useful?

Since the components in RecycleScroller are reused, you can't directly use the Vue standard `data` properties: otherwise they will be shared with different items in the list!

IdState will instead provide an `idState` object which is equivalent to `$data`, but it's linked to a single item with its identifier (you can change which field with `idProp` param).

## Example

In this example, we use the `id` of the `item` to have a "scoped" state to the item:

```vue
<script>
import { IdState } from 'vue-virtual-scroller'

export default {
  mixins: [
    IdState({
      // You can customize this
      idProp: vm => vm.item.id,
    }),
  ],

  props: {
    // Item in the list
    item: Object,
  },

  // This replaces data () { ... }
  idState() {
    return {
      replyOpen: false,
      replyText: '',
    }
  },
}
</script>

<template>
  <div class="question">
    <p>{{ item.question }}</p>
    <button @click="idState.replyOpen = !idState.replyOpen">
      Reply
    </button>
    <textarea
      v-if="idState.replyOpen"
      v-model="idState.replyText"
      placeholder="Type your reply"
    />
  </div>
</template>
```

## Parameters

| Parameter | Default | Description |
|-----------|---------|-------------|
| `idProp` | `vm => vm.item.id` | Field name on the component (for example: `'id'`) or function returning the id. |


================================================
FILE: docs/guide/index.md
================================================
# Getting Started

<div class="badges">

[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg)](https://npmx.dev/package/vue-virtual-scroller)
[![npm](https://img.shields.io/npm/dm/vue-virtual-scroller.svg)](https://npmx.dev/package/vue-virtual-scroller)
[![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/)

</div>

Blazing fast scrolling of any amount of data | [Demos](../demos/index.md) | [Video demo](https://www.youtube.com/watch?v=Uzq1KQV8f4k)

For Vue 2 support, see [here](https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller).

## Installation

```sh
npm install vue-virtual-scroller@next
```

```sh
yarn add vue-virtual-scroller@next
```

```sh
pnpm add vue-virtual-scroller@next
```

## Setup

`vue-virtual-scroller` ships ESM only. Use it from an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.

### Plugin import

Install all the components:

```js
import VueVirtualScroller from 'vue-virtual-scroller'

app.use(VueVirtualScroller)
```

Use specific components:

```js
import { RecycleScroller } from 'vue-virtual-scroller'

app.component('RecycleScroller', RecycleScroller)
```

::: warning
The CSS file must be imported when using the package:

```js
import 'vue-virtual-scroller/index.css'
```
:::

## Components

There are several components provided by `vue-virtual-scroller`:

- [**RecycleScroller**](./recycle-scroller) — only renders the visible items in your list. It also re-uses components and DOM elements to be as efficient and performant as possible.

- [**DynamicScroller**](./dynamic-scroller) — wraps RecycleScroller and extends its features to include dynamic size management. The main use case is when you **do not know the size of the items** in advance. It automatically "discovers" item dimensions as it renders new items during scrolling.

- [**DynamicScrollerItem**](./dynamic-scroller-item) — must wrap each item in a DynamicScroller to handle size computations.

- [**IdState**](./id-state) — a mixin that eases local state management in reused components inside a RecycleScroller.

- [**useRecycleScroller (headless)**](./use-recycle-scroller) — low-level composable API to build your own virtual scroller UI without using bundled components.

<style scoped>
.badges p {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
</style>


================================================
FILE: docs/guide/recycle-scroller.md
================================================
# RecycleScroller

RecycleScroller is a virtual scroller that only renders the visible items. As the user scrolls, RecycleScroller reuses all components and DOM nodes to maintain optimal performance.

## Basic usage

Use the scoped slot to render each item in the list:

```vue
<script>
export default {
  props: {
    list: Array,
  },
}
</script>

<template>
  <RecycleScroller
    v-slot="{ item }"
    class="scroller"
    :items="list"
    :item-size="32"
    key-field="id"
  >
    <div class="user">
      {{ item.name }}
    </div>
  </RecycleScroller>
</template>

<style scoped>
.scroller {
  height: 100%;
}

.user {
  height: 32%;
  padding: 0 12px;
  display: flex;
  align-items: center;
}
</style>
```

## Important notes

::: warning
You need to set the size of the virtual-scroller element and the items elements (for example, with CSS). Unless you are using [variable size mode](#variable-size-mode), all items should have the same height (or width in horizontal mode) to prevent display glitches.
:::

::: warning
If the items are objects, the scroller needs to be able to identify them. By default it will look for an `id` field on the items. This can be configured with the `keyField` prop if you are using another field name.
:::

- It is not recommended to use functional components inside RecycleScroller since the components are reused (so it will actually be slower).
- The list item components must be reactive to the `item` prop being updated without being re-created (use computed props or watchers to properly react to props changes!).
- You don't need to set `key` on list content (but you should on all nested `<img>` elements to prevent load glitches).
- The browsers have a size limitation on DOM elements, it means that currently the virtual scroller can't display more than ~500k items depending on the browser.
- Since DOM elements are reused for items, it's recommended to define hover styles using the provided `hover` class instead of the `:hover` state selector (e.g. `.vue-recycle-scroller__item-view.hover` or `.hover .some-element-inside-the-item-view`).

## How does it work?

- The RecycleScroller creates pools of views to render visible items to the user.
- A view holds a rendered item, and is reused inside its pool.
- For each type of item, a new pool is created so that the same components (and DOM trees) are reused for the same type.
- Views can be deactivated if they go off-screen, and can be reused anytime for a newly visible item.

Here is what the internals of RecycleScroller look like in vertical mode:

```html
<RecycleScroller>
  <!-- Wrapper element with a pre-calculated total height -->
  <wrapper
    :style="{ height: computedTotalHeight + 'px' }"
  >
    <!-- Each view is translated to the computed position -->
    <view
      v-for="view of pool"
      :style="{ transform: 'translateY(' + view.computedTop + 'px)' }"
    >
      <!-- Your elements will be rendered here -->
      <slot
        :item="view.item"
        :index="view.nr.index"
        :active="view.nr.used"
      />
    </view>
  </wrapper>
</RecycleScroller>
```

When the user scrolls inside RecycleScroller, the views are mostly just moved around to fill the new visible space, and the default slot properties updated. That way we get the minimum amount of components/elements creation and destruction and we use the full power of Vue virtual-dom diff algorithm to optimize DOM operations!

## Props

| Prop | Default | Description |
|------|---------|-------------|
| `items` | — | List of items you want to display in the scroller. |
| `direction` | `'vertical'` | Scrolling direction, either `'vertical'` or `'horizontal'`. |
| `itemSize` | `null` | Display height (or width in horizontal mode) of the items in pixels used to calculate the scroll size and position. If set to `null`, it will use [variable size mode](#variable-size-mode). |
| `gridItems` | — | Display that many items on the same line to create a grid. You must set `itemSize` to use this prop (dynamic sizes are not supported). |
| `itemSecondarySize` | — | Size in pixels (width in vertical mode, height in horizontal mode) of the items in the grid when `gridItems` is set. Defaults to `itemSize` if not set. |
| `minItemSize` | — | Minimum size used if the height (or width in horizontal mode) of an item is unknown. |
| `sizeField` | `'size'` | Field used to get the item's size in variable size mode. |
| `typeField` | `'type'` | Field used to differentiate different kinds of components in the list. For each distinct type, a pool of recycled items will be created. |
| `keyField` | `'id'` | Field used to identify items and optimize managing rendered views. |
| `pageMode` | `false` | Enable [Page mode](#page-mode). |
| `prerender` | `0` | Render a fixed number of items for Server-Side Rendering (SSR). |
| `buffer` | `200` | Amount of pixels to add to edges of the scrolling visible area to start rendering items further away. |
| `emitUpdate` | `false` | Emit an `'update'` event each time the virtual scroller content is updated (can impact performance). |
| `updateInterval` | `0` | The interval in ms at which the view will be checked for updates after scrolling. When set to `0`, check happens during the next animation frame. |
| `listClass` | `''` | Custom classes added to the item list wrapper. |
| `itemClass` | `''` | Custom classes added to each item. |
| `listTag` | `'div'` | The element to render as the list's wrapper. |
| `itemTag` | `'div'` | The element to render as the list item (the direct parent of the default slot content). |

## Events

| Event | Description |
|-------|-------------|
| `resize` | Emitted when the size of the scroller changes. |
| `visible` | Emitted when the scroller considers itself to be visible in the page. |
| `hidden` | Emitted when the scroller is hidden in the page. |
| `update(startIndex, endIndex, visibleStartIndex, visibleEndIndex)` | Emitted each time the views are updated, only if `emitUpdate` prop is `true`. |
| `scroll-start` | Emitted when the first item is rendered. |
| `scroll-end` | Emitted when the last item is rendered. |

## Default scoped slot props

| Prop | Description |
|------|-------------|
| `item` | Item being rendered in a view. |
| `index` | Reflects each item's position in the `items` array. |
| `active` | Whether or not the view is active. An active view is considered visible and being positioned by RecycleScroller. An inactive view is not considered visible and is hidden from the user. Any rendering-related computations should be skipped if the view is inactive. |

## Other slots

The `empty` slot is displayed only when `items` is empty.

```html
<main>
  <slot name="before"></slot>
  <wrapper>
    <!-- Reused view pools here -->
    <slot name="empty"></slot>
  </wrapper>
  <slot name="after"></slot>
</main>
```

Example:

```vue
<RecycleScroller
  class="scroller"
  :items="list"
  :item-size="32"
>
  <template #before>
    Hey! I'm a message displayed before the items!
  </template>

  <template v-slot="{ item }">
    <div class="user">
      {{ item.name }}
    </div>
  </template>
</RecycleScroller>
```

## Page mode

The page mode expands the virtual-scroller and uses the page viewport to compute which items are visible. That way, you can use it in a big page with HTML elements before or after (like a header and a footer). Set the `page-mode` prop to `true`:

```html
<header>
  <menu></menu>
</header>

<RecycleScroller page-mode>
  <!-- ... -->
</RecycleScroller>

<footer>
  Copyright 2017 - Cat
</footer>
```

## Variable size mode

::: warning
This mode can be performance heavy with a lot of items. Use with caution.
:::

If the `itemSize` prop is not set or is set to `null`, the virtual scroller will switch to variable size mode. You then need to expose a number field on the item objects with the size of the item element.

::: warning
You still need to set the size of the items with CSS correctly (with classes for example).
:::

Use the `sizeField` prop (default is `'size'`) to set the field used by the scroller to get the size for each item.

Example:

```js
const items = [
  {
    id: 1,
    label: 'Title',
    size: 64,
  },
  {
    id: 2,
    label: 'Foo',
    size: 32,
  },
  {
    id: 3,
    label: 'Bar',
    size: 32,
  },
]
```

## Buffer

You can set the `buffer` prop (in pixels) on the virtual-scroller to extend the viewport considered when determining the visible items. For example, if you set a buffer of 1000 pixels, the virtual-scroller will start rendering items that are 1000 pixels below the bottom of the scroller visible area, and will keep the items that are 1000 pixels above the top of the visible area.

The default value is `200`.

```html
<RecycleScroller :buffer="200" />
```

## Server-Side Rendering

The `prerender` prop can be set as the number of items to render on the server inside the virtual scroller:

```html
<RecycleScroller
  :items="items"
  :item-size="42"
  :prerender="10"
>
```


================================================
FILE: docs/guide/use-recycle-scroller.md
================================================
# useRecycleScroller (Headless)

`useRecycleScroller` is the low-level composable behind `RecycleScroller`.

Use it when you want full control over markup, styling, and rendering logic while keeping the same virtualization engine.

## When to use this

- You need a custom DOM structure that does not fit the component slot API.
- You want to integrate virtualization into an existing design system component.
- You want to control rendering/pooling behavior directly (for example with custom item wrappers).

If you just need virtual scrolling with standard markup, prefer [`RecycleScroller`](./recycle-scroller).

## Minimal fixed-size example

```vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRecycleScroller } from 'vue-virtual-scroller'

interface User {
  id: number
  name: string
}

const items = ref<User[]>(
  Array.from({ length: 10000 }, (_, i) => ({
    id: i + 1,
    name: `User ${i + 1}`,
  })),
)

const scrollerEl = ref<HTMLElement>()

const options = computed(() => ({
  items: items.value,
  keyField: 'id',
  direction: 'vertical' as const,
  itemSize: 40,
  gridItems: undefined,
  itemSecondarySize: undefined,
  minItemSize: null,
  sizeField: 'size',
  typeField: 'type',
  buffer: 200,
  pageMode: false,
  prerender: 0,
  emitUpdate: false,
  updateInterval: 0,
}))

const {
  pool,
  totalSize,
  handleScroll,
} = useRecycleScroller(options, scrollerEl)
</script>

<template>
  <div
    ref="scrollerEl"
    class="my-scroller"
    @scroll.passive="handleScroll"
  >
    <div
      class="my-scroller__inner"
      :style="{ minHeight: `${totalSize}px` }"
    >
      <div
        v-for="view in pool"
        :key="view.nr.id"
        class="my-scroller__item"
        :style="{ transform: `translateY(${view.position}px)` }"
      >
        <strong>#{{ view.nr.index }}</strong> {{ (view.item as User).name }}
      </div>
    </div>
  </div>
</template>

<style scoped>
.my-scroller {
  height: 400px;
  overflow-y: auto;
  position: relative;
  border: 1px solid #ddd;
}

.my-scroller__inner {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.my-scroller__item {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 40px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  box-sizing: border-box;
  border-bottom: 1px solid #f0f0f0;
}
</style>
```

## Required options

`useRecycleScroller` expects the same core options used internally by `RecycleScroller`:

- `items`
- `keyField`
- `direction`
- `itemSize`
- `minItemSize`
- `sizeField`
- `typeField`
- `buffer`
- `pageMode`
- `prerender`
- `emitUpdate`
- `updateInterval`

Optional grid options:

- `gridItems`
- `itemSecondarySize`

## Return values you will use most

- `pool`: visible/recycled views to render.
- `totalSize`: full virtual size (wrapper min-height/min-width).
- `handleScroll`: call this on scroll events.
- `scrollToItem(index)`: programmatic navigation.
- `scrollToPosition(px)`: absolute scroll positioning.
- `getScroll()`: current viewport range in pixels.
- `updateVisibleItems(itemsChanged, checkPositionDiff?)`: force recalculation.

## Variable-size mode

Set `itemSize: null` and provide a numeric field on each item (default `sizeField: 'size'`):

```ts
const options = computed(() => ({
  // ...
  itemSize: null,
  minItemSize: 40,
  sizeField: 'size',
}))
```

In this mode, item objects must expose the size field (`item.size` by default).

## Important notes

- You must provide scrollable sizing styles yourself (`height` or `width` + overflow).
- Use a stable key field for object items (default: `id`).
- The composable manages pooling and index mapping, but does not provide built-in markup or CSS.
- If you need automatic unknown-size measurement, use `DynamicScroller`/`DynamicScrollerItem` (or the `useDynamicScroller` + `useDynamicScrollerItem` composables).


================================================
FILE: docs/index.md
================================================
---
layout: home

hero:
  name: Vue Virtual Scroller
  tagline: Blazing fast scrolling of any amount of data
  actions:
    - theme: brand
      text: Get Started
      link: /guide/
    - theme: alt
      text: View on GitHub
      link: https://github.com/Akryum/vue-virtual-scroller

features:
  - title: Blazing Fast
    details: Minimal overhead with smart pooling and recycling of views for buttery smooth scrolling.
    icon: ⚡
  - title: RecycleScroller
    details: Only renders visible items and reuses components and DOM elements for optimal performance.
    icon: ♻️
  - title: DynamicScroller
    details: Extends RecycleScroller with dynamic size management for items with unknown sizes.
    icon: ↕️
  - title: Headless
    details: Provides low-level APIs for custom implementations and advanced use cases.
    icon: 🧠
---


================================================
FILE: eslint.config.mjs
================================================
// @ts-check
import antfu from '@antfu/eslint-config'

export default antfu()


================================================
FILE: netlify.toml
================================================
[build.environment]
NODE_VERSION = "16"
NPM_FLAGS = "--version" # prevent Netlify npm install

[[redirects]]
from = "/*"
to = "/index.html"
status = 200


================================================
FILE: package.json
================================================
{
  "name": "vue-virtual-scroller-monorepo",
  "version": "2.0.0-beta.10",
  "private": true,
  "packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af",
  "engines": {
    "node": ">=24"
  },
  "scripts": {
    "build": "pnpm run -r --filter=!demo build",
    "release": "pnpm run lint && pnpm run build && sheep release -b main --force",
    "lint": "eslint --cache",
    "test": "pnpm run -r test",
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs",
    "docs:preview": "vitepress preview docs"
  },
  "devDependencies": {
    "@akryum/sheep": "^0.5.2",
    "@antfu/eslint-config": "^7.7.0",
    "eslint": "^10.0.2",
    "typescript": "^5.3.3",
    "vitepress": "^1.6.4"
  }
}


================================================
FILE: packages/demo/.gitignore
================================================
.DS_Store
node_modules
/dist

/tests/e2e/videos/
/tests/e2e/screenshots/


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: packages/demo/README.md
================================================
# vue-virtual-scroller-demos

> Demos for vue-virtual-scroller

## Build Setup

``` bash
# install dependencies
yarn install

# serve with hot reload at localhost:8080
yarn run dev

# build for production with minification
yarn run build
```


================================================
FILE: packages/demo/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vue-virtual-scroller</title>
    <link rel="icon" href="/favicon.png">
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./src/main.js"></script>
  </body>
</html>


================================================
FILE: packages/demo/package.json
================================================
{
  "name": "demo",
  "version": "1.0.0",
  "private": true,
  "description": "Demos for vue-virtual-scroller",
  "author": "Guillaume Chau <guillaume.b.chau@gmail.com>",
  "scripts": {
    "dev": "vite dev --port 8080",
    "build": "vite build",
    "preview": "vite preview --port 8080"
  },
  "dependencies": {
    "@faker-js/faker": "^7.6.0",
    "vue": "^3.2.41",
    "vue-router": "^4.1.5",
    "vue-virtual-scroller": "workspace:*"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.1.2",
    "vite": "^3.1.8"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}


================================================
FILE: packages/demo/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>Vue Virtual Scroller Demos</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: packages/demo/src/App.vue
================================================
<template>
  <nav class="menu">
    <span class="package">
      <span class="package-name">vue-virtual-scroller</span>
    </span>

    <router-link
      :to="{ name: 'home' }"
      exact
    >
      Home
    </router-link>
    <router-link :to="{ name: 'recycle' }">
      Recycle scroller
    </router-link>
    <router-link :to="{ name: 'dynamic' }">
      Dynamic scroller
    </router-link>
    <router-link :to="{ name: 'horizontal' }">
      Horizontal
    </router-link>
    <router-link :to="{ name: 'test-chat' }">
      Scroll to bottom
    </router-link>
    <router-link :to="{ name: 'simple-list' }">
      Simple array
    </router-link>
    <router-link :to="{ name: 'chat' }">
      Chat demo
    </router-link>
    <router-link :to="{ name: 'grid' }">
      Grid demo
    </router-link>
  </nav>
  <router-view />
</template>

<style>
html,
body,
#app {
  box-sizing: border-box;
  height: 100%;
}

body {
  font-size: 16px;
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin: 0;
}

#app,
.page {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

.menu {
  flex: auto 0 0;
  display: flex;
  align-items: center;
  gap: 2px;
}

.menu,
.page {
  padding: 12px;
  box-sizing: border-box;
}

.package {
  margin-right: 12px;
}

.package-name {
  font-family: monospace;
  color: #2c3e50;
  background: #eee;
  padding: 5px 12px 3px;
}

.package-name,
.menu a {
  display: inline-block;
  border-radius: 3px;
}

.menu a {
  padding: 4px 12px;
  text-decoration: none;
  color: white;
  background: #2c3e50;
}

.menu a.router-link-active {
  background: #42b983;
}

.menu a:not(:last-child) {
  margin-right: 4px;
}

.vue-recycle-scroller {
  -webkit-overflow-scrolling: touch;
}

.vue-recycle-scroller__item-container,
.vue-recycle-scroller__item-wrapper {
  box-sizing: border-box;
}

.vue-recycle-scroller__item-view {
  cursor: pointer;
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
}

.tr, .td {
  box-sizing: border-box;
}

.vue-recycle-scroller__item-view .tr {
  display: flex;
  align-items: center;
}

.vue-recycle-scroller__item-view .td {
  display: block;
}

.vue-recycle-scroller__item-view.hover {
  background: #4fc08d;
  color: white;
}

.toolbar {
  flex: auto 0 0;
  text-align: center;
  margin-bottom: 12px;
  line-height: 32px;
  position: sticky;
  top: 0;
  z-index: 9999;
  background: white;
}

.recycle-scroller-demo.page-mode .toolbar {
  border-bottom: solid 1px #e0edfa;
}

.toolbar > *:not(:last-child) {
  margin-right: 24px;
}

.avatar {
  background: grey;
}
</style>


================================================
FILE: packages/demo/src/components/ChatDemo.vue
================================================
<script>
import { generateMessage } from '../data'

let id = 0

const messages = []
for (let i = 0; i < 10000; i++) {
  messages.push(generateMessage())
}

export default {
  data() {
    return {
      items: [],
      search: '',
      streaming: false,
    }
  },

  computed: {
    filteredItems() {
      const { search, items } = this
      if (!search)
        return items
      const lowerCaseSearch = search.toLowerCase()
      return items.filter(i => i.message.toLowerCase().includes(lowerCaseSearch))
    },
  },

  unmounted() {
    this.stopStream()
  },

  methods: {
    changeMessage(message) {
      Object.assign(message, generateMessage())
    },

    addMessage() {
      for (let i = 0; i < 10; i++) {
        this.items.push({
          id: id++,
          ...messages[id % 10000],
        })
      }
      this.scrollToBottom()

      if (this.streaming) {
        requestAnimationFrame(this.addMessage)
      }
    },

    scrollToBottom() {
      this.$refs.scroller.scrollToBottom()
    },

    startStream() {
      if (this.streaming)
        return
      this.streaming = true
      this.addMessage()
    },

    stopStream() {
      this.streaming = false
    },
  },
}
</script>

<template>
  <div class="chat-demo">
    <div class="toolbar">
      <button
        v-if="!streaming"
        @click="startStream()"
      >
        Start stream
      </button>
      <button
        v-else
        @click="stopStream()"
      >
        Stop stream
      </button>

      <input
        v-model="search"
        placeholder="Filter..."
      >
    </div>

    <DynamicScroller
      ref="scroller"
      :items="filteredItems"
      :min-item-size="54"
      class="scroller"
    >
      <template #before>
        <div class="notice">
          The message heights are unknown.
        </div>
      </template>

      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[
            item.message,
          ]"
          :data-index="index"
          :data-active="active"
          :title="`Click to change message ${index}`"
          class="message"
          @click="changeMessage(item)"
        >
          <div class="avatar">
            <img
              :key="item.avatar"
              :src="item.avatar"
              alt="avatar"
              class="image"
            >
          </div>
          <div class="text">
            {{ item.message }}
          </div>
          <div class="index">
            <span>{{ item.id }} (id)</span>
            <span>{{ index }} (index)</span>
          </div>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </div>
</template>

<style scoped>
.chat-demo {
  overflow: hidden;
  flex: auto 1 1;
  display: flex;
  flex-direction: column;
}

.scroller {
  flex: auto 1 1;
}

.notice {
  padding: 24px;
  font-size: 20px;
  color: #999;
}

.message {
  display: flex;
  min-height: 32px;
  padding: 12px;
  box-sizing: border-box;
}

.avatar {
  flex: auto 0 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  margin-right: 12px;
}

.avatar .image {
  max-width: 100%;
  max-height: 100%;
  border-radius: 50%;
}

.index,
.text {
  flex: 1;
}

.text {
  max-width: 400px;
}

.index {
  opacity: .5;
}

.index span {
  display: inline-block;
  width: 160px;
  text-align: right;
}
</style>


================================================
FILE: packages/demo/src/components/DynamicScrollerDemo.vue
================================================
<script>
import { generateMessage } from '../data'

const items = []
for (let i = 0; i < 10000; i++) {
  items.push({
    id: i,
    ...generateMessage(),
  })
}

export default {
  data() {
    return {
      items,
      search: '',
      updateParts: { viewStartIdx: 0, viewEndIdx: 0, visibleStartIdx: 0, visibleEndIdx: 0 },
    }
  },

  computed: {
    filteredItems() {
      const { search, items } = this
      if (!search)
        return items
      const lowerCaseSearch = search.toLowerCase()
      return items.filter(i => i.message.toLowerCase().includes(lowerCaseSearch))
    },
  },

  methods: {
    changeMessage(message) {
      Object.assign(message, generateMessage())
    },

    onUpdate(viewStartIndex, viewEndIndex, visibleStartIndex, visibleEndIndex) {
      this.updateParts.viewStartIdx = viewStartIndex
      this.updateParts.viewEndIdx = viewEndIndex
      this.updateParts.visibleStartIdx = visibleStartIndex
      this.updateParts.visibleEndIdx = visibleEndIndex
    },
  },
}
</script>

<template>
  <div class="dynamic-scroller-demo">
    <div class="toolbar">
      <input
        v-model="search"
        placeholder="Filter..."
      >
      <span>({{ updateParts.viewStartIdx }} - [{{ updateParts.visibleStartIdx }} - {{ updateParts.visibleEndIdx }}] - {{ updateParts.viewEndIdx }})</span>
    </div>

    <DynamicScroller
      :items="filteredItems"
      :min-item-size="54"
      :emit-update="true"
      class="scroller"
      @update="onUpdate"
    >
      <template #before>
        <div class="notice">
          The message heights are unknown.
        </div>
      </template>
      <template #after>
        <div class="notice">
          You have reached the end.
        </div>
      </template>
      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[
            item.message,
          ]"
          :data-index="index"
          :data-active="active"
          :title="`Click to change message ${index}`"
          class="message"
          @click="changeMessage(item)"
        >
          <div class="avatar">
            <img
              :key="item.avatar"
              :src="item.avatar"
              alt="avatar"
              class="image"
            >
          </div>
          <div class="text">
            {{ item.message }}
          </div>
          <div class="index">
            <span>{{ item.id }} (id)</span>
            <span>{{ index }} (index)</span>
          </div>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </div>
</template>

<style scoped>
.dynamic-scroller-demo {
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.scroller {
  flex: auto 1 1;
}

.scroller {
  border: solid 1px #42b983;
}

.toolbar {
  flex: auto 0 0;
  text-align: center;
}

.toolbar > *:not(:last-child) {
  margin-right: 24px;
}

.notice {
  padding: 24px;
  font-size: 20px;
  color: #999;
}

.message {
  display: flex;
  min-height: 32px;
  padding: 12px;
  box-sizing: border-box;
}

.avatar {
  flex: auto 0 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  margin-right: 12px;
}

.avatar .image {
  max-width: 100%;
  max-height: 100%;
  border-radius: 50%;
}

.index,
.text {
  flex: 1;
}

.text {
  max-width: 400px;
}

.index {
  opacity: .5;
}

.index span {
  display: inline-block;
  width: 160px;
  text-align: right;
}
</style>


================================================
FILE: packages/demo/src/components/GridDemo.vue
================================================
<script>
import { getData } from '../data'

export default {
  data() {
    return {
      list: [],
      gridItems: 6,
      scrollTo: 500,
    }
  },

  mounted() {
    this.list = getData(5000)
  },
}
</script>

<template>
  <div class="wrapper">
    <div class="toolbar">
      <label>
        Grid items per row
        <input
          v-model.number="gridItems"
          type="number"
          min="2"
          max="20"
        >
      </label>
      <input
        v-model.number="gridItems"
        type="range"
        min="2"
        max="20"
      >
      <span>
        <button @mousedown="$refs.scroller.scrollToItem(scrollTo)">Scroll To: </button>
        <input
          v-model.number="scrollTo"
          type="number"
          min="0"
          :max="list.length - 1"
        >
      </span>
    </div>

    <RecycleScroller
      ref="scroller"
      class="scroller"
      :items="list"
      :item-size="128"
      :grid-items="gridItems"
      :item-secondary-size="100"
    >
      <template #default="{ item, index }">
        <div class="item">
          <img
            :key="item.id"
            :src="item.value.avatar"
          >
          <div class="index">
            {{ index }}
          </div>
        </div>
      </template>
    </RecycleScroller>
  </div>
</template>

<style scoped>
.wrapper,
.scroller {
  height: 100%;
}

.wrapper {
  display: flex;
  flex-direction: column;
}

.toolbar {
  flex: none;
}

.scroller {
  flex: 1;
}

.scroller :deep(.hover) img {
  opacity: 0.5;
}

.item {
  position: relative;
  height: 100%;
}

.index {
  position: absolute;
  top: 2px;
  left: 2px;
  padding: 4px;
  border-radius: 4px;
  background-color: rgba(255, 255, 255, 0.85);
  color: black;
}

img {
  width: 100%;
  height: 100%;
  background: #eee;
  object-fit: cover;
}
</style>


================================================
FILE: packages/demo/src/components/Home.vue
================================================
<template>
  <div class="home">
    <h1>Virtual scrolling solutions</h1>

    <section>
      <router-link
        :to="{ name: 'recycle' }"
        class="route"
      >
        Recycle Scroller
      </router-link>
      <div class="description">
        The Recycle Scroller is a component that only renders the visible item in your list. It also re-use components and dom elements to be the most efficient and performant possible.
      </div>
    </section>

    <section>
      <router-link
        :to="{ name: 'dynamic' }"
        class="route"
      >
        Dynamic Scroller
      </router-link>
      <div class="description">
        This component is using Recycle Scroller under-the-hood and adds a dynamic height management feature on top of it. The main use case for this is <b>not knowing the height of the items</b> in advance: the Dynamic Scroller will automatically "discover" it when it renders new item as the user scrolls.<br>
        The items need to be wrapped in a special Dynamic Scroller Item component.
      </div>
    </section>

    <section>
      <a
        href="https://github.com/Akryum/vue-virtual-scroller"
        class="route"
      >Documentation</a>
      <div class="description">
        Read the full documentation on the repository.
      </div>
    </section>
  </div>
</template>

<style scoped>
.home {
  margin: 24px;
}

section {
  margin-top: 24px;
}

section .route {
  font-size: 24px;
  color: #42b983;
  display: block;
  margin-bottom: 6px;
}

section .description {
  max-width: 500px;
}
</style>


================================================
FILE: packages/demo/src/components/HorizontalDemo.vue
================================================
<script>
import { generateMessage } from '../data'

const items = []
for (let i = 0; i < 10000; i++) {
  items.push({
    id: i,
    ...generateMessage(),
  })
}

export default {
  data() {
    return {
      items,
      search: '',
      dismissInfo: false,
    }
  },

  computed: {
    filteredItems() {
      const { search, items } = this
      if (!search)
        return items
      const lowerCaseSearch = search.toLowerCase()
      return items.filter(i => i.message.toLowerCase().includes(lowerCaseSearch))
    },
  },

  methods: {
    changeMessage(message) {
      Object.assign(message, generateMessage())
    },
  },
}
</script>

<template>
  <div class="dynamic-scroller-demo">
    <div class="toolbar">
      <input
        v-model="search"
        placeholder="Filter..."
      >
    </div>

    <DynamicScroller
      :items="filteredItems"
      :min-item-size="54"
      direction="horizontal"
      class="scroller"
    >
      <template #before>
        <div
          v-if="!dismissInfo"
          class="notice"
        >
          <div>The message widths are unknown.</div>
          <div>Scroll to the left ➡️</div>
          <div>
            <button @click="dismissInfo = true">
              OK
            </button>
          </div>
        </div>
      </template>

      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :size-dependencies="[
            item.message,
          ]"
          :data-index="index"
          :data-active="active"
          :title="`Click to change message ${index}`"
          :style="{
            width: `${Math.max(130, Math.round(item.message.length / 20 * 20))}px`,
          }"
          class="message"
          @click="changeMessage(item)"
        >
          <div class="avatar">
            <img
              :key="item.avatar"
              :src="item.avatar"
              alt="avatar"
              class="image"
            >
          </div>
          <div class="text">
            {{ item.message }}
          </div>
          <div class="index">
            <span>{{ item.id }} (id)</span>
            <span>{{ index }} (index)</span>
          </div>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </div>
</template>

<style scoped>
.dynamic-scroller-demo {
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.scroller {
  flex: auto 1 1;
}

.notice {
  padding: 24px;
  font-size: 20px;
  color: #999;
}

.message {
  display: flex;
  flex-direction: column;
  min-height: 32px;
  padding: 12px;
  box-sizing: border-box;
}

.avatar {
  flex: auto 0 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  margin-bottom: 12px;
}

.avatar .image {
  max-width: 100%;
  max-height: 100%;
  border-radius: 50%;
}

.index,
.text {
  flex: 1;
}

.text {
  margin-bottom: 12px;
}

.index {
  opacity: .5;
}

.index span {
  display: block;
}
</style>


================================================
FILE: packages/demo/src/components/Person.vue
================================================
<script>
export default {
  props: ['item', 'index'],

  methods: {
    edit() {
      // eslint-disable-next-line vue/no-mutating-props
      this.item.value.name += '#'
    },
  },
}
</script>

<template>
  <div
    class="tr person"
    @click="edit"
  >
    <div class="td index">
      {{ index }}
    </div>
    <div class="td">
      <div class="info">
        <img
          :key="item.value.avatar"
          class="avatar"
          :src="item.value.avatar"
        >
        <span>{{ item.value.name }}</span>
      </div>
    </div>
  </div>
</template>

<style scoped>
.index {
  color: rgba(0, 0, 0, 0.2);
  width: 55px;
  text-align: right;
  flex: auto 0 0;
}

.person .td:first-child {
  padding: 12px;
}

.person .info {
  display: flex;
  align-items: center;
  height: 48px;
}

.avatar {
  width: 50px;
  height: 50px;
  margin-right: 12px;
}
</style>


================================================
FILE: packages/demo/src/components/RecycleScrollerDemo.vue
================================================
<script>
import { addItem, getData } from '../data'

import Person from './Person.vue'

export default {
  components: {
    Person,
  },

  data: () => ({
    items: [],
    count: 10000,
    renderScroller: true,
    showScroller: true,
    scopedSlots: false,
    buffer: 200,
    poolSize: 2000,
    enableLetters: true,
    pageMode: false,
    pageModeFullPage: true,
    scrollTo: 100,
    updateParts: { viewStartIdx: 0, viewEndIdx: 0, visibleStartIdx: 0, visibleEndIdx: 0 },
    showMessageBeforeItems: true,
  }),

  computed: {
    countInput: {
      get() {
        return this.count
      },
      set(val) {
        if (val > 500000) {
          val = 500000
        }
        else if (val < 0) {
          val = 0
        }
        this.count = val
      },
    },

    itemHeight() {
      return this.enableLetters ? null : 50
    },

    list() {
      return this.items.map(
        item => ({ ...{
          random: Math.random(),
        }, ...item }),
      )
    },
  },

  watch: {
    count() {
      this.generateItems()
    },
    enableLetters() {
      this.generateItems()
    },
  },

  mounted() {
    this.$nextTick(this.generateItems)
    window.scroller = this.$refs.scroller
  },

  methods: {
    generateItems() {
      this._dirty = true
      this.items = getData(this.count, this.enableLetters)
    },

    addItem() {
      addItem(this.items)
    },

    onUpdate(viewStartIndex, viewEndIndex, visibleStartIndex, visibleEndIndex) {
      this.updateParts.viewStartIdx = viewStartIndex
      this.updateParts.viewEndIdx = viewEndIndex
      this.updateParts.visibleStartIdx = visibleStartIndex
      this.updateParts.visibleEndIdx = visibleEndIndex
    },
  },
}
</script>

<template>
  <div
    class="recycle-scroller-demo"
    :class="{
      'page-mode': pageMode,
      'full-page': pageModeFullPage,
    }"
  >
    <div class="toolbar">
      <span>
        <input
          v-model="countInput"
          type="number"
          min="0"
          max="500000"
        > items
        <button @click="addItem()">+1</button>
      </span>
      <label>
        <input
          v-model="enableLetters"
          type="checkbox"
        > variable height
      </label>
      <label>
        <input
          v-model="pageMode"
          type="checkbox"
        > page mode
      </label>
      <label v-if="pageMode">
        <input
          v-model="pageModeFullPage"
          type="checkbox"
        > full page
      </label>
      <span>
        <input
          v-model.number="buffer"
          type="number"
          min="1"
          max="500000"
        > buffer
      </span>
      <span>
        <button @mousedown="$refs.scroller.scrollToItem(scrollTo)">Scroll To: </button>
        <input
          v-model.number="scrollTo"
          type="number"
          min="0"
          :max="list.length - 1"
        >
      </span>
      <span>
        <button @mousedown="renderScroller = !renderScroller">Toggle render</button>
        <button @mousedown="showScroller = !showScroller">Toggle visibility</button>
      </span>
      <label>
        <input
          v-model="showMessageBeforeItems"
          type="checkbox"
        > show message before items
      </label>
      <span>({{ updateParts.viewStartIdx }} - [{{ updateParts.visibleStartIdx }} - {{ updateParts.visibleEndIdx }}] - {{ updateParts.viewEndIdx }})</span>
    </div>

    <div
      v-if="renderScroller"
      v-show="showScroller"
      class="content"
    >
      <div class="wrapper">
        <RecycleScroller
          :key="pageModeFullPage"
          ref="scroller"
          class="scroller"
          :items="list"
          :item-size="itemHeight"
          :buffer="buffer"
          :page-mode="pageMode"
          key-field="id"
          size-field="height"
          :emit-update="true"
          @update="onUpdate"
        >
          <template #default="props">
            <div
              v-if="props.item.type === 'letter'"
              class="tr letter big"
              @click="props.item.height = (props.item.height === 200 ? 300 : 200)"
            >
              <div class="td index">
                {{ props.index }}
              </div>
              <div class="td value">
                {{ props.item.value }} Scoped
              </div>
            </div>
            <Person
              v-if="props.item.type === 'person'"
              :item="props.item"
              :index="props.index"
            />
          </template>
        </RecycleScroller>
      </div>
    </div>
  </div>
</template>

<style scoped>
.recycle-scroller-demo:not(.page-mode) {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.recycle-scroller-demo.page-mode:not(.full-page) {
  height: 100%;
}

.recycle-scroller-demo.page-mode {
  flex: auto 0 0;
}

.recycle-scroller-demo.page-mode .toolbar {
  border-bottom: solid 1px #e0edfa;
}

.content {
  flex: 100% 1 1;
  border: solid 1px #42b983;
  position: relative;
}

.recycle-scroller-demo.page-mode:not(.full-page) .content {
  overflow: auto;
}

.recycle-scroller-demo:not(.page-mode) .wrapper {
  overflow: hidden;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.scroller {
  width: 100%;
  height: 100%;
}

.notice {
  padding: 24px;
  font-size: 20px;
  color: #999;
}

.letter {
  text-transform: uppercase;
  color: grey;
  font-weight: bold;
}

.letter .td {
  padding: 12px;
}

.letter.big {
  font-weight: normal;
  height: 200px;
}

.letter.big .value {
  font-size: 120px;
}
</style>


================================================
FILE: packages/demo/src/components/SimpleList.vue
================================================
<script>
import { generateMessage } from '../data'

const items = []
for (let i = 0; i < 10000; i++) {
  items.push(generateMessage().message)
}

export default {
  data() {
    return {
      items,
      search: '',
      dynamic: true,
    }
  },

  computed: {
    filteredItems() {
      const { search, items } = this
      if (!search)
        return items
      const lowerCaseSearch = search.toLowerCase()
      return items.filter(i => i.toLowerCase().includes(lowerCaseSearch))
    },
  },
}
</script>

<template>
  <div class="dynamic-scroller-demo">
    <div class="toolbar">
      <input
        v-model="search"
        placeholder="Filter..."
      >

      <label>
        <input
          v-model="dynamic"
          type="checkbox"
        >
        Dynamic scroller
      </label>
    </div>

    <DynamicScroller
      v-if="dynamic"
      :items="filteredItems"
      :min-item-size="54"
      class="scroller"
    >
      <template #before-container>
        <div class="notice">
          Array of simple strings (no objects).
        </div>
      </template>

      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :index="index"
          :active="active"
          :data-index="index"
          :data-active="active"
          class="message"
        >
          <div class="text">
            {{ item }}
          </div>
          <div class="index">
            <span>{{ index }} (index)</span>
          </div>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>

    <RecycleScroller
      v-else
      :items="filteredItems.map((o, i) => `${i}: ${o.substr(0, 42)}...`)"
      :item-size="54"
      class="scroller"
    >
      <template #default="{ item, index }">
        <div class="message">
          <div class="text">
            {{ item }}
          </div>
          <div class="index">
            <span>{{ index }} (index)</span>
          </div>
        </div>
      </template>
    </RecycleScroller>
  </div>
</template>

<style scoped>
.dynamic-scroller-demo {
  flex: auto 1 1;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.scroller {
  flex: auto 1 1;
}

.notice {
  padding: 24px;
  font-size: 20px;
  color: #999;
}

.message {
  display: flex;
  min-height: 32px;
  padding: 12px;
  box-sizing: border-box;
}

.index,
.text {
  flex: 1;
}

.text {
  max-width: 400px;
}

.index span {
  display: inline-block;
  width: 160px;
  text-align: right;
}
</style>


================================================
FILE: packages/demo/src/components/TestChat.vue
================================================
<script>
import { faker } from '@faker-js/faker'

export default {
  name: 'TestChat',

  data() {
    return {
      items: [],
    }
  },

  methods: {
    addItems(count = 1) {
      for (let i = 0; i < count; i++) {
        this.items.push({
          text: faker.lorem.lines(),
          id: this.items.length + 1,
        })
      }
      this.scrollToBottom()
    },

    scrollToBottom() {
      this.$refs.scroller.scrollToBottom()
    },
  },
}
</script>

<template>
  <div class="hello">
    <div>
      <button @click="addItems()">
        Add item
      </button>
      <button @click="addItems(5)">
        Add 5 items
      </button>
      <button @click="addItems(10)">
        Add 10 items
      </button>
      <button @click="addItems(50)">
        Add 50 items
      </button>
    </div>

    <DynamicScroller
      ref="scroller"
      :items="items"
      :min-item-size="24"
      class="scroller"
      @resize="scrollToBottom()"
    >
      <template #default="{ item, index, active }">
        <DynamicScrollerItem
          :item="item"
          :active="active"
          :data-index="index"
        >
          <div class="message">
            {{ item.text }}
          </div>
        </DynamicScrollerItem>
      </template>
    </DynamicScroller>
  </div>
</template>

<style scoped>
.hello {
  flex: 0 1 1;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.scroller {
  flex: auto 1 1;
  border: 2px solid #ddd;
}

h1,
h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}

.message {
  padding: 10px 10px 9px;
  border-bottom: solid 1px #eee;
}
</style>


================================================
FILE: packages/demo/src/data.js
================================================
import { faker } from '@faker-js/faker'

let uid = 0

function generateItem() {
  return {
    name: faker.name.fullName(),
    avatar: faker.internet.avatar(),
  }
}

export function getData(count, letters) {
  const raw = {}

  const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('')

  for (const l of alphabet) {
    raw[l] = []
  }

  for (let i = 0; i < count; i++) {
    const item = generateItem()
    const letter = item.name.charAt(0).toLowerCase()
    raw[letter].push(item)
  }

  const list = []
  let index = 1

  for (const l of alphabet) {
    raw[l] = raw[l].sort((a, b) => a.name < b.name ? -1 : 1)
    if (letters) {
      list.push({
        id: uid++,
        index: index++,
        type: 'letter',
        value: l,
        height: 200,
      })
    }
    for (const item of raw[l]) {
      list.push({
        id: uid++,
        index: index++,
        type: 'person',
        value: item,
        height: 50,
      })
    }
  }

  return list
}

export function addItem(list) {
  list.push({
    id: uid++,
    index: list.length + 1,
    type: 'person',
    value: generateItem(),
    height: 50,
  })
}

export function generateMessage() {
  return {
    avatar: faker.internet.avatar(),
    message: faker.lorem.text(),
  }
}


================================================
FILE: packages/demo/src/main.js
================================================
import { createApp } from 'vue'

import VirtualScroller from 'vue-virtual-scroller'
import App from './App.vue'

import router from './router'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const app = createApp(App)
app.use(router)
app.use(VirtualScroller)
app.mount('#app')


================================================
FILE: packages/demo/src/router.js
================================================
import { createRouter, createWebHistory } from 'vue-router'

import ChatDemo from './components/ChatDemo.vue'
import Dynamic from './components/DynamicScrollerDemo.vue'
import GridDemo from './components/GridDemo.vue'
import Home from './components/Home.vue'
import HorizontalDemo from './components/HorizontalDemo.vue'
import Recycle from './components/RecycleScrollerDemo.vue'
import SimpleList from './components/SimpleList.vue'
import TestChat from './components/TestChat.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/', name: 'home', component: Home },
    { path: '/recycle', name: 'recycle', component: Recycle },
    { path: '/dynamic', name: 'dynamic', component: Dynamic },
    { path: '/test-chat', name: 'test-chat', component: TestChat },
    { path: '/simple-list', name: 'simple-list', component: SimpleList },
    { path: '/horizontal', name: 'horizontal', component: HorizontalDemo },
    { path: '/chat', name: 'chat', component: ChatDemo },
    { path: '/grid', name: 'grid', component: GridDemo },
  ],
})

export default router


================================================
FILE: packages/demo/vite.config.js
================================================
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue()],
})


================================================
FILE: packages/vue-virtual-scroller/LICENSE
================================================
MIT License

Copyright (c) 2020 guillaume.b.chau@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: packages/vue-virtual-scroller/README.md
================================================
# vue-virtual-scroller

[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg) ![npm](https://img.shields.io/npm/dm/vue-virtual-scroller.svg)](https://www.npmjs.com/package/vue-virtual-scroller)
[![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/)

[Documentation](https://vue-virtual-scroller.netlify.app/)

Blazing fast scrolling of any amount of data | [Live demo](https://vue-virtual-scroller-demo.netlify.app/) | [Video demo](https://www.youtube.com/watch?v=Uzq1KQV8f4k)

For Vue 2 support, see [here](https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller)

This package ships ESM only in the current Vue 3 line. Use it with an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.

[💚️ Become a Sponsor](https://github.com/sponsors/Akryum)

## Sponsors

<p align="center">
  <a href="https://guillaume-chau.info/sponsors/" target="_blank">
    <img src='https://akryum.netlify.app/sponsors.svg' alt="sponsors" />
  </a>
</p>


================================================
FILE: packages/vue-virtual-scroller/package.json
================================================
{
  "name": "vue-virtual-scroller",
  "type": "module",
  "version": "2.0.0-beta.10",
  "description": "Smooth scrolling for any amount of data",
  "author": {
    "name": "Guillaume Chau",
    "email": "guillaume.b.chau@gmail.com"
  },
  "license": "MIT",
  "homepage": "https://github.com/Akryum/vue-virtual-scroller#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Akryum/vue-virtual-scroller.git"
  },
  "bugs": {
    "url": "https://github.com/Akryum/vue-virtual-scroller/issues"
  },
  "keywords": [
    "vue",
    "vuejs",
    "plugin"
  ],
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/vue-virtual-scroller.js",
      "default": "./dist/vue-virtual-scroller.js"
    },
    "./dist/vue-virtual-scroller.css": "./dist/vue-virtual-scroller.css",
    "./index.css": "./dist/vue-virtual-scroller.css"
  },
  "module": "dist/vue-virtual-scroller.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "skills"
  ],
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch",
    "test": "vitest run",
    "test:watch": "vitest",
    "prepublishOnly": "pnpm run lint && pnpm run build",
    "lint": "cd ../../ && pnpm run lint"
  },
  "peerDependencies": {
    "vue": "^3.2.0"
  },
  "dependencies": {
    "mitt": "^2.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "@vue/test-utils": "^2.4.6",
    "jsdom": "^28.1.0",
    "typescript": "^5.3.3",
    "vite": "^6.0.0",
    "vite-plugin-dts": "^4.0.0",
    "vitest": "4.0.16",
    "vue": "^3.2.41"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],
  "publishConfig": {
    "access": "public"
  }
}


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md
================================================
---
name: vue-virtual-scroller
description: Use this skill for Vue 3 virtual scrolling with vue-virtual-scroller, including RecycleScroller, DynamicScroller, DynamicScrollerItem, and the useRecycleScroller headless composable for fixed-size lists, unknown-size rows, grids, chat feeds, and horizontal layouts.
---

# Vue Virtual Scroller

Use this skill when a task involves large Vue lists, DOM reuse, windowed rendering, or choosing between `RecycleScroller`, `DynamicScroller`, and headless virtualization with `useRecycleScroller`.

## Quick choice

| Surface | Use it when | Avoid it when |
|---|---|---|
| `RecycleScroller` | Item size is fixed or precomputed, or items expose a numeric size field for variable-size mode. | You need automatic measurement of unknown item sizes. |
| `DynamicScroller` | Item sizes are not known ahead of time and should be discovered during rendering. | You can provide a stable fixed size and want the lightest path. |
| `DynamicScrollerItem` | You are rendering children inside `DynamicScroller` and need size measurement updates. | You are not inside `DynamicScroller`. |
| `useRecycleScroller` | You need the virtualization engine but want custom markup, styling, or rendering control. | The slot-based component APIs already fit the UI. |

## Setup

`vue-virtual-scroller` targets Vue 3 and ships ESM only. Use it with an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.

```sh
pnpm add vue-virtual-scroller@next
```

Always import the package CSS:

```js
import 'vue-virtual-scroller/index.css'
```

Install all bundled components:

```js
import { createApp } from 'vue'
import VueVirtualScroller from 'vue-virtual-scroller'

const app = createApp(App)
app.use(VueVirtualScroller)
```

Or register/import only what you need:

```js
import { RecycleScroller } from 'vue-virtual-scroller'

app.component('RecycleScroller', RecycleScroller)
```

## Workflow

1. Decide whether sizes are known.
2. If sizes are fixed or already available on each item, start with `RecycleScroller`.
3. If sizes are unknown and discovered after render, use `DynamicScroller` with `DynamicScrollerItem`.
4. If the component slot structure is too limiting, switch to `useRecycleScroller`.
5. Set explicit scroll-container sizing and item sizing before debugging performance.

## Sizing rules

- The scroller element itself must have a real scrollable size such as a fixed `height` or `width` plus overflow.
- In `RecycleScroller`, all items should have the same size unless you intentionally use variable-size mode with `itemSize: null` and a numeric item field such as `size`.
- In `DynamicScroller`, `minItemSize` is required for initial layout.
- Horizontal lists use the same primitives, but sizing constraints apply on width instead of height.
- Grid mode is only supported with `RecycleScroller` and fixed item sizing.

## Practical guidance

### Fixed-size vs unknown-size

- Prefer `RecycleScroller` for tables, simple rows, and card lists where row height or card extent is stable.
- Use `RecycleScroller` variable-size mode only when the item already knows its size and can expose it through `sizeField`.
- Prefer `DynamicScroller` when the DOM must measure content, such as chat messages or cards whose rendered content changes height.

### Rendering pitfalls

- Reused views mean child components must react correctly when `item` changes; do not assume a fresh component instance per row.
- Functional components inside `RecycleScroller` are discouraged because reuse makes them slower, not faster.
- Do not add unnecessary `key` values to the immediate list content, but do key nested images to avoid load glitches.
- Use the provided `hover` class for hover styling instead of relying on `:hover` against recycled DOM nodes.

### Performance guardrails

- Variable-size mode in `RecycleScroller` can be expensive on very large lists.
- `watchData` on `DynamicScrollerItem` is documented as not recommended because deep watching can hurt performance.
- `emitUpdate` on `RecycleScroller` and `emitResize` on `DynamicScrollerItem` add extra work; keep them off unless the UI needs those events.
- Browsers still impose large-element size limits, so extremely large lists can hit practical limits around hundreds of thousands of items.

### Common patterns

- Chat feeds and append-heavy timelines map well to `DynamicScroller` plus `DynamicScrollerItem`.
- Multi-column card galleries map to `RecycleScroller` grid mode with `gridItems` and `itemSecondarySize`.
- Horizontal virtualized cards map to `DynamicScroller` with `direction="horizontal"` when widths are content-driven.
- Design-system integrations or nonstandard DOM trees map to `useRecycleScroller`.

## Scope limits

This skill intentionally focuses on the documented public surfaces:

- setup and installation
- `RecycleScroller`
- `DynamicScroller`
- `DynamicScrollerItem`
- `useRecycleScroller`

Do not infer undocumented behavior for these exported surfaces without updating docs first:

- `useDynamicScroller`
- `useDynamicScrollerItem`
- `useIdState`
- plugin install options beyond the documented setup path

## References

| Topic | Description | Reference |
|---|---|---|
| Installation and setup | Vue 3 setup, ESM-only constraint, CSS import, and component registration paths. | [references/installation-and-setup.md](./references/installation-and-setup.md) |
| RecycleScroller | Fixed-size lists, variable-size mode with explicit size fields, grid mode, page mode, and core events. | [references/recycle-scroller.md](./references/recycle-scroller.md) |
| DynamicScroller | Unknown-size rendering strategy and when to move off the fixed-size path. | [references/dynamic-scroller.md](./references/dynamic-scroller.md) |
| DynamicScrollerItem | Size measurement wrapper behavior, dependencies, and resize events. | [references/dynamic-scroller-item.md](./references/dynamic-scroller-item.md) |
| useRecycleScroller | Headless virtualization API for custom DOM structures. | [references/use-recycle-scroller.md](./references/use-recycle-scroller.md) |
| Reference index | Overview of all shipped references. | [references/index.md](./references/index.md) |

## Further reading

- [references/installation-and-setup.md](./references/installation-and-setup.md)
- [references/recycle-scroller.md](./references/recycle-scroller.md)
- [references/dynamic-scroller.md](./references/dynamic-scroller.md)
- [references/dynamic-scroller-item.md](./references/dynamic-scroller-item.md)
- [references/use-recycle-scroller.md](./references/use-recycle-scroller.md)


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md
================================================
# DynamicScrollerItem

Scope: the per-item measurement wrapper used inside `DynamicScroller`.

## Provenance

Generated from the package's public dynamic-item documentation and shipped demo patterns at skill generation time.

## When to use

- You are rendering an item inside `DynamicScroller`.
- The rendered content can change size after first render.
- You need resize events for item-level measurement updates.

## Required inputs

- `item`
- `active`

Recommended for real updates:

- `sizeDependencies`

## Core props/options

- `item`
- `active`
- `sizeDependencies`
- `watchData`
- `tag`
- `emitResize`

Documented guidance:

- Prefer `sizeDependencies` over `watchData`.
- `watchData` deeply watches the item and is not recommended for performance-sensitive lists.

## Events/returns

Documented event:

- `resize` when `emitResize` is `true`

## Pitfalls

- Omitting `active` breaks the documented optimization path for avoiding unnecessary recomputation.
- Reaching for `watchData` first is usually the wrong tradeoff; use targeted dependencies instead.
- `emitResize` increases work and should be enabled only when the parent UI needs to react.

## Example patterns

Track text-driven height changes:

```vue
<DynamicScrollerItem
  :item="item"
  :active="active"
  :size-dependencies="[item.message]"
>
  <p>{{ item.message }}</p>
</DynamicScrollerItem>
```

Custom tag:

```vue
<DynamicScrollerItem
  :item="item"
  :active="active"
  tag="article"
>
  {{ item.title }}
</DynamicScrollerItem>
```


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md
================================================
# DynamicScroller

Scope: the component path for unknown-size items that must be measured as they render.

## Provenance

Generated from the package's public dynamic-sizing documentation and shipped demo patterns at skill generation time.

## When to use

- Item heights or widths are not known before render.
- Message-like content can grow or change after filtering or appending.
- You need automatic size discovery instead of a precomputed size field.

## Required inputs

- `items`
- `minItemSize` for the initial render path
- A `DynamicScrollerItem` around each rendered item
- Scroll-container sizing in CSS

## Core props/options

`DynamicScroller` extends the documented `RecycleScroller` props.

Key documented guidance:

- `minItemSize` is required.
- It is not recommended to change `sizeField`, because size management is handled internally.
- You do not need a `size` field on each item.

Important inherited props often used in practice:

- `direction`
- `buffer`
- `pageMode`
- `prerender`

## Events/returns

`DynamicScroller` extends the documented `RecycleScroller` events, slot props, and other slots.

The default slot still exposes:

- `item`
- `index`
- `active`

## Pitfalls

- `DynamicScroller` does not detect size changes by itself; dynamic inputs must be forwarded to `DynamicScrollerItem` through `sizeDependencies`.
- Missing `minItemSize` degrades the initial layout.
- This path is heavier than fixed-size virtualization, so prefer `RecycleScroller` when item size is already known.

## Example patterns

Unknown-height rows:

```vue
<DynamicScroller
  :items="items"
  :min-item-size="54"
>
  <template #default="{ item, index, active }">
    <DynamicScrollerItem
      :item="item"
      :active="active"
      :size-dependencies="[item.message]"
      :data-index="index"
    >
      {{ item.message }}
    </DynamicScrollerItem>
  </template>
</DynamicScroller>
```

Append-heavy chat feed:

```vue
<DynamicScroller
  ref="scroller"
  :items="rows"
  :min-item-size="48"
  @resize="scroller?.scrollToBottom()"
>
  <template #default="{ item, active }">
    <DynamicScrollerItem
      :item="item"
      :active="active"
      :size-dependencies="[item.text]"
    >
      {{ item.text }}
    </DynamicScrollerItem>
  </template>
</DynamicScroller>
```

Horizontal dynamic cards:

```vue
<DynamicScroller
  :items="filteredRows"
  :min-item-size="180"
  direction="horizontal"
>
  <template #default="{ item, active }">
    <DynamicScrollerItem
      :item="item"
      :active="active"
      :size-dependencies="[item.message]"
    >
      {{ item.message }}
    </DynamicScrollerItem>
  </template>
</DynamicScroller>
```


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md
================================================
# Vue Virtual Scroller References

Focused reference map for the documented public surfaces covered by this skill.

| Topic | Description | Reference |
|---|---|---|
| Installation and setup | Package install, ESM-only note, CSS import, and component registration options. | [installation-and-setup.md](./installation-and-setup.md) |
| RecycleScroller | Core component for fixed-size and pre-sized virtual lists, including grid and page mode. | [recycle-scroller.md](./recycle-scroller.md) |
| DynamicScroller | Wrapper over `RecycleScroller` for unknown-size items measured during rendering. | [dynamic-scroller.md](./dynamic-scroller.md) |
| DynamicScrollerItem | Required child wrapper for dynamic measurement in `DynamicScroller`. | [dynamic-scroller-item.md](./dynamic-scroller-item.md) |
| useRecycleScroller | Headless composable for custom markup and rendering control. | [use-recycle-scroller.md](./use-recycle-scroller.md) |

## Coverage notes

- `IdState` is documented in the guide, but the package currently exports `useIdState`, so it is intentionally not shipped as a reference here until docs and exports are reconciled.
- `useDynamicScroller` and `useDynamicScrollerItem` are exported but do not currently have dedicated guide pages, so they are omitted from this initial skill.


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md
================================================
# Installation And Setup

Scope: install the package correctly in a Vue 3 app and avoid the common setup mistakes that make examples appear broken.

## Provenance

Generated from the package's public setup documentation at skill generation time.

## When to use

- You are adding `vue-virtual-scroller` to a Vue 3 application.
- You need to decide between global plugin install and direct component import.
- A demo or example is not rendering because the package CSS or ESM requirement was missed.

## Required inputs

- Vue 3 application context.
- ESM-aware build tool such as Vite, Nuxt, Rollup, or webpack 5.
- The package stylesheet import: `vue-virtual-scroller/index.css`.

## Core props/options

- Global plugin install:
  - `app.use(VueVirtualScroller)`
- Direct component import:
  - import and register `RecycleScroller`, `DynamicScroller`, or `DynamicScrollerItem` explicitly

The current guide documents the setup path, but does not fully document plugin option behavior. Keep setup guidance to the documented install patterns unless the docs are expanded first.

## Events/returns

- None at setup time.

## Pitfalls

- The package is ESM only in the current Vue 3 line.
- Missing the CSS import will make layout and virtualization behavior appear incorrect.
- Vue 2 references still exist in historical material, but they are out of scope for this package line.

## Example patterns

Global registration:

```js
import { createApp } from 'vue'
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/index.css'

const app = createApp(App)
app.use(VueVirtualScroller)
```

Direct import:

```js
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/index.css'

app.component('RecycleScroller', RecycleScroller)
```


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md
================================================
# RecycleScroller

Scope: the main component for virtualizing fixed-size lists, pre-sized variable lists, grids, and page-level scrolling.

## Provenance

Generated from the package's public component documentation and shipped demo patterns at skill generation time.

## When to use

- Item height or width is fixed.
- Each item already exposes a numeric size field and you want variable-size mode without DOM measurement.
- You need grid rendering with fixed item dimensions.
- You want page mode or SSR prerender support through the component API.

## Required inputs

- `items`
- A stable scroller size in CSS.
- `itemSize` for fixed-size mode, or `itemSize: null` plus a numeric size field for variable-size mode.
- `keyField` when object items do not use `id`.

## Core props/options

Common props:

- `items`
- `direction`
- `itemSize`
- `keyField`
- `buffer`
- `pageMode`
- `prerender`

Variable-size mode:

- `itemSize: null`
- `sizeField`
- `minItemSize` for unknown item sizes before they are fully known

Grid mode:

- `gridItems`
- `itemSecondarySize`

Other documented props:

- `typeField`
- `emitUpdate`
- `updateInterval`
- `listClass`
- `itemClass`
- `listTag`
- `itemTag`

## Events/returns

Documented events:

- `resize`
- `visible`
- `hidden`
- `update(startIndex, endIndex, visibleStartIndex, visibleEndIndex)` when `emitUpdate` is enabled
- `scroll-start`
- `scroll-end`

Default slot props:

- `item`
- `index`
- `active`

Named slots:

- `before`
- `empty`
- `after`

## Pitfalls

- The scroller element and item elements must be sized correctly with CSS.
- Do not use functional components in recycled views when performance matters.
- Child components must respond to `item` changing because views are reused.
- Nested images should still receive keys to avoid load glitches.
- Use the `hover` class rather than raw `:hover` selectors on recycled nodes.
- Browser element-size limits make extremely large lists impractical.
- Variable-size mode can become expensive with many items.

## Example patterns

Fixed-size rows:

```vue
<RecycleScroller
  v-slot="{ item }"
  :items="list"
  :item-size="32"
  key-field="id"
>
  <div class="row">
    {{ item.name }}
  </div>
</RecycleScroller>
```

Grid layout:

```vue
<RecycleScroller
  :items="cards"
  :item-size="166"
  :grid-items="5"
  :item-secondary-size="176"
>
  <template #default="{ item }">
    <article>{{ item.name }}</article>
  </template>
</RecycleScroller>
```

Page mode:

```vue
<RecycleScroller
  :items="items"
  :item-size="42"
  page-mode
/>
```


================================================
FILE: packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md
================================================
# useRecycleScroller

Scope: the headless virtualization composable for building custom scroll UIs without the bundled component markup.

## Provenance

Generated from the package's public headless virtualization documentation at skill generation time.

## When to use

- You need a custom DOM structure that does not fit the component slot API.
- You want to integrate virtualization into an existing design-system component.
- You want direct control over pooled views, scroll handling, and item placement.

## Required inputs

- A scroller element ref.
- An options object containing the same core settings used by `RecycleScroller`:
  - `items`
  - `keyField`
  - `direction`
  - `itemSize`
  - `minItemSize`
  - `sizeField`
  - `typeField`
  - `buffer`
  - `pageMode`
  - `prerender`
  - `emitUpdate`
  - `updateInterval`

Optional grid inputs:

- `gridItems`
- `itemSecondarySize`

## Core props/options

Fixed-size path:

- set `itemSize` to a number

Variable-size path:

- set `itemSize` to `null`
- provide a numeric field on each item
- set `sizeField` if that field is not `size`

The composable manages virtualization state, but markup, CSS, and event wiring stay in user land.

## Events/returns

Documented returns used most often:

- `pool`
- `totalSize`
- `handleScroll`
- `scrollToItem(index)`
- `scrollToPosition(px)`
- `getScroll()`
- `updateVisibleItems(itemsChanged, checkPositionDiff?)`

## Pitfalls

- You must provide your own scrollable sizing styles.
- Without a stable key field, object-item reuse becomes unreliable.
- This composable does not provide measurement for unknown-size items; move to `DynamicScroller` when content size must be discovered from the DOM.

## Example patterns

Minimal fixed-size setup:

```vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRecycleScroller } from 'vue-virtual-scroller'

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i + 1,
  name: `User ${i + 1}`,
})))

const scrollerEl = ref<HTMLElement>()

const options = computed(() => ({
  items: items.value,
  keyField: 'id',
  direction: 'vertical' as const,
  itemSize: 40,
  minItemSize: null,
  sizeField: 'size',
  typeField: 'type',
  buffer: 200,
  pageMode: false,
  prerender: 0,
  emitUpdate: false,
  updateInterval: 0,
}))

const { pool, totalSize, handleScroll } = useRecycleScroller(options, scrollerEl)
</script>
```

Variable-size setup with explicit item field:

```ts
const options = computed(() => ({
  items: items.value,
  keyField: 'id',
  direction: 'vertical' as const,
  itemSize: null,
  minItemSize: 40,
  sizeField: 'size',
  typeField: 'type',
  buffer: 200,
  pageMode: false,
  prerender: 0,
  emitUpdate: false,
  updateInterval: 0,
}))
```


================================================
FILE: packages/vue-virtual-scroller/src/components/DynamicScroller.spec.ts
================================================
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, h } from 'vue'
import DynamicScroller from './DynamicScroller.vue'

const scrollerScrollToItem = vi.fn()

const RecycleScrollerStub = defineComponent({
  name: 'RecycleScroller',
  props: {
    items: {
      type: Array,
      default: () => [],
    },
    minItemSize: {
      type: [Number, String],
      required: true,
    },
    direction: {
      type: String,
      default: 'vertical',
    },
    keyField: {
      type: String,
      default: 'id',
    },
    listTag: {
      type: String,
      default: 'div',
    },
    itemTag: {
      type: String,
      default: 'div',
    },
  },
  emits: ['resize', 'visible'],
  setup(props, { slots, emit, expose }) {
    const el = document.createElement('div')
    expose({
      el,
      scrollToItem: scrollerScrollToItem,
    })

    return () => h('div', { class: 'recycle-scroller-stub' }, [
      slots.before?.(),
      ...(props.items as unknown[]).map((item, index) => slots.default?.({
        item,
        index,
        active: index === 0,
      })),
      ...(props.items as unknown[]).length === 0
        ? slots.empty?.() ?? []
        : [],
      slots.after?.(),
      h('button', {
        class: 'emit-resize',
        type: 'button',
        onClick: () => emit('resize'),
      }),
      h('button', {
        class: 'emit-visible',
        type: 'button',
        onClick: () => emit('visible'),
      }),
    ])
  },
})

function mountDynamicScroller(props: any, slots?: any) {
  return mount(DynamicScroller, {
    props,
    slots,
    global: {
      stubs: {
        RecycleScroller: RecycleScrollerStub,
      },
    },
  })
}

describe('dynamicScroller', () => {
  beforeEach(() => {
    scrollerScrollToItem.mockReset()
  })

  it('forwards slot bindings from itemWithSize', () => {
    const items = [
      { id: 'a', label: 'Alpha' },
      { id: 'b', label: 'Beta' },
    ]

    const wrapper = mountDynamicScroller(
      {
        items,
        minItemSize: 20,
      },
      {
        default: ({ item, index, active, itemWithSize }: any) => h('div', { class: 'row' }, `${item.label}|${index}|${active ? 'active' : 'inactive'}|${itemWithSize.id}`),
        before: () => h('div', { class: 'before-slot' }, 'before'),
        after: () => h('div', { class: 'after-slot' }, 'after'),
        empty: () => h('div', { class: 'empty-slot' }, 'empty'),
      },
    )

    const rows = wrapper.findAll('.row')
    expect(rows).toHaveLength(2)
    expect(rows[0].text()).toBe('Alpha|0|active|a')
    expect(rows[1].text()).toBe('Beta|1|inactive|b')
    expect(wrapper.find('.before-slot').exists()).toBe(true)
    expect(wrapper.find('.after-slot').exists()).toBe(true)
    expect(wrapper.find('.empty-slot').exists()).toBe(false)
  })

  it('renders empty slot only when items is empty', () => {
    const wrapper = mountDynamicScroller(
      {
        items: [],
        minItemSize: 20,
      },
      {
        empty: () => h('div', { class: 'empty-slot' }, 'empty'),
      },
    )

    expect(wrapper.find('.empty-slot').exists()).toBe(true)
  })

  it('passes props to RecycleScroller and re-emits resize and visible', async () => {
    const wrapper = mountDynamicScroller({
      items: [{ id: 1 }],
      minItemSize: 24,
      direction: 'horizontal',
      listTag: 'ul',
      itemTag: 'li',
    })

    const scroller = wrapper.getComponent({ name: 'RecycleScroller' })
    expect(scroller.props('minItemSize')).toBe(24)
    expect(scroller.props('direction')).toBe('horizontal')
    expect(scroller.props('listTag')).toBe('ul')
    expect(scroller.props('itemTag')).toBe('li')

    await scroller.get('.emit-resize').trigger('click')
    await scroller.get('.emit-visible').trigger('click')

    expect(wrapper.emitted('resize')).toHaveLength(1)
    expect(wrapper.emitted('visible')).toHaveLength(1)
  })

  it('exposes scrollToItem and getItemSize', () => {
    const items = [
      { id: 'a', label: 'Alpha' },
    ]
    const wrapper = mountDynamicScroller({
      items,
      minItemSize: 20,
    })
    const vm = wrapper.vm as any

    vm.scrollToItem(3)
    expect(scrollerScrollToItem).toHaveBeenCalledWith(3)
    expect(vm.getItemSize(items[0])).toBe(0)
  })
})


================================================
FILE: packages/vue-virtual-scroller/src/components/DynamicScroller.vue
================================================
<script setup lang="ts">
import type { ItemWithSize, ScrollDirection } from '../types'
import { computed, ref } from 'vue'
import { useDynamicScroller } from '../composables/useDynamicScroller'
import RecycleScroller from './RecycleScroller.vue'

defineOptions({
  inheritAttrs: false,
})

const props = withDefaults(defineProps<{
  items: unknown[]
  keyField?: string
  direction?: ScrollDirection
  listTag?: string
  itemTag?: string
  minItemSize: number | string
}>(), {
  keyField: 'id',
  direction: 'vertical',
  listTag: 'div',
  itemTag: 'div',
})

const emit = defineEmits<{
  resize: []
  visible: []
}>()

// Template refs
const scroller = ref<InstanceType<typeof RecycleScroller>>()

// Derive the root DOM element from the scroller's exposed el ref
const scrollerEl = computed(() => scroller.value?.el)

const {
  itemsWithSize,
  forceUpdate,
  scrollToItem,
  getItemSize,
  scrollToBottom,
  onScrollerResize,
  onScrollerVisible,
} = useDynamicScroller(
  props,
  scroller,
  scrollerEl,
  {
    onResize: () => emit('resize'),
    onVisible: () => emit('visible'),
  },
)

function getDefaultSlotBindings(itemWithSize: unknown, index: number, active: boolean) {
  const typedItem = itemWithSize as ItemWithSize
  return {
    item: typedItem.item,
    index,
    active,
    itemWithSize: typedItem,
  }
}

// Expose
defineExpose({
  scrollToItem,
  scrollToBottom,
  getItemSize,
  forceUpdate,
})
</script>

<template>
  <RecycleScroller
    ref="scroller"
    :items="itemsWithSize"
    :min-item-size="props.minItemSize"
    :direction="props.direction"
    key-field="id"
    :list-tag="props.listTag"
    :item-tag="props.itemTag"
    v-bind="$attrs"
    @resize="onScrollerResize"
    @visible="onScrollerVisible"
  >
    <template #default="{ item: itemWithSize, index, active }">
      <slot v-bind="getDefaultSlotBindings(itemWithSize, index, active)" />
    </template>
    <template
      v-if="$slots.before"
      #before
    >
      <slot name="before" />
    </template>
    <template
      v-if="$slots.after"
      #after
    >
      <slot name="after" />
    </template>
    <template #empty>
      <slot name="empty" />
    </template>
  </RecycleScroller>
</template>


================================================
FILE: packages/vue-virtual-scroller/src/components/DynamicScrollerItem.spec.ts
================================================
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed } from 'vue'
import DynamicScrollerItem from './DynamicScrollerItem.vue'

const mocks = vi.hoisted(() => {
  return {
    useDynamicScrollerItem: vi.fn(),
  }
})

vi.mock('../composables/useDynamicScrollerItem', () => {
  return {
    useDynamicScrollerItem: mocks.useDynamicScrollerItem,
  }
})

describe('dynamicScrollerItem', () => {
  beforeEach(() => {
    mocks.useDynamicScrollerItem.mockReset()
    mocks.useDynamicScrollerItem.mockReturnValue({
      id: computed(() => 'row-1'),
      size: computed(() => 0),
      finalActive: computed(() => true),
      updateSize: vi.fn(),
    })
  })

  it('renders the configured tag and wires props to useDynamicScrollerItem', () => {
    const wrapper = mount(DynamicScrollerItem, {
      props: {
        item: { id: 'row-1' },
        active: true,
        watchData: true,
        emitResize: true,
        tag: 'section',
      },
      slots: {
        default: 'content',
      },
    })

    const [optionsArg, elRefArg] = mocks.useDynamicScrollerItem.mock.calls[0]

    expect(wrapper.element.tagName).toBe('SECTION')
    expect(wrapper.text()).toBe('content')
    expect(optionsArg.item).toEqual({ id: 'row-1' })
    expect(optionsArg.active).toBe(true)
    expect(optionsArg.watchData).toBe(true)
    expect(optionsArg.emitResize).toBe(true)
    expect(elRefArg).toHaveProperty('value')
  })

  it('re-emits resize from composable callbacks', () => {
    let onResize: ((id: string | number) => void) | undefined
    mocks.useDynamicScrollerItem.mockImplementation((_options, _el, callbacks) => {
      onResize = callbacks?.onResize
      return {
        id: computed(() => 'row-1'),
        size: computed(() => 0),
        finalActive: computed(() => true),
        updateSize: vi.fn(),
      }
    })

    const wrapper = mount(DynamicScrollerItem, {
      props: {
        item: { id: 'row-1' },
        active: true,
      },
    })

    onResize?.('row-1')

    expect(wrapper.emitted('resize')).toEqual([['row-1']])
  })
})


================================================
FILE: packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue
================================================
<script setup lang="ts">
import { ref } from 'vue'
import { useDynamicScrollerItem } from '../composables/useDynamicScrollerItem'

const props = withDefaults(defineProps<{
  item: unknown
  watchData?: boolean
  active: boolean
  index?: number
  sizeDependencies?: Record<string, unknown> | unknown[] | null
  emitResize?: boolean
  tag?: string
}>(), {
  watchData: false,
  index: undefined,
  sizeDependencies: null,
  emitResize: false,
  tag: 'div',
})

const emit = defineEmits<{
  resize: [id: string | number]
}>()

defineSlots()

const el = ref<HTMLElement>()

useDynamicScrollerItem(
  props,
  el,
  {
    onResize: id => emit('resize', id),
  },
)
</script>

<template>
  <component
    :is="props.tag"
    ref="el"
  >
    <slot />
  </component>
</template>


================================================
FILE: packages/vue-virtual-scroller/src/components/ItemView.spec.ts
================================================
import type { View } from '../types'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { h } from 'vue'
import ItemView from './ItemView.vue'

describe('itemView', () => {
  it('renders the configured tag and forwards slot props', () => {
    const view: View = {
      item: { label: 'Alpha' },
      position: 0,
      offset: 0,
      nr: {
        id: 1,
        index: 2,
        used: true,
        key: 'a',
        type: 'default',
      },
    }

    const wrapper = mount(ItemView, {
      props: {
        view,
        itemTag: 'li',
      },
      slots: {
        default: ({ item, index, active }: any) => h('span', { class: 'slot-content' }, `${item.label}|${index}|${active}`),
      },
    })

    expect(wrapper.element.tagName).toBe('LI')
    expect(wrapper.find('.slot-content').text()).toBe('Alpha|2|true')
  })

  it('uses nr.used for the active slot prop', () => {
    const wrapper = mount(ItemView, {
      props: {
        view: {
          item: { label: 'Beta' },
          position: 0,
          offset: 0,
          nr: {
            id: 2,
            index: 0,
            used: false,
            key: 'b',
            type: 'default',
          },
        },
        itemTag: 'div',
      },
      slots: {
        default: ({ active }: any) => h('span', { class: 'active-flag' }, String(active)),
      },
    })

    expect(wrapper.find('.active-flag').text()).toBe('false')
  })
})


================================================
FILE: packages/vue-virtual-scroller/src/components/ItemView.vue
================================================
<!-- Avoid re-renders of slots -->

<script setup lang="ts">
import type { View } from '../types'

const props = defineProps<{
  view: View
  itemTag: string
}>()
</script>

<template>
  <component
    :is="props.itemTag"
    class="vue-recycle-scroller__item-view"
  >
    <slot
      :item="props.view.item"
      :index="props.view.nr.index"
      :active="props.view.nr.used"
    />
  </component>
</template>


================================================
FILE: packages/vue-virtual-scroller/src/components/RecycleScroller.spec.ts
================================================
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, defineComponent, h, nextTick, ref } from 'vue'
import RecycleScroller from './RecycleScroller.vue'

const mocks = vi.hoisted(() => {
  return {
    useRecycleScroller: vi.fn(),
    scrollToItem: vi.fn(),
    scrollToPosition: vi.fn(),
    getScroll: vi.fn(),
    updateVisibleItems: vi.fn(),
    handleScroll: vi.fn(),
    handleResize: vi.fn(),
    handleVisibilityChange: vi.fn(),
  }
})

vi.mock('../composables/useRecycleScroller', () => {
  return {
    useRecycleScroller: mocks.useRecycleScroller,
  }
})

const ResizeObserverStub = defineComponent({
  name: 'ResizeObserver',
  emits: ['notify'],
  setup(_props, { emit }) {
    return () => h('button', {
      class: 'resize-observer-notify',
      type: 'button',
      onClick: () => emit('notify'),
    })
  },
})

describe('recycleScroller', () => {
  beforeEach(() => {
    mocks.useRecycleScroller.mockReset()
    mocks.scrollToItem.mockReset()
    mocks.scrollToPosition.mockReset()
    mocks.getScroll.mockReset()
    mocks.updateVisibleItems.mockReset()
    mocks.handleScroll.mockReset()
    mocks.handleResize.mockReset()
    mocks.handleVisibilityChange.mockReset()

    mocks.getScroll.mockReturnValue({ start: 0, end: 80 })
    mocks.updateVisibleItems.mockReturnValue({ continuous: true })

    mocks.useRecycleScroller.mockImplementation((_options, _el, _before, _after, callbacks) => {
      return {
        pool: ref([{
          item: { id: 'a', label: 'Alpha' },
          position: 0,
          offset: 0,
          nr: {
            id: 1,
            index: 0,
            used: true,
            key: 'a',
            type: 'default',
          },
        }]),
        totalSize: ref(320),
        ready: ref(true),
        sizes: computed(() => []),
        simpleArray: computed(() => false),
        scrollToItem: (index: number) => mocks.scrollToItem(index),
        scrollToPosition: (position: number) => mocks.scrollToPosition(position),
        getScroll: () => mocks.getScroll(),
        updateVisibleItems: (itemsChanged: boolean, checkPositionDiff?: boolean) =>
          mocks.updateVisibleItems(itemsChanged, checkPositionDiff),
        handleScroll: () => mocks.handleScroll(),
        handleResize: () => {
          mocks.handleResize()
          callbacks?.onResize?.()
        },
        handleVisibilityChange: (isVisible: boolean, entry: IntersectionObserverEntry) => {
          mocks.handleVisibilityChange(isVisible, entry)
          if (isVisible)
            callbacks?.onVisible?.()
          else
            callbacks?.onHidden?.()
        },
        sortViews: vi.fn(),
      }
    })
  })

  it('renders slots and forwards default slot props from pool views', async () => {
    const wrapper = mount(RecycleScroller, {
      props: {
        items: [{ id: 'a' }],
        itemSize: 20,
      },
      slots: {
        before: () => h('div', { class: 'before-slot' }, 'before'),
        default: ({ item, index, active }: any) => h('div', { class: 'row' }, `${item.label}|${index}|${active}`),
        empty: () => h('div', { class: 'empty-slot' }, 'empty'),
        after: () => h('div', { class: 'after-slot' }, 'after'),
      },
      global: {
        stubs: {
          ResizeObserver: ResizeObserverStub,
        },
      },
    })

    await nextTick()

    expect(wrapper.classes()).toContain('vue-recycle-scroller')
    expect(wrapper.classes()).toContain('ready')
    expect(wrapper.classes()).toContain('direction-vertical')
    expect(wrapper.find('.before-slot').exists()).toBe(true)
    expect(wrapper.find('.after-slot').exists()).toBe(true)
    expect(wrapper.find('.empty-slot').exists()).toBe(false)
    expect(wrapper.find('.row').text()).toBe('Alpha|0|true')
    expect(wrapper.emitted('visible')).toHaveLength(1)
  })

  it('renders empty slot only when items is empty', async () => {
    const wrapper = mount(RecycleScroller, {
      props: {
        items: [],
        itemSize: 20,
      },
      slots: {
        empty: () => h('div', { class: 'empty-slot' }, 'empty'),
      },
      global: {
        stubs: {
          ResizeObserver: ResizeObserverStub,
        },
      },
    })

    await nextTick()

    expect(wrapper.find('.empty-slot').exists()).toBe(true)
  })

  it('wires scroll/resize handlers and exposes composable methods', async () => {
    const wrapper = mount(RecycleScroller, {
      props: {
        items: [{ id: 'a' }],
        itemSize: 20,
      },
      global: {
        stubs: {
          ResizeObserver: ResizeObserverStub,
        },
      },
    })
    const vm = wrapper.vm as any

    await wrapper.trigger('scroll')
    await wrapper.get('.resize-observer-notify').trigger('click')

    expect(mocks.handleScroll).toHaveBeenCalledTimes(1)
    expect(mocks.handleResize).toHaveBeenCalledTimes(1)
    expect(wrapper.emitted('resize')).toHaveLength(1)

    vm.scrollToItem(5)
    vm.scrollToPosition(180)

    expect(mocks.scrollToItem).toHaveBeenCalledWith(5)
    expect(mocks.scrollToPosition).toHaveBeenCalledWith(180)
    expect(vm.getScroll()).toEqual({ start: 0, end: 80 })
  })

  it('passes options to useRecycleScroller', () => {
    mount(RecycleScroller, {
      props: {
        items: [{ id: 'a' }],
        direction: 'horizontal',
        keyField: 'id',
        listTag: 'ul',
        itemTag: 'li',
        itemSize: 30,
        buffer: 150,
      },
      global: {
        stubs: {
          ResizeObserver: ResizeObserverStub,
        },
      },
    })

    const [optionsArg] = mocks.useRecycleScroller.mock.calls[0]
    expect(optionsArg.direction).toBe('horizontal')
    expect(optionsArg.keyField).toBe('id')
    expect(optionsArg.listTag).toBe('ul')
    expect(optionsArg.itemTag).toBe('li')
    expect(optionsArg.itemSize).toBe(30)
    expect(optionsArg.buffer).toBe(150)
  })
})


================================================
FILE: packages/vue-virtual-scroller/src/components/RecycleScroller.vue
================================================
<script setup lang="ts">
import type { ScrollDirection } from '../types'
import { ref } from 'vue'
import { useRecycleScroller } from '../composables/useRecycleScroller'
import { ObserveVisibility } from '../directives/observeVisibility'
import ItemView from './ItemView.vue'
import ResizeObserver from './ResizeObserver.vue'

const props = withDefaults(defineProps<{
  items: unknown[]
  keyField?: string
  direction?: ScrollDirection
  listTag?: string
  itemTag?: string
  itemSize?: number | null
  gridItems?: number
  itemSecondarySize?: number
  minItemSize?: number | string | null
  sizeField?: string
  typeField?: string
  buffer?: number
  pageMode?: boolean
  prerender?: number
  emitUpdate?: boolean
  disableTransform?: boolean
  updateInterval?: number
  skipHover?: boolean
  listClass?: string | Record<string, boolean> | Array<string | Record<string, boolean>>
  itemClass?: string | Record<string, boolean> | Array<string | Record<string, boolean>>
}>(), {
  keyField: 'id',
  direction: 'vertical',
  listTag: 'div',
  itemTag: 'div',
  itemSize: null,
  gridItems: undefined,
  itemSecondarySize: undefined,
  minItemSize: null,
  sizeField: 'size',
  typeField: 'type',
  buffer: 200,
  pageMode: false,
  prerender: 0,
  emitUpdate: false,
  disableTransform: false,
  updateInterval: 0,
  skipHover: false,
  listClass: '',
  itemClass: '',
})

const emit = defineEmits<{
  'resize': []
  'visible': []
  'hidden': []
  'update': [startIndex: number, endIndex: number, visibleStartIndex: number, visibleEndIndex: number]
  'scroll-start': []
  'scroll-end': []
}>()

const vObserveVisibility = ObserveVisibility

// Template refs
const el = ref<HTMLElement>()
const before = ref<HTMLElement>()
const after = ref<HTMLElement>()

// Hover state (UI-specific, not in composable)
const hoverKey = ref<string | number | null>(null)

const {
  pool,
  totalSize,
  ready,
  scrollToItem,
  scrollToPosition,
  getScroll,
  updateVisibleItems,
  handleScroll,
  handleResize,
  handleVisibilityChange,
} = useRecycleScroller(
  props,
  el,
  before,
  after,
  {
    onResize: () => emit('resize'),
    onVisible: () => emit('visible'),
    onHidden: () => emit('hidden'),
    onUpdate: (startIndex, endIndex, visibleStartIndex, visibleEndIndex) =>
      emit('update', startIndex, endIndex, visibleStartIndex, visibleEndIndex),
  },
)

// Expose public methods and el ref
defineExpose({
  el,
  scrollToItem,
  scrollToPosition,
  getScroll,
  updateVisibleItems,
})
</script>

<template>
  <div
    ref="el"
    v-observe-visibility="handleVisibilityChange"
    class="vue-recycle-scroller"
    :class="{
      ready,
      'page-mode': props.pageMode,
      [`direction-${props.direction}`]: true,
    }"
    @scroll.passive="handleScroll"
  >
    <div
      v-if="$slots.before"
      ref="before"
      class="vue-recycle-scroller__slot"
    >
      <slot
        name="before"
      />
    </div>

    <component
      :is="props.listTag"
      :style="{ [props.direction === 'vertical' ? 'minHeight' : 'minWidth']: `${totalSize}px` }"
      class="vue-recycle-scroller__item-wrapper"
      :class="props.listClass"
    >
      <ItemView
        v-for="view of pool"
        :key="view.nr.id"
        :view="view"
        :item-tag="props.itemTag"
        :style="ready
          ? [
            (props.disableTransform
              ? { [props.direction === 'vertical' ? 'top' : 'left']: `${view.position}px`, willChange: 'unset' }
              : { transform: `translate${props.direction === 'vertical' ? 'Y' : 'X'}(${view.position}px) translate${props.direction === 'vertical' ? 'X' : 'Y'}(${view.offset}px)` }),
            {
              width: props.gridItems ? `${props.direction === 'vertical' ? props.itemSecondarySize || props.itemSize : props.itemSize}px` : undefined,
              height: props.gridItems ? `${props.direction === 'horizontal' ? props.itemSecondarySize || props.itemSize : props.itemSize}px` : undefined,
              visibility: view.nr.used ? 'visible' : 'hidden',
            },
          ]
          : null"
        class="vue-recycle-scroller__item-view"
        :class="[
          props.itemClass,
          {
            hover: !props.skipHover && hoverKey === view.nr.key,
          },
        ]"
        v-on="props.skipHover ? {} : {
          mouseenter: () => { hoverKey = view.nr.key },
          mouseleave: () => { hoverKey = null },
        }"
      >
        <template #default="slotProps">
          <slot v-bind="slotProps" />
        </template>
      </ItemView>

      <slot
        v-if="props.items.length === 0"
        name="empty"
      />
    </component>

    <div
      v-if="$slots.after"
      ref="after"
      class="vue-recycle-scroller__slot"
    >
      <slot
        name="after"
      />
    </div>

    <ResizeObserver @notify="handleResize" />
  </div>
</template>

<style>
.vue-recycle-scroller {
  position: relative;
}

.vue-recycle-scroller.direction-vertical:not(.page-mode) {
  overflow-y: auto;
}

.vue-recycle-scroller.direction-horizontal:not(.page-mode) {
  overflow-x: auto;
}

.vue-recycle-scroller.direction-horizontal {
  display: flex;
}

.vue-recycle-scroller__slot {
  flex: auto 0 0;
}

.vue-recycle-scroller__item-wrapper {
  flex: 1;
  box-sizing: border-box;
  overflow: hidden;
  position: relative;
}

.vue-recycle-scroller.ready .vue-recycle-scroller__item-view {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
}

.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper {
  width: 100%;
}

.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper {
  height: 100%;
}

.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view {
  width: 100%;
}

.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view {
  height: 100%;
}
</style>


================================================
FILE: packages/vue-virtual-scroller/src/components/ResizeObserver.vue
================================================
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'

const emit = defineEmits<{
  notify: []
}>()

const el = ref<HTMLElement>()

let observer: ResizeObserver | null = null
let fallbackListener: (() => void) | null = null

function notify() {
  emit('notify')
}

onMounted(() => {
  const target = el.value?.parentElement
  if (!target)
    return

  if (typeof ResizeObserver !== 'undefined') {
    observer = new ResizeObserver(() => {
      notify()
    })
    observer.observe(target)
    return
  }

  fallbackListener = () => notify()
  window.addEventListener('resize', fallbackListener)
})

onBeforeUnmount(() => {
  if (observer) {
    observer.disconnect()
    observer = null
  }
  if (fallbackListener) {
    window.removeEventListener('resize', fallbackListener)
    fallbackListener = null
  }
})
</script>

<template>
  <div
    ref="el"
    class="vue-recycle-scroller__resize-observer"
    aria-hidden="true"
  />
</template>

<style scoped>
.vue-recycle-scroller__resize-observer {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  z-index: -1;
}
</style>


================================================
FILE: packages/vue-virtual-scroller/src/composables/useDynamicScroller.spec.ts
================================================
import type { ScrollDirection } from '../types'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent, nextTick, reactive, ref } from 'vue'
import { useDynamicScroller } from './useDynamicScroller'

function mountHarness(initialItems: unknown[]) {
  const scrollToItemSpy = vi.fn()
  const onResize = vi.fn()
  const onVisible = vi.fn()
  const options = reactive({
    items: initialItems,
    keyField: 'id',
    direction: 'vertical' as ScrollDirection,
    minItemSize: 20,
  })
  const scrollerRef = ref({
    scrollToItem: scrollToItemSpy,
  })
  const el = ref(document.createElement('div'))

  const Harness = defineComponent({
    setup() {
      const state = useDynamicScroller(options, scrollerRef, el, {
        onResize,
        onVisible,
      })
      return {
        ...state,
        options,
        el,
      }
    },
    template: '<div />',
  })

  const wrapper = mount(Harness)
  return {
    wrapper,
    vm: wrapper.vm as any,
    options,
    el,
    scrollToItemSpy,
    onResize,
    onVisible,
  }
}

describe('useDynamicScroller', () => {
  it('builds itemsWithSize and exposes item size helpers', async () => {
    const items = [
      { id: 'a', label: 'Alpha' },
      { id: 'b', label: 'Beta' },
    ]
    const { vm, options } = mountHarness(items)

    expect(vm.itemsWithSize).toHaveLength(2)
    expect(vm.itemsWithSize[0].id).toBe('a')
    expect(vm.itemsWithSize[0].size).toBe(0)
    expect(vm.itemsWithSize[1].id).toBe('b')

    vm.vscrollData.sizes.a = 42
    await nextTick()

    expect(vm.getItemSize(options.items[0])).toBe(42)
  })

  it('handles simple-array mode and callback forwarding', () => {
    const { vm, onResize, onVisible } = mountHarness(['one', 'two'])

    expect(vm.simpleArray).toBe(true)
    expect(vm.itemsWithSize[1].id).toBe(1)

    vm.vscrollData.sizes[1] = 30
    expect(vm.getItemSize('two')).toBe(30)

    vm.onScrollerResize()
    vm.onScrollerVisible()

    expect(onResize).toHaveBeenCalledTimes(1)
    expect(onVisible).toHaveBeenCalledTimes(1)
  })

  it('forwards scrollToItem and clears sizes on direction change', async () => {
    const { vm, options, scrollToItemSpy } = mountHarness([
      { id: 'a' },
      { id: 'b' },
    ])

    vm.scrollToItem(4)
    expect(scrollToItemSpy).toHaveBeenCalledWith(4)

    vm.vscrollData.sizes.a = 10
    vm.vscrollData.sizes.b = 15
    expect(Object.keys(vm.vscrollData.sizes)).toHaveLength(2)

    options.direction = 'horizontal'
    await nextTick()

    expect(Object.keys(vm.vscrollData.sizes)).toHaveLength(0)
  })

  it('returns early on scrollToBottom when no scroller element is available', () => {
    const { vm, el } = mountHarness([{ id: 'a' }])
    el.value = undefined as any

    expect(() => vm.scrollToBottom()).not.toThrow()
  })
})


================================================
FILE: packages/vue-virtual-scroller/src/composables/useDynamicScroller.ts
================================================
import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from 'vue'
import type { ItemWithSize, ScrollDirection, VScrollData } from '../types'
import mitt from 'mitt'
import { computed, nextTick, onActivated, onDeactivated, onUnmounted, provide, reactive, toValue, watch } from 'vue'

export interface UseDynamicScrollerOptions {
  items: unknown[]
  keyField: string
  direction: ScrollDirection
  minItemSize: number | string
}

export interface UseDynamicScrollerReturn {
  vscrollData: VScrollData
  itemsWithSize: ComputedRef<ItemWithSize[]>
  simpleArray: ComputedRef<boolean>
  resizeObserver: ResizeObserver | undefined
  forceUpdate: (clear?: boolean) => void
  scrollToItem: (index: number) => void
  getItemSize: (item: unknown, index?: number) => number
  scrollToBottom: () => void
  onScrollerResize: () => void
  onScrollerVisible: () => void
}

export function useDynamicScroller(
  options: MaybeRefOrGetter<UseDynamicScrollerOptions>,
  scrollerRef: MaybeRef<{ scrollToItem: (index: number) => void } | undefined>,
  el: MaybeRef<HTMLElement | undefined>,
  callbacks?: {
    onResize?: () => void
    onVisible?: () => void
  },
): UseDynamicScrollerReturn {
  // Internal state (non-reactive)
  let _undefinedSizes = 0
  let _undefinedMap: Record<string | number, boolean | undefined> = {}
  const _events = mitt()
  let _scrollingToBottom = false
  let _resizeObserver: ResizeObserver | undefined

  // Reactive state
  const vscrollData = reactive<VScrollData>({
    active: true,
    sizes: {},
    keyField: toValue(options).keyField,
    simpleArray: false,
  })

  // ResizeObserver setup
  if (typeof ResizeObserver !== 'undefined') {
    _resizeObserver = new ResizeObserver((entries) => {
      requestAnimationFrame(() => {
        if (!Array.isArray(entries)) {
          return
        }
        for (const entry of entries) {
          if (entry.target && (entry.target as any).$_vs_onResize) {
            let width: number, height: number
            if (entry.borderBoxSize) {
              const resizeObserverSize = entry.borderBoxSize[0]
              width = resizeObserverSize.inlineSize
              height = resizeObserverSize.blockSize
            }
            else {
              width = entry.contentRect.width
              height = entry.contentRect.height
            }
            ;(entry.target as any).$_vs_onResize((entry.target as any).$_vs_id, width, height)
          }
        }
      })
    })
  }

  // Provide
  provide('vscrollData', vscrollData)
  provide('vscrollParent', {
    get $_undefinedSizes() { return _undefinedSizes },
    set $_undefinedSizes(v: number) { _undefinedSizes = v },
    get $_undefinedMap() { return _undefinedMap },
    set $_undefinedMap(v: Record<string | number, boolean | undefined>) { _undefinedMap = v },
    $_events: _events,
    direction: computed(() => toValue(options).direction),
  })
  provide('vscrollResizeObserver', _resizeObserver)

  // Computed
  const simpleArray = computed(() => {
    const opts = toValue(options)
    return opts.items.length > 0 && typeof opts.items[0] !== 'object'
  })

  const itemsWithSize = computed<ItemWithSize[]>(() => {
    const result: ItemWithSize[] = []
    const opts = toValue(options)
    const { items, keyField } = opts
    const simple = simpleArray.value
    const sizes = vscrollData.sizes
    const l = items.length
    for (let i = 0; i < l; i++) {
      const item = items[i]
      const id = simple ? i : (item as any)[keyField]
      let size: number | undefined = sizes[id]
      if (typeof size === 'undefined' && !_undefinedMap[id]) {
        size = 0
      }
      result.push({
        item,
        id,
        size,
      })
    }
    return result
  })

  // Methods
  function onScrollerResize() {
    if (toValue(scrollerRef)) {
      forceUpdate()
    }
    callbacks?.onResize?.()
  }

  function onScrollerVisible() {
    _events.emit('vscroll:update', { force: false })
    callbacks?.onVisible?.()
  }

  function forceUpdate(clear = false) {
    if (clear || simpleArray.value) {
      vscrollData.sizes = {}
    }
    _events.emit('vscroll:update', { force: true })
  }

  function scrollToItem(index: number) {
    const scroller = toValue(scrollerRef)
    if (scroller)
      scroller.scrollToItem(index)
  }

  function getItemSize(item: unknown, index?: number): number {
    const opts = toValue(options)
    const id = simpleArray.value ? (index ?? opts.items.indexOf(item)) : (item as any)[opts.keyField]
    return vscrollData.sizes[id] || 0
  }

  function scrollToBottom() {
    const elValue = toValue(el)
    if (!elValue)
      return
    if (_scrollingToBottom)
      return
    _scrollingToBottom = true
    // Item is inserted to the DOM
    nextTick(() => {
      elValue.scrollTop = elValue.scrollHeight + 5000
      // Item sizes are computed
      const cb = () => {
        elValue.scrollTop = elValue.scrollHeight + 5000
        requestAnimationFrame(() => {
          elValue.scrollTop = elValue.scrollHeight + 5000
          if (_undefinedSizes === 0) {
            _scrollingToBottom = false
          }
          else {
            requestAnimationFrame(cb)
          }
        })
      }
      requestAnimationFrame(cb)
    }
Download .txt
gitextract_9c2q8070/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   ├── config.yml
│   │   └── feature-request.yml
│   └── workflows/
│       ├── continuous-publish.yml
│       ├── pr-title.yml
│       ├── release-notes.yml
│       └── test.yml
├── .gitignore
├── .node-version
├── CHANGELOG.md
├── README.md
├── SKILLS-GENERATION.md
├── docs/
│   ├── .vitepress/
│   │   ├── components/
│   │   │   └── demos/
│   │   │       ├── ChatStreamDocDemo.vue
│   │   │       ├── DemoShell.vue
│   │   │       ├── DynamicScrollerDocDemo.vue
│   │   │       ├── GridDocDemo.vue
│   │   │       ├── HorizontalDocDemo.vue
│   │   │       ├── RecycleScrollerDocDemo.vue
│   │   │       ├── SimpleListDocDemo.vue
│   │   │       ├── TestChatDocDemo.vue
│   │   │       └── demo-data.ts
│   │   ├── config.mts
│   │   └── theme/
│   │       ├── index.ts
│   │       └── style.css
│   ├── demos/
│   │   ├── chat.md
│   │   ├── dynamic-scroller.md
│   │   ├── grid.md
│   │   ├── horizontal.md
│   │   ├── index.md
│   │   ├── recycle-scroller.md
│   │   ├── simple-list.md
│   │   └── test-chat.md
│   ├── guide/
│   │   ├── ai-skills.md
│   │   ├── dynamic-scroller-item.md
│   │   ├── dynamic-scroller.md
│   │   ├── id-state.md
│   │   ├── index.md
│   │   ├── recycle-scroller.md
│   │   └── use-recycle-scroller.md
│   └── index.md
├── eslint.config.mjs
├── netlify.toml
├── package.json
├── packages/
│   ├── demo/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── index.html
│   │   ├── src/
│   │   │   ├── App.vue
│   │   │   ├── components/
│   │   │   │   ├── ChatDemo.vue
│   │   │   │   ├── DynamicScrollerDemo.vue
│   │   │   │   ├── GridDemo.vue
│   │   │   │   ├── Home.vue
│   │   │   │   ├── HorizontalDemo.vue
│   │   │   │   ├── Person.vue
│   │   │   │   ├── RecycleScrollerDemo.vue
│   │   │   │   ├── SimpleList.vue
│   │   │   │   └── TestChat.vue
│   │   │   ├── data.js
│   │   │   ├── main.js
│   │   │   └── router.js
│   │   └── vite.config.js
│   └── vue-virtual-scroller/
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       ├── skills/
│       │   └── vue-virtual-scroller/
│       │       ├── SKILL.md
│       │       └── references/
│       │           ├── dynamic-scroller-item.md
│       │           ├── dynamic-scroller.md
│       │           ├── index.md
│       │           ├── installation-and-setup.md
│       │           ├── recycle-scroller.md
│       │           └── use-recycle-scroller.md
│       ├── src/
│       │   ├── components/
│       │   │   ├── DynamicScroller.spec.ts
│       │   │   ├── DynamicScroller.vue
│       │   │   ├── DynamicScrollerItem.spec.ts
│       │   │   ├── DynamicScrollerItem.vue
│       │   │   ├── ItemView.spec.ts
│       │   │   ├── ItemView.vue
│       │   │   ├── RecycleScroller.spec.ts
│       │   │   ├── RecycleScroller.vue
│       │   │   └── ResizeObserver.vue
│       │   ├── composables/
│       │   │   ├── useDynamicScroller.spec.ts
│       │   │   ├── useDynamicScroller.ts
│       │   │   ├── useDynamicScrollerItem.ts
│       │   │   ├── useIdState.spec.ts
│       │   │   ├── useIdState.ts
│       │   │   ├── useRecycleScroller.spec.ts
│       │   │   └── useRecycleScroller.ts
│       │   ├── config.ts
│       │   ├── directives/
│       │   │   └── observeVisibility.ts
│       │   ├── index.spec.ts
│       │   ├── index.ts
│       │   ├── scrollparent.spec.ts
│       │   ├── scrollparent.ts
│       │   ├── shims-vue.d.ts
│       │   ├── types.ts
│       │   ├── utils.spec.ts
│       │   └── utils.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.json
│       └── vite.config.ts
└── pnpm-workspace.yaml
Download .txt
SYMBOL INDEX (75 symbols across 18 files)

FILE: docs/.vitepress/components/demos/demo-data.ts
  type PersonRow (line 1) | interface PersonRow {
  type Person (line 9) | interface Person {
  type MessageRow (line 15) | interface MessageRow {
  constant FIRST_NAMES (line 24) | const FIRST_NAMES = [
  constant LAST_NAMES (line 47) | const LAST_NAMES = [
  constant WORDS (line 70) | const WORDS = [
  function createRng (line 103) | function createRng(seed = 1) {
  function pick (line 111) | function pick<T>(rng: () => number, values: T[]) {
  function capitalize (line 115) | function capitalize(text: string) {
  function sentence (line 119) | function sentence(rng: () => number, minWords = 8, maxWords = 20) {
  function initialsFromName (line 128) | function initialsFromName(name: string) {
  function hueFromText (line 133) | function hueFromText(text: string, salt = 0) {
  function avatarStyle (line 141) | function avatarStyle(hue: number) {
  function createPeopleRows (line 147) | function createPeopleRows(count: number, withLetters = true, seed = 42) {
  function createMessages (line 198) | function createMessages(count: number, seed = 99) {
  function mutateMessage (line 218) | function mutateMessage(row: MessageRow, seed = 1234) {
  function createSimpleStrings (line 223) | function createSimpleStrings(count: number, seed = 7) {
  constant GRADIENTS (line 232) | const GRADIENTS = [
  function gradientAt (line 241) | function gradientAt(index: number) {

FILE: packages/demo/src/data.js
  function generateItem (line 5) | function generateItem() {
  function getData (line 12) | function getData(count, letters) {
  function addItem (line 55) | function addItem(list) {
  function generateMessage (line 65) | function generateMessage() {

FILE: packages/vue-virtual-scroller/src/components/DynamicScroller.spec.ts
  method setup (line 37) | setup(props, { slots, emit, expose }) {
  function mountDynamicScroller (line 69) | function mountDynamicScroller(props: any, slots?: any) {

FILE: packages/vue-virtual-scroller/src/components/RecycleScroller.spec.ts
  method setup (line 28) | setup(_props, { emit }) {

FILE: packages/vue-virtual-scroller/src/composables/useDynamicScroller.spec.ts
  function mountHarness (line 7) | function mountHarness(initialItems: unknown[]) {

FILE: packages/vue-virtual-scroller/src/composables/useDynamicScroller.ts
  type UseDynamicScrollerOptions (line 6) | interface UseDynamicScrollerOptions {
  type UseDynamicScrollerReturn (line 13) | interface UseDynamicScrollerReturn {
  function useDynamicScroller (line 26) | function useDynamicScroller(

FILE: packages/vue-virtual-scroller/src/composables/useDynamicScrollerItem.ts
  type UseDynamicScrollerItemOptions (line 5) | interface UseDynamicScrollerItemOptions {
  type UseDynamicScrollerItemReturn (line 14) | interface UseDynamicScrollerItemReturn {
  function useDynamicScrollerItem (line 21) | function useDynamicScrollerItem(

FILE: packages/vue-virtual-scroller/src/composables/useIdState.spec.ts
  method idState (line 19) | idState() {
  method setup (line 25) | setup() {
  method idState (line 63) | idState() {
  method setup (line 68) | setup() {
  method setup (line 103) | setup() {

FILE: packages/vue-virtual-scroller/src/composables/useIdState.ts
  type IdPropFn (line 3) | type IdPropFn = (vm: any) => string | number
  function useIdState (line 5) | function useIdState({

FILE: packages/vue-virtual-scroller/src/composables/useRecycleScroller.spec.ts
  function createView (line 7) | function createView(index: number, used = true): View {
  function mountHarness (line 22) | function mountHarness() {

FILE: packages/vue-virtual-scroller/src/composables/useRecycleScroller.ts
  type UseRecycleScrollerOptions (line 8) | interface UseRecycleScrollerOptions {
  type UseRecycleScrollerReturn (line 25) | interface UseRecycleScrollerReturn {
  function useRecycleScroller (line 43) | function useRecycleScroller(

FILE: packages/vue-virtual-scroller/src/config.ts
  type VirtualScrollerConfig (line 1) | interface VirtualScrollerConfig {

FILE: packages/vue-virtual-scroller/src/directives/observeVisibility.ts
  type ObserveVisibilityCallback (line 3) | type ObserveVisibilityCallback = (isVisible: boolean, entry: Intersectio...
  type ObserveVisibilityValue (line 5) | interface ObserveVisibilityValue {
  type ObserveVisibilityState (line 10) | interface ObserveVisibilityState {
  function normalizeValue (line 19) | function normalizeValue(value: ObserveVisibilityCallback | ObserveVisibi...
  function updateState (line 37) | function updateState(el: Element, binding: DirectiveBinding<ObserveVisib...
  function teardown (line 63) | function teardown(el: Element) {
  method mounted (line 72) | mounted(el, binding) {
  method updated (line 75) | updated(el, binding) {
  method unmounted (line 80) | unmounted(el) {

FILE: packages/vue-virtual-scroller/src/index.ts
  function registerComponents (line 25) | function registerComponents(app: App, prefix: string) {
  method install (line 38) | install(app: App, options?: PluginOptions) {

FILE: packages/vue-virtual-scroller/src/scrollparent.ts
  function parents (line 5) | function parents(node: Node, ps: Node[]): Node[] {
  function style (line 13) | function style(node: Element, prop: string): string {
  function overflow (line 17) | function overflow(node: Element): string {
  function scroll (line 21) | function scroll(node: Element): boolean {
  function getScrollParent (line 25) | function getScrollParent(node: Node): Element | undefined {

FILE: packages/vue-virtual-scroller/src/shims-vue.d.ts
  type EventHandler (line 9) | type EventHandler = (event?: unknown) => void
  type Emitter (line 11) | interface Emitter {

FILE: packages/vue-virtual-scroller/src/types.ts
  type ScrollDirection (line 1) | type ScrollDirection = 'vertical' | 'horizontal'
  type ScrollState (line 3) | interface ScrollState {
  type ViewNonReactive (line 8) | interface ViewNonReactive {
  type View (line 16) | interface View {
  type SizeEntry (line 23) | interface SizeEntry {
  type Sizes (line 28) | interface Sizes {
  type VScrollData (line 32) | interface VScrollData {
  type ItemWithSize (line 39) | interface ItemWithSize {
  type PluginOptions (line 45) | interface PluginOptions {

FILE: packages/vue-virtual-scroller/src/utils.ts
  function supportsPassive (line 3) | function supportsPassive(): boolean {
  method get (line 11) | get() {
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (251K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 62,
    "preview": "# These are supported funding model platforms\n\ngithub: Akryum\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "chars": 2916,
    "preview": "name: 🐞 Bug report\ndescription: Report an issue with vue-virtual-scroller\nlabels: [to triage]\nbody:\n  - type: markdown\n "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 230,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions & Discussions\n    url: https://github.com/Akryum/vue-virt"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "chars": 2031,
    "preview": "name: 🚀 New feature proposal\ndescription: Propose a new feature to be added to vue-virtual-scroller\nlabels: ['enhancemen"
  },
  {
    "path": ".github/workflows/continuous-publish.yml",
    "chars": 438,
    "preview": "name: Publish Any Commit\non: [push, pull_request]\n\njobs:\n  auto-publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - "
  },
  {
    "path": ".github/workflows/pr-title.yml",
    "chars": 411,
    "preview": "name: Check PR title\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\njobs:\n  c"
  },
  {
    "path": ".github/workflows/release-notes.yml",
    "chars": 596,
    "preview": "name: Create release\n\non:\n  push:\n    tags:\n      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\njobs:\n  bui"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 409,
    "preview": "name: Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkou"
  },
  {
    "path": ".gitignore",
    "chars": 91,
    "preview": "node_modules/\n.temp/\n.cache/\ndist/\n.eslintcache\ndocs/.vitepress/cache\ndocs/.vitepress/dist\n"
  },
  {
    "path": ".node-version",
    "chars": 7,
    "preview": "25.8.0\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 13073,
    "preview": "## v2.0.0-beta.10\n\n[compare changes](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.9...v2.0.0-beta."
  },
  {
    "path": "README.md",
    "chars": 1009,
    "preview": "# vue-virtual-scroller\n\n[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg) ![npm](https://img.shields.io/npm"
  },
  {
    "path": "SKILLS-GENERATION.md",
    "chars": 11269,
    "preview": "# Skills Generation (vue-virtual-scroller)\n\nThis file is the canonical process for generating and updating package skill"
  },
  {
    "path": "docs/.vitepress/components/demos/ChatStreamDocDemo.vue",
    "chars": 3622,
    "preview": "<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref } from 'vue'\nimport DynamicScroller from '../../../../p"
  },
  {
    "path": "docs/.vitepress/components/demos/DemoShell.vue",
    "chars": 506,
    "preview": "<script setup lang=\"ts\">\ndefineProps<{\n  title: string\n  description: string\n}>()\n</script>\n\n<template>\n  <section class"
  },
  {
    "path": "docs/.vitepress/components/demos/DynamicScrollerDocDemo.vue",
    "chars": 3154,
    "preview": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtu"
  },
  {
    "path": "docs/.vitepress/components/demos/GridDocDemo.vue",
    "chars": 2219,
    "preview": "<script setup lang=\"ts\">\nimport type { Person } from './demo-data'\nimport { computed, ref } from 'vue'\nimport RecycleScr"
  },
  {
    "path": "docs/.vitepress/components/demos/HorizontalDocDemo.vue",
    "chars": 2348,
    "preview": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtu"
  },
  {
    "path": "docs/.vitepress/components/demos/RecycleScrollerDocDemo.vue",
    "chars": 3920,
    "preview": "<script setup lang=\"ts\">\nimport type { Person, PersonRow } from './demo-data'\nimport { computed, onMounted, ref, watch }"
  },
  {
    "path": "docs/.vitepress/components/demos/SimpleListDocDemo.vue",
    "chars": 2931,
    "preview": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtu"
  },
  {
    "path": "docs/.vitepress/components/demos/TestChatDocDemo.vue",
    "chars": 2299,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtual-scrolle"
  },
  {
    "path": "docs/.vitepress/components/demos/demo-data.ts",
    "chars": 5029,
    "preview": "export interface PersonRow {\n  id: number\n  index: number\n  type: 'person' | 'letter'\n  value: string | Person\n  height:"
  },
  {
    "path": "docs/.vitepress/config.mts",
    "chars": 2320,
    "preview": "import { defineConfig } from 'vitepress'\n\nexport default defineConfig({\n  title: 'Vue Virtual Scroller',\n  description: "
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "chars": 109,
    "preview": "import DefaultTheme from 'vitepress/theme'\nimport './style.css'\n\nexport default {\n  extends: DefaultTheme,\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/style.css",
    "chars": 4592,
    "preview": ":root {\n  --demo-bg: linear-gradient(145deg, #f4f8f2 0%, #edf7f6 48%, #f8f2e9 100%);\n  --demo-surface: rgba(255, 255, 25"
  },
  {
    "path": "docs/demos/chat.md",
    "chars": 2432,
    "preview": "<script setup>\nimport ChatStreamDocDemo from '../.vitepress/components/demos/ChatStreamDocDemo.vue'\n</script>\n\n# Chat St"
  },
  {
    "path": "docs/demos/dynamic-scroller.md",
    "chars": 1751,
    "preview": "<script setup>\nimport DynamicScrollerDocDemo from '../.vitepress/components/demos/DynamicScrollerDocDemo.vue'\n</script>\n"
  },
  {
    "path": "docs/demos/grid.md",
    "chars": 1580,
    "preview": "<script setup>\nimport GridDocDemo from '../.vitepress/components/demos/GridDocDemo.vue'\n</script>\n\n# Grid Demo\n\nUse this"
  },
  {
    "path": "docs/demos/horizontal.md",
    "chars": 1525,
    "preview": "<script setup>\nimport HorizontalDocDemo from '../.vitepress/components/demos/HorizontalDocDemo.vue'\n</script>\n\n# Horizon"
  },
  {
    "path": "docs/demos/index.md",
    "chars": 666,
    "preview": "# Demos\n\nInteractive demos for common real-world use cases.\n\n## Pick a demo\n\n- [RecycleScroller demo](./recycle-scroller"
  },
  {
    "path": "docs/demos/recycle-scroller.md",
    "chars": 1781,
    "preview": "<script setup>\nimport RecycleScrollerDocDemo from '../.vitepress/components/demos/RecycleScrollerDocDemo.vue'\n</script>\n"
  },
  {
    "path": "docs/demos/simple-list.md",
    "chars": 1557,
    "preview": "<script setup>\nimport SimpleListDocDemo from '../.vitepress/components/demos/SimpleListDocDemo.vue'\n</script>\n\n# Simple "
  },
  {
    "path": "docs/demos/test-chat.md",
    "chars": 1476,
    "preview": "<script setup>\nimport TestChatDocDemo from '../.vitepress/components/demos/TestChatDocDemo.vue'\n</script>\n\n# Test Chat D"
  },
  {
    "path": "docs/guide/ai-skills.md",
    "chars": 1364,
    "preview": "# AI & Skills\n\nIf you use AI coding agents, `vue-virtual-scroller` ships a package skill that can be discovered from the"
  },
  {
    "path": "docs/guide/dynamic-scroller-item.md",
    "chars": 1047,
    "preview": "# DynamicScrollerItem\n\nThe component that should wrap all the items in a [DynamicScroller](./dynamic-scroller) to handle"
  },
  {
    "path": "docs/guide/dynamic-scroller.md",
    "chars": 1761,
    "preview": "# DynamicScroller\n\nThis works just like the [RecycleScroller](./recycle-scroller), but it can render items with unknown "
  },
  {
    "path": "docs/guide/id-state.md",
    "chars": 1504,
    "preview": "# IdState\n\nThis is a convenience mixin that can replace `data` in components being rendered in a [RecycleScroller](./rec"
  },
  {
    "path": "docs/guide/index.md",
    "chars": 2360,
    "preview": "# Getting Started\n\n<div class=\"badges\">\n\n[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg)](https://npmx.de"
  },
  {
    "path": "docs/guide/recycle-scroller.md",
    "chars": 8985,
    "preview": "# RecycleScroller\n\nRecycleScroller is a virtual scroller that only renders the visible items. As the user scrolls, Recyc"
  },
  {
    "path": "docs/guide/use-recycle-scroller.md",
    "chars": 3857,
    "preview": "# useRecycleScroller (Headless)\n\n`useRecycleScroller` is the low-level composable behind `RecycleScroller`.\n\nUse it when"
  },
  {
    "path": "docs/index.md",
    "chars": 839,
    "preview": "---\nlayout: home\n\nhero:\n  name: Vue Virtual Scroller\n  tagline: Blazing fast scrolling of any amount of data\n  actions:\n"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 78,
    "preview": "// @ts-check\nimport antfu from '@antfu/eslint-config'\n\nexport default antfu()\n"
  },
  {
    "path": "netlify.toml",
    "chars": 153,
    "preview": "[build.environment]\nNODE_VERSION = \"16\"\nNPM_FLAGS = \"--version\" # prevent Netlify npm install\n\n[[redirects]]\nfrom = \"/*\""
  },
  {
    "path": "package.json",
    "chars": 820,
    "preview": "{\n  \"name\": \"vue-virtual-scroller-monorepo\",\n  \"version\": \"2.0.0-beta.10\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@"
  },
  {
    "path": "packages/demo/.gitignore",
    "chars": 275,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n/tests/e2e/videos/\n/tests/e2e/screenshots/\n\n\n# local env files\n.env.local\n.env.*.local\n\n# "
  },
  {
    "path": "packages/demo/README.md",
    "chars": 242,
    "preview": "# vue-virtual-scroller-demos\n\n> Demos for vue-virtual-scroller\n\n## Build Setup\n\n``` bash\n# install dependencies\nyarn ins"
  },
  {
    "path": "packages/demo/index.html",
    "chars": 269,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>vue-virtual-scroller</title>\n    <link r"
  },
  {
    "path": "packages/demo/package.json",
    "chars": 609,
    "preview": "{\n  \"name\": \"demo\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"Demos for vue-virtual-scroller\",\n  \"autho"
  },
  {
    "path": "packages/demo/public/index.html",
    "chars": 599,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
  },
  {
    "path": "packages/demo/src/App.vue",
    "chars": 2676,
    "preview": "<template>\n  <nav class=\"menu\">\n    <span class=\"package\">\n      <span class=\"package-name\">vue-virtual-scroller</span>\n"
  },
  {
    "path": "packages/demo/src/components/ChatDemo.vue",
    "chars": 3408,
    "preview": "<script>\nimport { generateMessage } from '../data'\n\nlet id = 0\n\nconst messages = []\nfor (let i = 0; i < 10000; i++) {\n  "
  },
  {
    "path": "packages/demo/src/components/DynamicScrollerDemo.vue",
    "chars": 3477,
    "preview": "<script>\nimport { generateMessage } from '../data'\n\nconst items = []\nfor (let i = 0; i < 10000; i++) {\n  items.push({\n  "
  },
  {
    "path": "packages/demo/src/components/GridDemo.vue",
    "chars": 1831,
    "preview": "<script>\nimport { getData } from '../data'\n\nexport default {\n  data() {\n    return {\n      list: [],\n      gridItems: 6,"
  },
  {
    "path": "packages/demo/src/components/Home.vue",
    "chars": 1558,
    "preview": "<template>\n  <div class=\"home\">\n    <h1>Virtual scrolling solutions</h1>\n\n    <section>\n      <router-link\n        :to=\""
  },
  {
    "path": "packages/demo/src/components/HorizontalDemo.vue",
    "chars": 2978,
    "preview": "<script>\nimport { generateMessage } from '../data'\n\nconst items = []\nfor (let i = 0; i < 10000; i++) {\n  items.push({\n  "
  },
  {
    "path": "packages/demo/src/components/Person.vue",
    "chars": 872,
    "preview": "<script>\nexport default {\n  props: ['item', 'index'],\n\n  methods: {\n    edit() {\n      // eslint-disable-next-line vue/n"
  },
  {
    "path": "packages/demo/src/components/RecycleScrollerDemo.vue",
    "chars": 5538,
    "preview": "<script>\nimport { addItem, getData } from '../data'\n\nimport Person from './Person.vue'\n\nexport default {\n  components: {"
  },
  {
    "path": "packages/demo/src/components/SimpleList.vue",
    "chars": 2508,
    "preview": "<script>\nimport { generateMessage } from '../data'\n\nconst items = []\nfor (let i = 0; i < 10000; i++) {\n  items.push(gene"
  },
  {
    "path": "packages/demo/src/components/TestChat.vue",
    "chars": 1702,
    "preview": "<script>\nimport { faker } from '@faker-js/faker'\n\nexport default {\n  name: 'TestChat',\n\n  data() {\n    return {\n      it"
  },
  {
    "path": "packages/demo/src/data.js",
    "chars": 1254,
    "preview": "import { faker } from '@faker-js/faker'\n\nlet uid = 0\n\nfunction generateItem() {\n  return {\n    name: faker.name.fullName"
  },
  {
    "path": "packages/demo/src/main.js",
    "chars": 290,
    "preview": "import { createApp } from 'vue'\n\nimport VirtualScroller from 'vue-virtual-scroller'\nimport App from './App.vue'\n\nimport "
  },
  {
    "path": "packages/demo/src/router.js",
    "chars": 1120,
    "preview": "import { createRouter, createWebHistory } from 'vue-router'\n\nimport ChatDemo from './components/ChatDemo.vue'\nimport Dyn"
  },
  {
    "path": "packages/demo/vite.config.js",
    "chars": 127,
    "preview": "import vue from '@vitejs/plugin-vue'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [vue("
  },
  {
    "path": "packages/vue-virtual-scroller/LICENSE",
    "chars": 1083,
    "preview": "MIT License\n\nCopyright (c) 2020 guillaume.b.chau@gmail.com\n\nPermission is hereby granted, free of charge, to any person "
  },
  {
    "path": "packages/vue-virtual-scroller/README.md",
    "chars": 1009,
    "preview": "# vue-virtual-scroller\n\n[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg) ![npm](https://img.shields.io/npm"
  },
  {
    "path": "packages/vue-virtual-scroller/package.json",
    "chars": 1702,
    "preview": "{\n  \"name\": \"vue-virtual-scroller\",\n  \"type\": \"module\",\n  \"version\": \"2.0.0-beta.10\",\n  \"description\": \"Smooth scrolling"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md",
    "chars": 6569,
    "preview": "---\nname: vue-virtual-scroller\ndescription: Use this skill for Vue 3 virtual scrolling with vue-virtual-scroller, includ"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md",
    "chars": 1512,
    "preview": "# DynamicScrollerItem\n\nScope: the per-item measurement wrapper used inside `DynamicScroller`.\n\n## Provenance\n\nGenerated "
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md",
    "chars": 2658,
    "preview": "# DynamicScroller\n\nScope: the component path for unknown-size items that must be measured as they render.\n\n## Provenance"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md",
    "chars": 1296,
    "preview": "# Vue Virtual Scroller References\n\nFocused reference map for the documented public surfaces covered by this skill.\n\n| To"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md",
    "chars": 1787,
    "preview": "# Installation And Setup\n\nScope: install the package correctly in a Vue 3 app and avoid the common setup mistakes that m"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md",
    "chars": 2543,
    "preview": "# RecycleScroller\n\nScope: the main component for virtualizing fixed-size lists, pre-sized variable lists, grids, and pag"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md",
    "chars": 2735,
    "preview": "# useRecycleScroller\n\nScope: the headless virtualization composable for building custom scroll UIs without the bundled c"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScroller.spec.ts",
    "chars": 4312,
    "preview": "import { mount } from '@vue/test-utils'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { defineCom"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScroller.vue",
    "chars": 2211,
    "preview": "<script setup lang=\"ts\">\nimport type { ItemWithSize, ScrollDirection } from '../types'\nimport { computed, ref } from 'vu"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScrollerItem.spec.ts",
    "chars": 2115,
    "preview": "import { mount } from '@vue/test-utils'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { computed "
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue",
    "chars": 773,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useDynamicScrollerItem } from '../composables/useDynamicScro"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/ItemView.spec.ts",
    "chars": 1463,
    "preview": "import type { View } from '../types'\nimport { mount } from '@vue/test-utils'\nimport { describe, expect, it } from 'vites"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/ItemView.vue",
    "chars": 414,
    "preview": "<!-- Avoid re-renders of slots -->\n\n<script setup lang=\"ts\">\nimport type { View } from '../types'\n\nconst props = defineP"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/RecycleScroller.spec.ts",
    "chars": 5903,
    "preview": "import { mount } from '@vue/test-utils'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { computed,"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/RecycleScroller.vue",
    "chars": 5868,
    "preview": "<script setup lang=\"ts\">\nimport type { ScrollDirection } from '../types'\nimport { ref } from 'vue'\nimport { useRecycleSc"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/ResizeObserver.vue",
    "chars": 1125,
    "preview": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\n\nconst emit = defineEmits<{\n  notify: []\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useDynamicScroller.spec.ts",
    "chars": 2840,
    "preview": "import type { ScrollDirection } from '../types'\nimport { mount } from '@vue/test-utils'\nimport { describe, expect, it, v"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useDynamicScroller.ts",
    "chars": 6726,
    "preview": "import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from 'vue'\nimport type { ItemWithSize, ScrollDirection, VScrollD"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useDynamicScrollerItem.ts",
    "chars": 6829,
    "preview": "import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from 'vue'\nimport type { VScrollData } from '../types'\nimport { "
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useIdState.spec.ts",
    "chars": 2642,
    "preview": "import { mount } from '@vue/test-utils'\nimport { describe, expect, it } from 'vitest'\nimport { defineComponent, nextTick"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useIdState.ts",
    "chars": 1582,
    "preview": "import { getCurrentInstance, nextTick, onBeforeUpdate, reactive, ref, watch } from 'vue'\n\ntype IdPropFn = (vm: any) => s"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useRecycleScroller.spec.ts",
    "chars": 2156,
    "preview": "import type { View } from '../types'\nimport { mount } from '@vue/test-utils'\nimport { describe, expect, it, vi } from 'v"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useRecycleScroller.ts",
    "chars": 19042,
    "preview": "import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue'\nimport type { ScrollDirection, ScrollState, Size"
  },
  {
    "path": "packages/vue-virtual-scroller/src/config.ts",
    "chars": 208,
    "preview": "export interface VirtualScrollerConfig {\n  itemsLimit: number\n  installComponents?: boolean\n  componentsPrefix?: string\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/directives/observeVisibility.ts",
    "chars": 2204,
    "preview": "import type { Directive, DirectiveBinding } from 'vue'\n\ntype ObserveVisibilityCallback = (isVisible: boolean, entry: Int"
  },
  {
    "path": "packages/vue-virtual-scroller/src/index.spec.ts",
    "chars": 2212,
    "preview": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport config from './config'\nimport plugin, { DynamicScrol"
  },
  {
    "path": "packages/vue-virtual-scroller/src/index.ts",
    "chars": 2003,
    "preview": "import type { App } from 'vue'\nimport type { PluginOptions } from './types'\nimport DynamicScroller from './components/Dy"
  },
  {
    "path": "packages/vue-virtual-scroller/src/scrollparent.spec.ts",
    "chars": 963,
    "preview": "import { describe, expect, it } from 'vitest'\nimport { getScrollParent } from './scrollparent'\n\ndescribe('getScrollParen"
  },
  {
    "path": "packages/vue-virtual-scroller/src/scrollparent.ts",
    "chars": 1020,
    "preview": "// Fork of https://github.com/olahol/scrollparent.js to be able to build with Rollup\n\nconst regex = /auto|scroll/\n\nfunct"
  },
  {
    "path": "packages/vue-virtual-scroller/src/shims-vue.d.ts",
    "chars": 499,
    "preview": "declare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n\n  const component: DefineComponent<object, object"
  },
  {
    "path": "packages/vue-virtual-scroller/src/types.ts",
    "chars": 797,
    "preview": "export type ScrollDirection = 'vertical' | 'horizontal'\n\nexport interface ScrollState {\n  start: number\n  end: number\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/utils.spec.ts",
    "chars": 226,
    "preview": "import { describe, expect, it } from 'vitest'\nimport { supportsPassive } from './utils'\n\ndescribe('supportsPassive', () "
  },
  {
    "path": "packages/vue-virtual-scroller/src/utils.ts",
    "chars": 381,
    "preview": "let _supportsPassive = false\n\nexport function supportsPassive(): boolean {\n  return _supportsPassive\n}\n\nif (typeof windo"
  },
  {
    "path": "packages/vue-virtual-scroller/tsconfig.build.json",
    "chars": 110,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\n    \"node_modules\",\n    \"dist\",\n    \"src/**/*.spec.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/tsconfig.json",
    "chars": 486,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"jsx\": \"preserve\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n  "
  },
  {
    "path": "packages/vue-virtual-scroller/vite.config.ts",
    "chars": 845,
    "preview": "import { readFileSync } from 'node:fs'\nimport process from 'node:process'\nimport vue from '@vitejs/plugin-vue'\nimport { "
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 108,
    "preview": "shellEmulator: true\n\ntrustPolicy: no-downgrade\n\npackages:\n  - packages/*\nonlyBuiltDependencies:\n  - esbuild\n"
  }
]

About this extraction

This page contains the full source code of the Akryum/vue-virtual-scroller GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (227.9 KB), approximately 64.0k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!