diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 099ba078..a14264cb 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -27,6 +27,7 @@ "@tailwindcss/vite": "^4.2.1", "@tanstack/svelte-query": "^6.1.0", "@tanstack/svelte-query-devtools": "^6.0.4", + "@tanstack/svelte-virtual": "^3.13.23", "@types/node": "^25", "@types/nprogress": "^0.2.3", "@types/qrcode": "^1.5.6", @@ -2834,6 +2835,34 @@ "svelte": "^5.25.0" } }, + "node_modules/@tanstack/svelte-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-virtual/-/svelte-virtual-3.13.23.tgz", + "integrity": "sha512-kUFdKtevYPhuv9bN2/NhE8zCr6pO3lP2a4xYwyzwqvNZim0EMHdq9nKMFgS4rMHE9Iacdj4+PwMYDfRZ45a/+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "svelte": "^3.48.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 2bbf46a6..62cf21d3 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -15,6 +15,7 @@ "test": "npm run test:unit -- --run", "test:unit": "vitest" }, + "dependencies": {}, "devDependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.2", @@ -35,6 +36,7 @@ "@tailwindcss/vite": "^4.2.1", "@tanstack/svelte-query": "^6.1.0", "@tanstack/svelte-query-devtools": "^6.0.4", + "@tanstack/svelte-virtual": "^3.13.23", "@types/node": "^25", "@types/nprogress": "^0.2.3", "@types/qrcode": "^1.5.6", diff --git a/src/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte b/src/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte index 51afd054..f87f0acb 100644 --- a/src/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte +++ b/src/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte @@ -2,6 +2,9 @@ import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui'; import { Scrollbar } from './index'; import { cn, type WithoutChild } from '$lib/utils.js'; + import { createVirtualizer, type VirtualizerOptions } from '@tanstack/svelte-virtual'; + import type { VirtualItem } from '@tanstack/virtual-core'; + import type { Snippet } from 'svelte'; let { ref = $bindable(null), @@ -10,6 +13,8 @@ orientation = 'vertical', scrollbarXClasses = '', scrollbarYClasses = '', + virtualOptions, + item, children, ...restProps }: WithoutChild & { @@ -17,7 +22,20 @@ scrollbarXClasses?: string | undefined; scrollbarYClasses?: string | undefined; viewportRef?: HTMLElement | null; + virtualOptions?: + | Omit, 'getScrollElement'> + | undefined; + item?: Snippet<[VirtualItem]> | undefined; } = $props(); + + const virtualizer = $derived( + virtualOptions && viewportRef + ? createVirtualizer({ ...virtualOptions, getScrollElement: () => viewportRef }) + : null + ); + + const virtualItems = $derived($virtualizer?.getVirtualItems() ?? []); + const totalSize = $derived($virtualizer?.getTotalSize() ?? 0); - {@render children?.()} + {#if virtualizer && item} +
+ {#each virtualItems as row (row.index)} +
+ {@render item(row)} +
+ {/each} +
+ {:else} + {@render children?.()} + {/if} {#if orientation === 'vertical' || orientation === 'both'}