@tanstack/marko-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.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @tanstack/marko-virtual
2
+
3
+ Headless UI for virtualizing scrollable elements in [Marko 6](https://markojs.com), built on top of [`@tanstack/virtual-core`](https://tanstack.com/virtual).
4
+
5
+ Part of the [TanStack Virtual](https://tanstack.com/virtual) family.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @tanstack/marko-virtual
11
+ # or
12
+ pnpm add @tanstack/marko-virtual
13
+ ```
14
+
15
+ **Peer dependency:** `marko ^6.0.0`
16
+
17
+ ## Setup
18
+
19
+ Add to your project's `marko.json` to make the tags available:
20
+
21
+ ```json
22
+ {
23
+ "taglib-imports": ["@tanstack/marko-virtual/marko.json"]
24
+ }
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### `<virtualizer>` — scrollable container
30
+
31
+ Use when you control the scroll element (a div with `overflow: auto`).
32
+
33
+ ```marko
34
+ <!DOCTYPE html>
35
+ <html>
36
+ <body>
37
+
38
+ <let/mounted = false/>
39
+ <script() { mounted = true }/>
40
+
41
+ <div/scrollEl style="height: 400px; overflow-y: auto">
42
+ <if=mounted>
43
+ <virtualizer|{ virtualItems, totalSize }|
44
+ count=10000
45
+ estimateSize=() => 35
46
+ getScrollElement=scrollEl
47
+ >
48
+ <div style=`height: ${totalSize}px; position: relative`>
49
+ <for|item| of=virtualItems>
50
+ <div
51
+ data-index=item.index
52
+ style=`position: absolute; transform: translateY(${item.start}px);
53
+ height: ${item.size}px; width: 100%`
54
+ >
55
+ Row ${item.index}
56
+ </div>
57
+ </for>
58
+ </div>
59
+ </virtualizer>
60
+ </if>
61
+ </div>
62
+
63
+ </body>
64
+ </html>
65
+ ```
66
+
67
+ > **Why `mounted`?** The virtualizer measures the scroll element's dimensions on mount. During SSR the DOM has no layout, so `offsetHeight` is 0 and no items would be calculated. The `<if=mounted>` guard ensures the virtualizer only renders after the browser has real dimensions available.
68
+
69
+ ### `<window-virtualizer>` — full-page scrolling
70
+
71
+ Use when the page itself is the scroll container.
72
+
73
+ ```marko
74
+ <!DOCTYPE html>
75
+ <html>
76
+ <body>
77
+
78
+ <window-virtualizer|{ virtualItems, totalSize }| count=10000 estimateSize=() => 35>
79
+ <div style=`height: ${totalSize}px; position: relative`>
80
+ <for|item| of=virtualItems>
81
+ <div
82
+ data-index=item.index
83
+ style=`position: absolute; transform: translateY(${item.start}px);
84
+ height: ${item.size}px; width: 100%`
85
+ >
86
+ Row ${item.index}
87
+ </div>
88
+ </for>
89
+ </div>
90
+ </window-virtualizer>
91
+
92
+ </body>
93
+ </html>
94
+ ```
95
+
96
+ ## Tag parameters
97
+
98
+ Both tags use Marko 6's tag parameters pattern. The body receives virtual state via `|{ ... }|` destructuring:
99
+
100
+ ```marko
101
+ <virtualizer|{ virtualItems, totalSize, measureElement, scrollToIndex, scrollToOffset }|
102
+ count=...
103
+ getScrollElement=...
104
+ >
105
+ <!-- virtualItems, totalSize etc are in scope here -->
106
+ </virtualizer>
107
+ ```
108
+
109
+ ## API Reference
110
+
111
+ ### `<virtualizer>`
112
+
113
+ | Attribute | Type | Required | Description |
114
+ | -------------------- | --------------------------- | -------- | --------------------------------------------------- |
115
+ | `count` | `number` | ✅ | Total number of items |
116
+ | `getScrollElement` | `() => Element \| null` | ✅ | Returns the scroll container |
117
+ | `estimateSize` | `(index: number) => number` | | Estimated item size in px (default: `50`) |
118
+ | `overscan` | `number` | | Items to render outside the viewport (default: `5`) |
119
+ | `horizontal` | `boolean` | | Enable horizontal scrolling (default: `false`) |
120
+ | `paddingStart` | `number` | | Padding before the first item in px |
121
+ | `paddingEnd` | `number` | | Padding after the last item in px |
122
+ | `scrollPaddingStart` | `number` | | Scroll padding at the start |
123
+ | `scrollPaddingEnd` | `number` | | Scroll padding at the end |
124
+ | `gap` | `number` | | Gap between items in px |
125
+ | `lanes` | `number` | | Number of lanes for grid layouts |
126
+ | `initialOffset` | `number \| (() => number)` | | Initial scroll offset |
127
+
128
+ **Tag parameters provided to body:**
129
+
130
+ | Parameter | Type | Description |
131
+ | ---------------- | ----------------------------------------------------- | -------------------------------------------------------- |
132
+ | `virtualItems` | `VirtualItem[]` | Items to render, with `index`, `start`, `size`, `key` |
133
+ | `totalSize` | `number` | Total scrollable size in px — set on the inner container |
134
+ | `measureElement` | `(el: Element \| null) => void` | Pass to `ref` for dynamic size measurement |
135
+ | `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Scroll to an item by index |
136
+ | `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Scroll to a px offset |
137
+
138
+ ### `<window-virtualizer>`
139
+
140
+ Same as `<virtualizer>` except there is no `getScrollElement`, `horizontal`, or `initialOffset` — the window is always the scroll container.
141
+
142
+ ## Examples
143
+
144
+ All examples use `@marko/run`. Run any with:
145
+
146
+ ```bash
147
+ pnpm --filter tanstack-marko-virtual-example-<name> dev
148
+ ```
149
+
150
+ | Example | Description |
151
+ | ----------------- | ----------------------------------------------- |
152
+ | `fixed` | Fixed-size rows, columns, and grid |
153
+ | `variable` | Variable sizes via `estimateSize` |
154
+ | `dynamic` | Unknown sizes measured via `measureElement` |
155
+ | `grid` | Two virtualizers sharing one scroll element |
156
+ | `smooth-scroll` | `scrollToIndex` with CSS smooth scrolling |
157
+ | `infinite-scroll` | Lazy data loading with a fixed total count |
158
+ | `window` | Full-page scrolling with `<window-virtualizer>` |
159
+
160
+ ## Dynamic sizing
161
+
162
+ Use `measureElement` to measure items whose size isn't known upfront:
163
+
164
+ ```marko
165
+ <virtualizer|{ virtualItems, totalSize, measureElement }|
166
+ count=items.length
167
+ estimateSize=() => 50
168
+ getScrollElement=scrollEl
169
+ >
170
+ <div style=`height: ${totalSize}px; position: relative`>
171
+ <for|item| of=virtualItems>
172
+ <div
173
+ ref=measureElement
174
+ data-index=item.index
175
+ style=`position: absolute; transform: translateY(${item.start}px); width: 100%`
176
+ >
177
+ ${items[item.index]}
178
+ </div>
179
+ </for>
180
+ </div>
181
+ </virtualizer>
182
+ ```
183
+
184
+ > `data-index` is required on measured elements — the virtualizer uses it to map measurements back to items.
185
+
186
+ ## TypeScript
187
+
188
+ The tags are fully typed. The Marko language server reads the source `.marko` files directly — no `.d.ts` generation is needed. Ensure `@marko/language-tools` is installed in your editor for IDE support.
189
+
190
+ ## License
191
+
192
+ MIT © [Tanner Linsley](https://github.com/tannerlinsley)
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const virtualCore = require("@tanstack/virtual-core");
4
+ Object.keys(virtualCore).forEach((k) => {
5
+ if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
6
+ enumerable: true,
7
+ get: () => virtualCore[k]
8
+ });
9
+ });
10
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @tanstack/marko-virtual — Marko 6 adapter for @tanstack/virtual-core
3
+ *
4
+ * Provides row, column, and grid virtualisation for Marko 6 applications
5
+ * via the `<virtualizer>` and `<window-virtualizer>` tags.
6
+ *
7
+ * Tags are auto-discovered by the Marko compiler when this package is
8
+ * installed — no imports needed. Just use `<virtualizer>` or
9
+ * `<window-virtualizer>` directly in your `.marko` files.
10
+ *
11
+ * @example
12
+ * ```marko
13
+ * // Row virtualisation
14
+ * <div/scrollEl style="height: 400px; overflow-y: auto">
15
+ * <virtualizer|{ virtualItems, totalSize }|
16
+ * count=10000
17
+ * estimateSize=() => 35
18
+ * getScrollElement=scrollEl
19
+ * >
20
+ * <div style=`height: ${totalSize}px; position: relative`>
21
+ * <for|item| of=virtualItems>
22
+ * <div style=`position: absolute; top: 0; transform: translateY(${item.start}px); height: ${item.size}px`>
23
+ * Row ${item.index}
24
+ * </div>
25
+ * </for>
26
+ * </div>
27
+ * </virtualizer>
28
+ * </div>
29
+ *
30
+ * // Column virtualisation — same tag, horizontal=true
31
+ * <virtualizer|{ virtualItems, totalSize }|
32
+ * count=10000
33
+ * estimateSize=() => 100
34
+ * horizontal=true
35
+ * getScrollElement=scrollEl
36
+ * >
37
+ * ...
38
+ * </virtualizer>
39
+ *
40
+ * // Grid — compose two <virtualizer> tags
41
+ * <virtualizer|{ virtualItems: rowItems, totalSize: rowTotal }|
42
+ * count=10000 estimateSize=() => 35 getScrollElement=scrollEl
43
+ * >
44
+ * <virtualizer|{ virtualItems: colItems, totalSize: colTotal }|
45
+ * count=200 estimateSize=() => 100 horizontal=true getScrollElement=scrollEl
46
+ * >
47
+ * ...
48
+ * </virtualizer>
49
+ * </virtualizer>
50
+ * ```
51
+ *
52
+ * @packageDocumentation
53
+ */
54
+ export * from '@tanstack/virtual-core';
@@ -0,0 +1,16 @@
1
+ import { VirtualizerOptions } from '@tanstack/virtual-core';
2
+ export interface VirtualizerInput {
3
+ count: number;
4
+ estimateSize?: (index: number) => number;
5
+ getScrollElement: () => Element | null;
6
+ overscan?: number;
7
+ horizontal?: boolean;
8
+ paddingStart?: number;
9
+ paddingEnd?: number;
10
+ scrollPaddingStart?: number;
11
+ scrollPaddingEnd?: number;
12
+ gap?: number;
13
+ lanes?: number;
14
+ initialOffset?: number | (() => number);
15
+ }
16
+ export declare function buildOptions(input: VirtualizerInput, notify: () => void): VirtualizerOptions<Element, Element>;
@@ -0,0 +1,13 @@
1
+ import { VirtualizerOptions } from '@tanstack/virtual-core';
2
+ export interface WindowVirtualizerInput {
3
+ count: number;
4
+ estimateSize?: (index: number) => number;
5
+ overscan?: number;
6
+ paddingStart?: number;
7
+ paddingEnd?: number;
8
+ scrollPaddingStart?: number;
9
+ scrollPaddingEnd?: number;
10
+ gap?: number;
11
+ lanes?: number;
12
+ }
13
+ export declare function buildOptions(input: WindowVirtualizerInput, notify: () => void): VirtualizerOptions<Window, Element>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @tanstack/marko-virtual — Marko 6 adapter for @tanstack/virtual-core
3
+ *
4
+ * Provides row, column, and grid virtualisation for Marko 6 applications
5
+ * via the `<virtualizer>` and `<window-virtualizer>` tags.
6
+ *
7
+ * Tags are auto-discovered by the Marko compiler when this package is
8
+ * installed — no imports needed. Just use `<virtualizer>` or
9
+ * `<window-virtualizer>` directly in your `.marko` files.
10
+ *
11
+ * @example
12
+ * ```marko
13
+ * // Row virtualisation
14
+ * <div/scrollEl style="height: 400px; overflow-y: auto">
15
+ * <virtualizer|{ virtualItems, totalSize }|
16
+ * count=10000
17
+ * estimateSize=() => 35
18
+ * getScrollElement=scrollEl
19
+ * >
20
+ * <div style=`height: ${totalSize}px; position: relative`>
21
+ * <for|item| of=virtualItems>
22
+ * <div style=`position: absolute; top: 0; transform: translateY(${item.start}px); height: ${item.size}px`>
23
+ * Row ${item.index}
24
+ * </div>
25
+ * </for>
26
+ * </div>
27
+ * </virtualizer>
28
+ * </div>
29
+ *
30
+ * // Column virtualisation — same tag, horizontal=true
31
+ * <virtualizer|{ virtualItems, totalSize }|
32
+ * count=10000
33
+ * estimateSize=() => 100
34
+ * horizontal=true
35
+ * getScrollElement=scrollEl
36
+ * >
37
+ * ...
38
+ * </virtualizer>
39
+ *
40
+ * // Grid — compose two <virtualizer> tags
41
+ * <virtualizer|{ virtualItems: rowItems, totalSize: rowTotal }|
42
+ * count=10000 estimateSize=() => 35 getScrollElement=scrollEl
43
+ * >
44
+ * <virtualizer|{ virtualItems: colItems, totalSize: colTotal }|
45
+ * count=200 estimateSize=() => 100 horizontal=true getScrollElement=scrollEl
46
+ * >
47
+ * ...
48
+ * </virtualizer>
49
+ * </virtualizer>
50
+ * ```
51
+ *
52
+ * @packageDocumentation
53
+ */
54
+ export * from '@tanstack/virtual-core';
@@ -0,0 +1,2 @@
1
+ export * from "@tanstack/virtual-core";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,16 @@
1
+ import { VirtualizerOptions } from '@tanstack/virtual-core';
2
+ export interface VirtualizerInput {
3
+ count: number;
4
+ estimateSize?: (index: number) => number;
5
+ getScrollElement: () => Element | null;
6
+ overscan?: number;
7
+ horizontal?: boolean;
8
+ paddingStart?: number;
9
+ paddingEnd?: number;
10
+ scrollPaddingStart?: number;
11
+ scrollPaddingEnd?: number;
12
+ gap?: number;
13
+ lanes?: number;
14
+ initialOffset?: number | (() => number);
15
+ }
16
+ export declare function buildOptions(input: VirtualizerInput, notify: () => void): VirtualizerOptions<Element, Element>;
@@ -0,0 +1,13 @@
1
+ import { VirtualizerOptions } from '@tanstack/virtual-core';
2
+ export interface WindowVirtualizerInput {
3
+ count: number;
4
+ estimateSize?: (index: number) => number;
5
+ overscan?: number;
6
+ paddingStart?: number;
7
+ paddingEnd?: number;
8
+ scrollPaddingStart?: number;
9
+ scrollPaddingEnd?: number;
10
+ gap?: number;
11
+ lanes?: number;
12
+ }
13
+ export declare function buildOptions(input: WindowVirtualizerInput, notify: () => void): VirtualizerOptions<Window, Element>;
package/marko.json ADDED
@@ -0,0 +1 @@
1
+ { "tags-dir": "./src/tags", "script-lang": "ts" }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@tanstack/marko-virtual",
3
+ "version": "0.0.1",
4
+ "description": "Headless UI for virtualizing scrollable elements in Marko 6",
5
+ "author": "Tanner Linsley",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/virtual.git",
10
+ "directory": "packages/marko-virtual"
11
+ },
12
+ "homepage": "https://tanstack.com/virtual",
13
+ "funding": {
14
+ "type": "github",
15
+ "url": "https://github.com/sponsors/tannerlinsley"
16
+ },
17
+ "keywords": [
18
+ "marko",
19
+ "virtual",
20
+ "virtual-core",
21
+ "virtualizer",
22
+ "datagrid"
23
+ ],
24
+ "type": "module",
25
+ "types": "dist/esm/index.d.ts",
26
+ "main": "dist/cjs/index.cjs",
27
+ "module": "dist/esm/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "import": {
31
+ "types": "./dist/esm/index.d.ts",
32
+ "default": "./dist/esm/index.js"
33
+ },
34
+ "require": {
35
+ "types": "./dist/cjs/index.d.cts",
36
+ "default": "./dist/cjs/index.cjs"
37
+ }
38
+ },
39
+ "./package.json": "./package.json",
40
+ "./marko.json": "./marko.json",
41
+ "./src/tags/*": "./src/tags/*"
42
+ },
43
+ "sideEffects": false,
44
+ "files": [
45
+ "dist",
46
+ "src/tags",
47
+ "marko.json",
48
+ "README.md"
49
+ ],
50
+ "scripts": {
51
+ "clean": "premove ./dist ./coverage",
52
+ "test:eslint": "eslint ./src",
53
+ "test:types": "tsc",
54
+ "test:lib": "vitest",
55
+ "test:lib:dev": "pnpm run test:lib --watch",
56
+ "test:build": "publint --strict",
57
+ "build": "vite build"
58
+ },
59
+ "dependencies": {
60
+ "@tanstack/virtual-core": "workspace:*"
61
+ },
62
+ "devDependencies": {
63
+ "@marko/vite": "^3.0.0",
64
+ "@testing-library/dom": "^10.0.0",
65
+ "marko": "^6.0.0"
66
+ },
67
+ "peerDependencies": {
68
+ "marko": "^6.0.0"
69
+ }
70
+ }
@@ -0,0 +1,78 @@
1
+ import { Virtualizer } from "@tanstack/virtual-core"
2
+ import type { VirtualItem, ScrollToOptions } from "@tanstack/virtual-core"
3
+ import { buildOptions } from "./options"
4
+
5
+ export interface Input {
6
+ count: number
7
+ estimateSize?: (index: number) => number
8
+ getScrollElement: () => Element | null
9
+ overscan?: number
10
+ horizontal?: boolean
11
+ paddingStart?: number
12
+ paddingEnd?: number
13
+ scrollPaddingStart?: number
14
+ scrollPaddingEnd?: number
15
+ gap?: number
16
+ lanes?: number
17
+ initialOffset?: number | (() => number)
18
+ content: Marko.Body<[{
19
+ virtualItems: VirtualItem[]
20
+ totalSize: number
21
+ measureElement: ((el: Element | null) => void) | undefined
22
+ scrollToIndex: (index: number, options?: ScrollToOptions) => void
23
+ scrollToOffset: (offset: number, options?: ScrollToOptions) => void
24
+ }]>
25
+ }
26
+
27
+ <let/v = (null as Virtualizer<Element, Element> | null)/>
28
+ <let/cleanup = (null as (() => void) | null)/>
29
+ <let/items = ([] as VirtualItem[])/>
30
+ <let/size = 0/>
31
+
32
+ <lifecycle<{}>
33
+ onMount() {
34
+ const notify = () => {
35
+ const inst = v
36
+ if (!inst) return
37
+ items = inst.getVirtualItems()
38
+ size = inst.getTotalSize()
39
+ }
40
+
41
+ v = new Virtualizer<Element, Element>(buildOptions(input, notify))
42
+ cleanup = v!._didMount()
43
+ v!._willUpdate()
44
+ }
45
+
46
+ onUpdate() {
47
+ const current = v
48
+ if (!current) return
49
+
50
+ const notify = () => {
51
+ const inst = v
52
+ if (!inst) return
53
+ items = inst.getVirtualItems()
54
+ size = inst.getTotalSize()
55
+ }
56
+
57
+ current.setOptions({ ...current.options, ...buildOptions(input, notify) })
58
+
59
+ // _willUpdate() is a no-op when the scroll element is unchanged, so
60
+ // notify() explicitly afterwards to recompute on count/option changes.
61
+ current._willUpdate()
62
+ notify()
63
+ }
64
+
65
+ onDestroy() {
66
+ cleanup?.()
67
+ v = null
68
+ cleanup = null
69
+ }
70
+ />
71
+
72
+ <${input.content}
73
+ virtualItems=items
74
+ totalSize=size
75
+ measureElement=v?.measureElement
76
+ scrollToIndex=(index: number, options?: ScrollToOptions) => v?.scrollToIndex(index, options)
77
+ scrollToOffset=(offset: number, options?: ScrollToOptions) => v?.scrollToOffset(offset, options)
78
+ />
@@ -0,0 +1,50 @@
1
+ import {
2
+ elementScroll,
3
+ observeElementOffset,
4
+ observeElementRect,
5
+ } from '@tanstack/virtual-core'
6
+ import type { VirtualizerOptions } from '@tanstack/virtual-core'
7
+
8
+ // The subset of tag input that maps onto virtual-core options. Kept here
9
+ // (rather than imported from index.marko) so this helper has no dependency
10
+ // on the Marko template's generated types.
11
+ export interface VirtualizerInput {
12
+ count: number
13
+ estimateSize?: (index: number) => number
14
+ getScrollElement: () => Element | null
15
+ overscan?: number
16
+ horizontal?: boolean
17
+ paddingStart?: number
18
+ paddingEnd?: number
19
+ scrollPaddingStart?: number
20
+ scrollPaddingEnd?: number
21
+ gap?: number
22
+ lanes?: number
23
+ initialOffset?: number | (() => number)
24
+ }
25
+
26
+ // Single source of truth for the input -> virtual-core option mapping,
27
+ // shared by onMount (construction) and onUpdate (setOptions).
28
+ export function buildOptions(
29
+ input: VirtualizerInput,
30
+ notify: () => void,
31
+ ): VirtualizerOptions<Element, Element> {
32
+ return {
33
+ count: input.count,
34
+ estimateSize: input.estimateSize ?? (() => 50),
35
+ getScrollElement: input.getScrollElement,
36
+ overscan: input.overscan ?? 5,
37
+ horizontal: input.horizontal ?? false,
38
+ paddingStart: input.paddingStart,
39
+ paddingEnd: input.paddingEnd,
40
+ scrollPaddingStart: input.scrollPaddingStart,
41
+ scrollPaddingEnd: input.scrollPaddingEnd,
42
+ gap: input.gap,
43
+ lanes: input.lanes,
44
+ initialOffset: input.initialOffset,
45
+ observeElementRect,
46
+ observeElementOffset,
47
+ scrollToFn: elementScroll,
48
+ onChange: notify,
49
+ }
50
+ }
@@ -0,0 +1,75 @@
1
+ import { Virtualizer } from "@tanstack/virtual-core"
2
+ import type { VirtualItem, ScrollToOptions } from "@tanstack/virtual-core"
3
+ import { buildOptions } from "./options"
4
+
5
+ export interface Input {
6
+ count: number
7
+ estimateSize?: (index: number) => number
8
+ overscan?: number
9
+ paddingStart?: number
10
+ paddingEnd?: number
11
+ scrollPaddingStart?: number
12
+ scrollPaddingEnd?: number
13
+ gap?: number
14
+ lanes?: number
15
+ content: Marko.Body<[{
16
+ virtualItems: VirtualItem[]
17
+ totalSize: number
18
+ measureElement: ((el: Element | null) => void) | undefined
19
+ scrollToIndex: (index: number, options?: ScrollToOptions) => void
20
+ scrollToOffset: (offset: number, options?: ScrollToOptions) => void
21
+ }]>
22
+ }
23
+
24
+ <let/v = (null as Virtualizer<Window, Element> | null)/>
25
+ <let/cleanup = (null as (() => void) | null)/>
26
+ <let/items = ([] as VirtualItem[])/>
27
+ <let/size = 0/>
28
+
29
+ <lifecycle<{}>
30
+ onMount() {
31
+ const notify = () => {
32
+ const inst = v
33
+ if (!inst) return
34
+ items = inst.getVirtualItems()
35
+ size = inst.getTotalSize()
36
+ }
37
+
38
+ v = new Virtualizer<Window, Element>(buildOptions(input, notify))
39
+ cleanup = v!._didMount()
40
+ v!._willUpdate()
41
+ }
42
+
43
+ onUpdate() {
44
+ const current = v
45
+ if (!current) return
46
+
47
+ const notify = () => {
48
+ const inst = v
49
+ if (!inst) return
50
+ items = inst.getVirtualItems()
51
+ size = inst.getTotalSize()
52
+ }
53
+
54
+ current.setOptions({ ...current.options, ...buildOptions(input, notify) })
55
+
56
+ // _willUpdate() is a no-op when the scroll element is unchanged, so
57
+ // notify() explicitly afterwards to recompute on count/option changes.
58
+ current._willUpdate()
59
+ notify()
60
+ }
61
+
62
+ onDestroy() {
63
+ cleanup?.()
64
+ v = null
65
+ cleanup = null
66
+ }
67
+ />
68
+
69
+ <${input.content}
70
+ virtualItems=items
71
+ totalSize=size
72
+ measureElement=v?.measureElement
73
+ scrollToIndex=(index: number, options?: ScrollToOptions) => v?.scrollToIndex(index, options)
74
+ scrollToOffset=(offset: number, options?: ScrollToOptions) => v?.scrollToOffset(offset, options)
75
+ />
@@ -0,0 +1,46 @@
1
+ import {
2
+ observeWindowOffset,
3
+ observeWindowRect,
4
+ windowScroll,
5
+ } from '@tanstack/virtual-core'
6
+ import type { VirtualizerOptions } from '@tanstack/virtual-core'
7
+
8
+ // The subset of tag input that maps onto virtual-core options. Kept here
9
+ // (rather than imported from index.marko) so this helper has no dependency
10
+ // on the Marko template's generated types.
11
+ export interface WindowVirtualizerInput {
12
+ count: number
13
+ estimateSize?: (index: number) => number
14
+ overscan?: number
15
+ paddingStart?: number
16
+ paddingEnd?: number
17
+ scrollPaddingStart?: number
18
+ scrollPaddingEnd?: number
19
+ gap?: number
20
+ lanes?: number
21
+ }
22
+
23
+ // Single source of truth for the input -> virtual-core option mapping,
24
+ // shared by onMount (construction) and onUpdate (setOptions).
25
+ export function buildOptions(
26
+ input: WindowVirtualizerInput,
27
+ notify: () => void,
28
+ ): VirtualizerOptions<Window, Element> {
29
+ return {
30
+ count: input.count,
31
+ estimateSize: input.estimateSize ?? (() => 50),
32
+ getScrollElement: () => (typeof document !== 'undefined' ? window : null),
33
+ overscan: input.overscan ?? 5,
34
+ paddingStart: input.paddingStart,
35
+ paddingEnd: input.paddingEnd,
36
+ scrollPaddingStart: input.scrollPaddingStart,
37
+ scrollPaddingEnd: input.scrollPaddingEnd,
38
+ gap: input.gap,
39
+ lanes: input.lanes,
40
+ initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0),
41
+ observeElementRect: observeWindowRect,
42
+ observeElementOffset: observeWindowOffset,
43
+ scrollToFn: windowScroll,
44
+ onChange: notify,
45
+ }
46
+ }