@pyreon/virtual 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ import { onMount, onUnmount } from '@pyreon/core'
2
+ import { signal, effect, batch } from '@pyreon/reactivity'
3
+ import type { Signal } from '@pyreon/reactivity'
4
+ import {
5
+ Virtualizer,
6
+ elementScroll,
7
+ observeElementOffset,
8
+ observeElementRect,
9
+ type VirtualizerOptions,
10
+ type VirtualItem,
11
+ } from '@tanstack/virtual-core'
12
+
13
+ export type UseVirtualizerOptions<
14
+ TScrollElement extends Element,
15
+ TItemElement extends Element,
16
+ > = () => Omit<
17
+ VirtualizerOptions<TScrollElement, TItemElement>,
18
+ 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
19
+ > &
20
+ Partial<
21
+ Pick<
22
+ VirtualizerOptions<TScrollElement, TItemElement>,
23
+ 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
24
+ >
25
+ >
26
+
27
+ export interface UseVirtualizerResult<
28
+ TScrollElement extends Element,
29
+ TItemElement extends Element,
30
+ > {
31
+ /** The virtualizer instance — read to access all methods. */
32
+ instance: Virtualizer<TScrollElement, TItemElement>
33
+ /** Reactive signal of currently visible virtual items. */
34
+ virtualItems: Signal<VirtualItem[]>
35
+ /** Reactive signal of the total scrollable size in pixels. */
36
+ totalSize: Signal<number>
37
+ /** Reactive signal indicating whether the user is scrolling. */
38
+ isScrolling: Signal<boolean>
39
+ }
40
+
41
+ /**
42
+ * Create a reactive TanStack Virtual virtualizer for element-based scrolling.
43
+ *
44
+ * Options are passed as a function so reactive signals (e.g. count, estimateSize)
45
+ * can be read inside, and the virtualizer updates automatically when they change.
46
+ *
47
+ * @example
48
+ * const parentRef = signal<HTMLDivElement | null>(null)
49
+ * const virtual = useVirtualizer(() => ({
50
+ * count: 10000,
51
+ * getScrollElement: () => parentRef(),
52
+ * estimateSize: () => 35,
53
+ * }))
54
+ * // virtual.virtualItems() — array of visible VirtualItem
55
+ * // virtual.totalSize() — total height/width for the inner container
56
+ */
57
+ export function useVirtualizer<
58
+ TScrollElement extends Element,
59
+ TItemElement extends Element,
60
+ >(
61
+ options: UseVirtualizerOptions<TScrollElement, TItemElement>,
62
+ ): UseVirtualizerResult<TScrollElement, TItemElement> {
63
+ const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> = {
64
+ observeElementRect,
65
+ observeElementOffset,
66
+ scrollToFn: elementScroll,
67
+ ...options(),
68
+ }
69
+
70
+ const virtualItems = signal<VirtualItem[]>([])
71
+ const totalSize = signal(0)
72
+ const isScrolling = signal(false)
73
+
74
+ // Store latest user options so onChange always reads the freshest reference
75
+ let latestUserOpts = options()
76
+
77
+ const instance = new Virtualizer<TScrollElement, TItemElement>(
78
+ resolvedOptions,
79
+ )
80
+
81
+ // Track reactive options: when signals inside options() change, update the virtualizer.
82
+ const effectCleanup = effect(() => {
83
+ latestUserOpts = options()
84
+ instance.setOptions({
85
+ ...instance.options,
86
+ ...latestUserOpts,
87
+ onChange: (inst, sync) => {
88
+ batch(() => {
89
+ virtualItems.set(inst.getVirtualItems())
90
+ totalSize.set(inst.getTotalSize())
91
+ isScrolling.set(inst.isScrolling)
92
+ })
93
+ // Read latest opts to avoid stale closure
94
+ latestUserOpts.onChange?.(inst, sync)
95
+ },
96
+ })
97
+
98
+ // After updating options, recalculate and re-emit
99
+ instance._willUpdate()
100
+ batch(() => {
101
+ virtualItems.set(instance.getVirtualItems())
102
+ totalSize.set(instance.getTotalSize())
103
+ })
104
+ })
105
+
106
+ // Lifecycle: mount observers, clean up on unmount.
107
+ let mountCleanup: (() => void) | undefined
108
+ onMount(() => {
109
+ mountCleanup = instance._didMount()
110
+ instance._willUpdate()
111
+ batch(() => {
112
+ virtualItems.set(instance.getVirtualItems())
113
+ totalSize.set(instance.getTotalSize())
114
+ })
115
+ return undefined
116
+ })
117
+
118
+ onUnmount(() => {
119
+ effectCleanup.dispose()
120
+ mountCleanup?.()
121
+ })
122
+
123
+ return { instance, virtualItems, totalSize, isScrolling }
124
+ }
@@ -0,0 +1,106 @@
1
+ import { onMount, onUnmount } from '@pyreon/core'
2
+ import { signal, effect, batch } from '@pyreon/reactivity'
3
+ import type { Signal } from '@pyreon/reactivity'
4
+ import {
5
+ Virtualizer,
6
+ windowScroll,
7
+ observeWindowOffset,
8
+ observeWindowRect,
9
+ type VirtualizerOptions,
10
+ type VirtualItem,
11
+ } from '@tanstack/virtual-core'
12
+
13
+ export type UseWindowVirtualizerOptions<TItemElement extends Element> =
14
+ () => Omit<
15
+ VirtualizerOptions<Window, TItemElement>,
16
+ | 'observeElementRect'
17
+ | 'observeElementOffset'
18
+ | 'scrollToFn'
19
+ | 'getScrollElement'
20
+ > &
21
+ Partial<
22
+ Pick<
23
+ VirtualizerOptions<Window, TItemElement>,
24
+ 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
25
+ >
26
+ >
27
+
28
+ export interface UseWindowVirtualizerResult<TItemElement extends Element> {
29
+ instance: Virtualizer<Window, TItemElement>
30
+ virtualItems: Signal<VirtualItem[]>
31
+ totalSize: Signal<number>
32
+ isScrolling: Signal<boolean>
33
+ }
34
+
35
+ /**
36
+ * Create a reactive TanStack Virtual virtualizer for window-based scrolling.
37
+ *
38
+ * @example
39
+ * const virtual = useWindowVirtualizer(() => ({
40
+ * count: 10000,
41
+ * estimateSize: () => 35,
42
+ * }))
43
+ */
44
+ export function useWindowVirtualizer<TItemElement extends Element>(
45
+ options: UseWindowVirtualizerOptions<TItemElement>,
46
+ ): UseWindowVirtualizerResult<TItemElement> {
47
+ const virtualItems = signal<VirtualItem[]>([])
48
+ const totalSize = signal(0)
49
+ const isScrolling = signal(false)
50
+
51
+ const resolvedOptions: VirtualizerOptions<Window, TItemElement> = {
52
+ observeElementRect: observeWindowRect,
53
+ observeElementOffset: observeWindowOffset,
54
+ scrollToFn: windowScroll,
55
+ initialOffset: typeof document !== 'undefined' ? window.scrollY : 0,
56
+ getScrollElement: () =>
57
+ typeof window !== 'undefined' ? window : (null as unknown as Window),
58
+ ...options(),
59
+ }
60
+
61
+ // Store latest user options so onChange always reads the freshest reference
62
+ let latestUserOpts = options()
63
+
64
+ const instance = new Virtualizer<Window, TItemElement>(resolvedOptions)
65
+
66
+ const effectCleanup = effect(() => {
67
+ latestUserOpts = options()
68
+ instance.setOptions({
69
+ ...instance.options,
70
+ ...latestUserOpts,
71
+ onChange: (inst, sync) => {
72
+ batch(() => {
73
+ virtualItems.set(inst.getVirtualItems())
74
+ totalSize.set(inst.getTotalSize())
75
+ isScrolling.set(inst.isScrolling)
76
+ })
77
+ // Read latest opts to avoid stale closure
78
+ latestUserOpts.onChange?.(inst, sync)
79
+ },
80
+ })
81
+
82
+ instance._willUpdate()
83
+ batch(() => {
84
+ virtualItems.set(instance.getVirtualItems())
85
+ totalSize.set(instance.getTotalSize())
86
+ })
87
+ })
88
+
89
+ let mountCleanup: (() => void) | undefined
90
+ onMount(() => {
91
+ mountCleanup = instance._didMount()
92
+ instance._willUpdate()
93
+ batch(() => {
94
+ virtualItems.set(instance.getVirtualItems())
95
+ totalSize.set(instance.getTotalSize())
96
+ })
97
+ return undefined
98
+ })
99
+
100
+ onUnmount(() => {
101
+ effectCleanup.dispose()
102
+ mountCleanup?.()
103
+ })
104
+
105
+ return { instance, virtualItems, totalSize, isScrolling }
106
+ }