diff --git a/src/shared/lib/utils/createVirtualizer/createVirtualizer.ts b/src/shared/lib/utils/createVirtualizer/createVirtualizer.ts
new file mode 100644
index 0000000..49abe4c
--- /dev/null
+++ b/src/shared/lib/utils/createVirtualizer/createVirtualizer.ts
@@ -0,0 +1,114 @@
+import {
+ createVirtualizer as coreCreateVirtualizer,
+ observeElementRect,
+} from '@tanstack/svelte-virtual';
+import type { VirtualItem as CoreVirtualItem } from '@tanstack/virtual-core';
+import { get } from 'svelte/store';
+
+export interface VirtualItem {
+ index: number;
+ start: number;
+ size: number;
+ end: number;
+ key: string | number;
+}
+
+export interface VirtualizerOptions {
+ /** Total number of items in the data array */
+ count: number;
+ /** Function to estimate the size of an item at a given index */
+ estimateSize: (index: number) => number;
+ /** Number of extra items to render outside viewport (default: 5) */
+ overscan?: number;
+ /** Function to get the key of an item at a given index (defaults to index) */
+ getItemKey?: (index: number) => string | number;
+ /** Optional margin in pixels for scroll calculations */
+ scrollMargin?: number;
+}
+
+/**
+ * Creates a reactive virtualizer using Svelte 5 runes and TanStack's core library.
+ *
+ * @example
+ * ```ts
+ * const virtualizer = createVirtualizer(() => ({
+ * count: items.length,
+ * estimateSize: () => 80,
+ * overscan: 5,
+ * }));
+ *
+ * // In template:
+ * //
+ * // {#each virtualizer.items as item}
+ * //
+ * // {items[item.index]}
+ * //
+ * // {/each}
+ * //
+ * ```
+ */
+export function createVirtualizer(
+ optionsGetter: () => VirtualizerOptions,
+) {
+ let element = $state(null);
+
+ const internalStore = coreCreateVirtualizer({
+ get count() {
+ return optionsGetter().count;
+ },
+ get estimateSize() {
+ return optionsGetter().estimateSize;
+ },
+ get overscan() {
+ return optionsGetter().overscan ?? 5;
+ },
+ get scrollMargin() {
+ return optionsGetter().scrollMargin;
+ },
+ get getItemKey() {
+ return optionsGetter().getItemKey ?? (i => i);
+ },
+ getScrollElement: () => element,
+ observeElementRect: observeElementRect,
+ });
+
+ const state = $derived(get(internalStore));
+
+ const virtualItems = $derived(
+ state.getVirtualItems().map((item: CoreVirtualItem): VirtualItem => ({
+ index: item.index,
+ start: item.start,
+ size: item.size,
+ end: item.end,
+ key: typeof item.key === 'bigint' ? Number(item.key) : item.key,
+ })),
+ );
+
+ return {
+ get items() {
+ return virtualItems;
+ },
+
+ get totalSize() {
+ return state.getTotalSize();
+ },
+
+ get scrollOffset() {
+ return state.scrollOffset ?? 0;
+ },
+
+ get scrollElement() {
+ return element;
+ },
+ set scrollElement(el) {
+ element = el;
+ },
+
+ scrollToIndex: (idx: number, opt?: { align?: 'start' | 'center' | 'end' | 'auto' }) =>
+ state.scrollToIndex(idx, opt),
+
+ scrollToOffset: (off: number) => state.scrollToOffset(off),
+
+ measureElement: (el: HTMLElement) => state.measureElement(el),
+ };
+}
diff --git a/src/shared/store/createVirtualizerStore.test.ts b/src/shared/store/createVirtualizerStore.test.ts
deleted file mode 100644
index 123bd44..0000000
--- a/src/shared/store/createVirtualizerStore.test.ts
+++ /dev/null
@@ -1,436 +0,0 @@
-/**
- * ============================================================================
- * VIRTUALIZER STORE - UNIT TESTS
- * ============================================================================
- *
- * Tests for createVirtualizerStore
- *
- * Note: These tests focus on the store API and behavior without requiring
- * a full DOM environment. Integration tests with actual DOM are in
- * component tests.
- */
-
-import {
- describe,
- expect,
- it,
- vi,
-} from 'vitest';
-import { createVirtualizerStore } from './createVirtualizerStore';
-
-describe('createVirtualizerStore', () => {
- const count = 100;
-
- describe('initialization', () => {
- it('should create virtualizer store with default options', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store).toBeDefined();
- expect(store.virtualItems).toBeDefined();
- expect(typeof store.virtualItems.subscribe).toBe('function');
- });
-
- it('should use custom estimateSize function', () => {
- const estimateSize = vi.fn(() => 120);
-
- const store = createVirtualizerStore({
- count,
- estimateSize,
- });
-
- expect(typeof estimateSize).toBe('function');
- expect(store).toBeDefined();
- });
-
- it('should use custom overscan value', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- overscan: 10,
- });
-
- expect(store).toBeDefined();
- });
-
- it('should use getItemKey for stable keys', () => {
- const getItemKey = vi.fn((index: number) => `item-${index}`);
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- getItemKey,
- });
-
- expect(typeof getItemKey).toBe('function');
- });
-
- it('should use scrollMargin', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- scrollMargin: 50,
- });
-
- expect(store).toBeDefined();
- });
- });
-
- describe('virtual items structure', () => {
- it('should provide virtual items as a readable store', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.virtualItems).toBeDefined();
- expect(typeof store.virtualItems.subscribe).toBe('function');
- });
- });
-
- describe('total size calculation', () => {
- it('should provide totalSize as a readable store', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.totalSize).toBeDefined();
- expect(typeof store.totalSize.subscribe).toBe('function');
- });
-
- it('should calculate total size via subscription', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- let size = 0;
- const unsubscribe = store.totalSize.subscribe(value => {
- size = value;
- });
-
- expect(size).toBeGreaterThanOrEqual(0);
- unsubscribe();
- });
- });
-
- describe('scroll offset', () => {
- it('should provide scrollOffset as a readable store', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.scrollOffset).toBeDefined();
- expect(typeof store.scrollOffset.subscribe).toBe('function');
- });
-
- it('should initialize scrollOffset to 0 via subscription', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- let offset = 0;
- const unsubscribe = store.scrollOffset.subscribe(value => {
- offset = value;
- });
-
- expect(offset).toBe(0);
- unsubscribe();
- });
- });
-
- describe('scroll element binding', () => {
- it('should allow binding scrollElement', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- const mockElement = {} as HTMLElement;
- store.scrollElement = mockElement;
-
- expect(store.scrollElement).toBe(mockElement);
- });
-
- it('should allow updating scrollElement', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- const mockElement1 = {} as HTMLElement;
- const mockElement2 = {} as HTMLElement;
-
- store.scrollElement = mockElement1;
- expect(store.scrollElement).toBe(mockElement1);
-
- store.scrollElement = mockElement2;
- expect(store.scrollElement).toBe(mockElement2);
- });
-
- it('should initialize scrollElement as null', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.scrollElement).toBeNull();
- });
- });
-
- describe('scroll methods API', () => {
- it('should provide scrollToIndex method', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.scrollToIndex).toBeDefined();
- expect(typeof store.scrollToIndex).toBe('function');
- });
-
- it('should provide scrollToOffset method', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.scrollToOffset).toBeDefined();
- expect(typeof store.scrollToOffset).toBe('function');
- });
-
- it('should provide measureElement method', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(store.measureElement).toBeDefined();
- expect(typeof store.measureElement).toBe('function');
- });
- });
-
- describe('scroll methods functionality', () => {
- it('should handle scrollToIndex with options', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(() => {
- store.scrollToIndex(10, { align: 'start' });
- }).not.toThrow();
- });
-
- it('should handle scrollToIndex without options', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(() => {
- store.scrollToIndex(10);
- }).not.toThrow();
- });
-
- it('should handle scrollToOffset', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- expect(() => {
- store.scrollToOffset(100);
- }).not.toThrow();
- });
-
- it('should handle measureElement', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- const mockElement = {
- dataset: { index: '5' },
- getAttribute: () => null,
- } as unknown as HTMLElement;
-
- expect(() => {
- store.measureElement(mockElement);
- }).not.toThrow();
- });
- });
-
- describe('edge cases', () => {
- it('should handle empty items (count: 0)', () => {
- const store = createVirtualizerStore({
- count: 0,
- estimateSize: () => 80,
- });
-
- expect(store.virtualItems).toBeDefined();
- expect(store.totalSize).toBeDefined();
- expect(store.scrollOffset).toBeDefined();
- });
-
- it('should handle single item (count: 1)', () => {
- const store = createVirtualizerStore({
- count: 1,
- estimateSize: () => 80,
- });
-
- expect(store.virtualItems).toBeDefined();
- expect(store.totalSize).toBeDefined();
- expect(store.scrollOffset).toBeDefined();
- });
-
- it('should handle large dataset', () => {
- const store = createVirtualizerStore({
- count: 10000,
- estimateSize: () => 80,
- });
-
- expect(store.virtualItems).toBeDefined();
- expect(store.totalSize).toBeDefined();
- expect(store.scrollOffset).toBeDefined();
- });
- });
-
- describe('custom configuration', () => {
- it('should accept all optional parameters', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 100,
- overscan: 7,
- scrollMargin: 50,
- getItemKey: index => `key-${index}`,
- });
-
- expect(store).toBeDefined();
- expect(store.virtualItems).toBeDefined();
- expect(store.totalSize).toBeDefined();
- expect(store.scrollOffset).toBeDefined();
- expect(store.scrollToIndex).toBeDefined();
- expect(store.scrollToOffset).toBeDefined();
- expect(store.measureElement).toBeDefined();
- expect(store.scrollElement).toBeDefined();
- });
-
- it('should work with minimal configuration', () => {
- const store = createVirtualizerStore({
- count: 0,
- estimateSize: () => 80,
- });
-
- expect(store).toBeDefined();
- });
- });
-
- describe('reactive stores', () => {
- it('should return virtualItems as a readable store', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- // virtualItems should be a Readable store
- expect(typeof store.virtualItems.subscribe).toBe('function');
- });
-
- it('should return totalSize as a readable store', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- // totalSize should be a Readable store
- expect(typeof store.totalSize.subscribe).toBe('function');
- });
-
- it('should return scrollOffset as a readable store', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- // scrollOffset should be a Readable store
- expect(typeof store.scrollOffset.subscribe).toBe('function');
- });
-
- it('should provide virtualItems via subscription', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- let items: any[] = [];
- const unsubscribe = store.virtualItems.subscribe(value => {
- items = value;
- });
-
- expect(Array.isArray(items)).toBe(true);
- unsubscribe();
- });
-
- it('should provide totalSize via subscription', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- let size = 0;
- const unsubscribe = store.totalSize.subscribe(value => {
- size = value;
- });
-
- expect(typeof size).toBe('number');
- expect(size).toBeGreaterThanOrEqual(0);
- unsubscribe();
- });
-
- it('should provide scrollOffset via subscription', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 80,
- });
-
- let offset = 0;
- const unsubscribe = store.scrollOffset.subscribe(value => {
- offset = value;
- });
-
- expect(typeof offset).toBe('number');
- unsubscribe();
- });
- });
-
- describe('dynamic estimateSize', () => {
- it('should handle function-based estimateSize', () => {
- const estimateSize = (index: number): number => {
- return 80 + (index % 2) * 40; // Alternate between 80 and 120
- };
-
- const store = createVirtualizerStore({
- count,
- estimateSize,
- });
-
- expect(store).toBeDefined();
- expect(store.totalSize).toBeDefined();
- });
-
- it('should handle constant estimateSize', () => {
- const store = createVirtualizerStore({
- count,
- estimateSize: () => 100,
- });
-
- expect(store).toBeDefined();
- expect(store.totalSize).toBeDefined();
- });
- });
-});
diff --git a/src/shared/store/createVirtualizerStore.ts b/src/shared/store/createVirtualizerStore.ts
deleted file mode 100644
index cc1dcf4..0000000
--- a/src/shared/store/createVirtualizerStore.ts
+++ /dev/null
@@ -1,282 +0,0 @@
-/**
- * ============================================================================
- * VIRTUALIZER STORE - STORE PATTERN
- * ============================================================================
- *
- * Svelte store-based virtualizer for virtualized lists.
- *
- * Benefits of store pattern over hook:
- * - More Svelte-native (stores are idiomatic, hooks are React-inspired)
- * - Better reactivity (stores auto-derive values using derived())
- * - Consistent with project patterns (createFilterStore, createControlStore)
- * - More extensible (can add store methods)
- * - Type-safe with TypeScript generics
- *
- * Performance:
- * - Renders only visible items (50-100 max regardless of total count)
- * - Maintains 60FPS scrolling with 10,000+ items
- * - Minimal memory usage
- * - Smooth scrolling without jank
- *
- * Usage:
- * ```svelte
- *
- *
- *
- *
- * {#each virtualItems as item (item.key)}
- *
- * {items[item.index].name}
- *
- * {/each}
- *
- *
- * ```
- */
-
-import { createVirtualizer } from '@tanstack/svelte-virtual';
-import type { VirtualItem as CoreVirtualItem } from '@tanstack/virtual-core';
-import {
- type Readable,
- type Writable,
- derived,
- writable,
-} from 'svelte/store';
-
-/**
- * Virtual item returned by the virtualizer
- */
-export interface VirtualItem {
- /** Item index in the original array */
- index: number;
- /** Start position (pixels) */
- start: number;
- /** Item size (pixels) */
- size: number;
- /** End position (pixels) */
- end: number;
- /** Stable key for rendering */
- key: string | number;
-}
-
-/**
- * Configuration options for createVirtualizerStore
- */
-export interface VirtualizerOptions {
- /** Fixed count of items (required) */
- count: number;
- /** Estimated size for each item (in pixels) */
- estimateSize: (index: number) => number;
- /** Number of items to render beyond viewport */
- overscan?: number;
- /** Function to get stable key for each item */
- getItemKey?: (index: number) => string | number;
- /** Scroll offset threshold for triggering update (in pixels) */
- scrollMargin?: number;
-}
-
-/**
- * Options for scrollToIndex
- */
-export interface ScrollToIndexOptions {
- /** Alignment behavior */
- align?: 'start' | 'center' | 'end' | 'auto';
-}
-
-/**
- * Virtualizer store model with reactive stores and methods
- */
-export interface VirtualizerStore {
- /** Subscribe to scroll element state */
- subscribe: Writable<{ scrollElement: HTMLElement | null }>['subscribe'];
- /** Set scroll element state */
- set: Writable<{ scrollElement: HTMLElement | null }>['set'];
- /** Update scroll element state */
- update: Writable<{ scrollElement: HTMLElement | null }>['update'];
- /** Array of virtual items to render (reactive store) */
- virtualItems: Readable;
- /** Total size of all items (in pixels) (reactive store) */
- totalSize: Readable;
- /** Current scroll offset (in pixels) (reactive store) */
- scrollOffset: Readable;
- /** Scroll to a specific item index */
- scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;
- /** Scroll to a specific offset */
- scrollToOffset: (offset: number) => void;
- /** Manually measure an item element */
- measureElement: (element: HTMLElement) => void;
- /** Scroll element reference (getter/setter for binding) */
- scrollElement: HTMLElement | null;
-}
-
-/**
- * Create a virtualizer store using Svelte stores
- *
- * This store wraps TanStack Virtual in a Svelte-idiomatic way.
- * The scroll element can be bound to the store for reactive virtualization.
- *
- * @param options - Virtualization configuration
- * @returns VirtualizerStore with reactive values and methods
- *
- * @example
- * ```svelte
- *
- *
- *
- *
- *
- * ```
- */
-export function createVirtualizerStore(
- options: VirtualizerOptions,
-): VirtualizerStore {
- const {
- count,
- estimateSize,
- overscan = 5,
- getItemKey,
- scrollMargin,
- } = options;
-
- // Internal state for scroll element
- const { subscribe: scrollElementSubscribe, set: setScrollElement, update } = writable<
- { scrollElement: HTMLElement | null }
- >({ scrollElement: null });
-
- // Create virtualizer - returns a readable store
- const virtualizerStore = createVirtualizer({
- count,
- getScrollElement: () => {
- let scrollElement: HTMLElement | null = null;
- scrollElementSubscribe(state => {
- scrollElement = state.scrollElement;
- });
- return scrollElement;
- },
- estimateSize,
- overscan,
- scrollMargin,
- getItemKey: getItemKey ?? ((index: number) => index),
- });
-
- // Current virtualizer instance (unwrapped from readable store)
- let virtualizerInstance: any;
-
- // Subscribe to the readable store
- const _unsubscribe = virtualizerStore.subscribe(value => {
- virtualizerInstance = value;
- });
-
- /**
- * Get virtual items from current instance
- */
- function getVirtualItems(): VirtualItem[] {
- if (!virtualizerInstance) return [];
-
- const items = virtualizerInstance.getVirtualItems();
-
- return items.map((item: CoreVirtualItem): VirtualItem => ({
- index: item.index,
- start: item.start,
- size: item.size,
- end: item.end,
- key: String(item.key),
- }));
- }
-
- /**
- * Get total size from current instance
- */
- function getTotalSize(): number {
- return virtualizerInstance ? virtualizerInstance.getTotalSize() : 0;
- }
-
- /**
- * Get current scroll offset
- */
- function getScrollOffset(): number {
- return virtualizerInstance?.scrollOffset ?? 0;
- }
-
- /**
- * Scroll to a specific item index
- *
- * @param index - Item index to scroll to
- * @param options - Alignment options
- */
- function scrollToIndex(index: number, options?: ScrollToIndexOptions): void {
- virtualizerInstance?.scrollToIndex(index, options);
- }
-
- /**
- * Scroll to a specific offset
- *
- * @param offset - Scroll offset in pixels
- */
- function scrollToOffset(offset: number): void {
- virtualizerInstance?.scrollToOffset(offset);
- }
-
- /**
- * Manually measure an item element
- *
- * Useful when item sizes are dynamic and need precise measurement.
- *
- * @param element - The element to measure
- */
- function measureElement(element: HTMLElement): void {
- virtualizerInstance?.measureElement(element);
- }
-
- // Create derived stores for reactive values
- const virtualItemsStore = derived(virtualizerStore, () => getVirtualItems());
- const totalSizeStore = derived(virtualizerStore, () => getTotalSize());
- const scrollOffsetStore = derived(virtualizerStore, () => getScrollOffset());
-
- // Return store object with methods and derived stores
- return {
- subscribe: scrollElementSubscribe,
- set: setScrollElement,
- update,
- virtualItems: virtualItemsStore,
- totalSize: totalSizeStore,
- scrollOffset: scrollOffsetStore,
- scrollToIndex,
- scrollToOffset,
- measureElement,
- get scrollElement() {
- let scrollElement: HTMLElement | null = null;
- scrollElementSubscribe(state => {
- scrollElement = state.scrollElement;
- });
- return scrollElement;
- },
- set scrollElement(el) {
- setScrollElement({ scrollElement: el });
- },
- };
-}
diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts
deleted file mode 100644
index d6cab99..0000000
--- a/src/shared/store/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Shared store exports
- *
- * Exports all store creators and types for Svelte 5 reactive state management
- */
-
-export { createFilterStore } from './createFilterStore';
-export type {
- FilterModel,
- FilterStore,
- Property,
-} from './createFilterStore';
-
-export { createControlStore } from './createControlStore';
-export type {
- ControlModel,
- ControlStoreModel,
-} from './createControlStore';
-
-export { createVirtualizerStore } from './createVirtualizerStore';
-export type {
- ScrollToIndexOptions,
- VirtualItem,
- VirtualizerOptions,
- VirtualizerStore,
-} from './createVirtualizerStore';