@rs-x/cli 2.0.0-next.15 → 2.0.0-next.17

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.
Files changed (31) hide show
  1. package/bin/rsx.cjs +337 -7
  2. package/package.json +1 -1
  3. package/{rs-x-vscode-extension-2.0.0-next.15.vsix → rs-x-vscode-extension-2.0.0-next.17.vsix} +0 -0
  4. package/templates/next-demo/README.md +26 -0
  5. package/templates/next-demo/app/globals.css +431 -0
  6. package/templates/next-demo/app/layout.tsx +22 -0
  7. package/templates/next-demo/app/page.tsx +5 -0
  8. package/templates/next-demo/components/demo-app.tsx +114 -0
  9. package/templates/next-demo/components/virtual-table-row.tsx +40 -0
  10. package/templates/next-demo/components/virtual-table-shell.tsx +86 -0
  11. package/templates/next-demo/hooks/use-virtual-table-controller.ts +26 -0
  12. package/templates/next-demo/hooks/use-virtual-table-viewport.ts +41 -0
  13. package/templates/next-demo/lib/row-data.ts +35 -0
  14. package/templates/next-demo/lib/row-model.ts +45 -0
  15. package/templates/next-demo/lib/rsx-bootstrap.ts +46 -0
  16. package/templates/next-demo/lib/virtual-table-controller.ts +247 -0
  17. package/templates/next-demo/lib/virtual-table-data.service.ts +126 -0
  18. package/templates/vue-demo/README.md +27 -0
  19. package/templates/vue-demo/src/App.vue +89 -0
  20. package/templates/vue-demo/src/components/VirtualTableRow.vue +33 -0
  21. package/templates/vue-demo/src/components/VirtualTableShell.vue +58 -0
  22. package/templates/vue-demo/src/composables/use-virtual-table-controller.ts +33 -0
  23. package/templates/vue-demo/src/composables/use-virtual-table-viewport.ts +40 -0
  24. package/templates/vue-demo/src/env.d.ts +6 -0
  25. package/templates/vue-demo/src/lib/row-data.ts +35 -0
  26. package/templates/vue-demo/src/lib/row-model.ts +45 -0
  27. package/templates/vue-demo/src/lib/rsx-bootstrap.ts +46 -0
  28. package/templates/vue-demo/src/lib/virtual-table-controller.ts +247 -0
  29. package/templates/vue-demo/src/lib/virtual-table-data.service.ts +126 -0
  30. package/templates/vue-demo/src/main.ts +12 -0
  31. package/templates/vue-demo/src/style.css +440 -0
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { useRef, useSyncExternalStore } from 'react';
4
+
5
+ import {
6
+ VirtualTableController,
7
+ type VirtualTableSnapshot,
8
+ } from '@/lib/virtual-table-controller';
9
+
10
+ export function useVirtualTableController(): {
11
+ controller: VirtualTableController;
12
+ snapshot: VirtualTableSnapshot;
13
+ } {
14
+ const controllerRef = useRef<VirtualTableController | null>(null);
15
+ if (!controllerRef.current) {
16
+ controllerRef.current = new VirtualTableController();
17
+ }
18
+ const controller = controllerRef.current;
19
+ const snapshot = useSyncExternalStore(
20
+ controller.subscribe,
21
+ controller.getSnapshot,
22
+ controller.getSnapshot,
23
+ );
24
+
25
+ return { controller, snapshot };
26
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { type RefObject, useEffect, useRef } from 'react';
4
+
5
+ import type { VirtualTableController } from '@/lib/virtual-table-controller';
6
+
7
+ const COMPACT_BREAKPOINT_PX = 720;
8
+ const DEFAULT_ROW_HEIGHT = 36;
9
+ const COMPACT_ROW_HEIGHT = 168;
10
+
11
+ export function useVirtualTableViewport(
12
+ controller: VirtualTableController,
13
+ ): RefObject<HTMLDivElement | null> {
14
+ const viewportRef = useRef<HTMLDivElement | null>(null);
15
+
16
+ useEffect(() => {
17
+ const viewport = viewportRef.current;
18
+ if (!viewport) {
19
+ return;
20
+ }
21
+
22
+ const syncMetrics = (): void => {
23
+ controller.setViewportHeight(viewport.clientHeight);
24
+ controller.setRowHeight(
25
+ viewport.clientWidth <= COMPACT_BREAKPOINT_PX
26
+ ? COMPACT_ROW_HEIGHT
27
+ : DEFAULT_ROW_HEIGHT,
28
+ );
29
+ };
30
+
31
+ syncMetrics();
32
+ const observer = new ResizeObserver(syncMetrics);
33
+ observer.observe(viewport);
34
+
35
+ return () => {
36
+ observer.disconnect();
37
+ };
38
+ }, [controller]);
39
+
40
+ return viewportRef;
41
+ }
@@ -0,0 +1,35 @@
1
+ export type SortKey = 'id' | 'name' | 'price' | 'quantity' | 'category';
2
+ export type SortDirection = 'asc' | 'desc';
3
+
4
+ export type RowData = {
5
+ id: number;
6
+ name: string;
7
+ price: number;
8
+ quantity: number;
9
+ category: string;
10
+ updatedAt: string;
11
+ };
12
+
13
+ const categories = ['Hardware', 'Software', 'Design', 'Ops'];
14
+
15
+ function pad(value: number): string {
16
+ return value.toString().padStart(2, '0');
17
+ }
18
+
19
+ export function createRowData(id: number): RowData {
20
+ const zeroBasedId = id - 1;
21
+ const price = 25 + (zeroBasedId % 1000) / 10;
22
+ const quantity = 1 + (Math.floor(zeroBasedId / 1000) % 100);
23
+ const category = categories[zeroBasedId % categories.length] ?? 'General';
24
+ const month = pad(((zeroBasedId * 7) % 12) + 1);
25
+ const day = pad(((zeroBasedId * 11) % 28) + 1);
26
+
27
+ return {
28
+ id,
29
+ name: `Product ${id.toString().padStart(7, '0')}`,
30
+ price,
31
+ quantity,
32
+ category,
33
+ updatedAt: `2026-${month}-${day}`,
34
+ };
35
+ }
@@ -0,0 +1,45 @@
1
+ import { rsx, type IExpression } from '@rs-x/expression-parser';
2
+
3
+ import type { RowData } from './row-data';
4
+
5
+ export type RowModel = {
6
+ model: RowData;
7
+ idExpr: IExpression<number>;
8
+ nameExpr: IExpression<string>;
9
+ categoryExpr: IExpression<string>;
10
+ priceExpr: IExpression<number>;
11
+ quantityExpr: IExpression<number>;
12
+ updatedAtExpr: IExpression<string>;
13
+ totalExpr: IExpression<number>;
14
+ };
15
+
16
+ export function createRowModel(): RowModel {
17
+ const model: RowData = {
18
+ id: 0,
19
+ name: '',
20
+ price: 0,
21
+ quantity: 0,
22
+ category: 'General',
23
+ updatedAt: '2026-01-01',
24
+ };
25
+
26
+ return {
27
+ model,
28
+ idExpr: rsx<number>('id')(model),
29
+ nameExpr: rsx<string>('name')(model),
30
+ categoryExpr: rsx<string>('category')(model),
31
+ priceExpr: rsx<number>('price')(model),
32
+ quantityExpr: rsx<number>('quantity')(model),
33
+ updatedAtExpr: rsx<string>('updatedAt')(model),
34
+ totalExpr: rsx<number>('price * quantity')(model),
35
+ };
36
+ }
37
+
38
+ export function updateRowModel(target: RowModel, data: RowData): void {
39
+ target.model.id = data.id;
40
+ target.model.name = data.name;
41
+ target.model.price = data.price;
42
+ target.model.quantity = data.quantity;
43
+ target.model.category = data.category;
44
+ target.model.updatedAt = data.updatedAt;
45
+ }
@@ -0,0 +1,46 @@
1
+ import { InjectionContainer } from '@rs-x/core';
2
+ import { RsXExpressionParserModule } from '@rs-x/expression-parser';
3
+
4
+ let initialized = false;
5
+
6
+ type RsxCompiledModule = {
7
+ registerRsxAotCompiledExpressions?: () => void;
8
+ };
9
+
10
+ type RsxPreparsedModule = {
11
+ registerRsxAotParsedExpressionCache?: () => void;
12
+ };
13
+
14
+ async function loadCompiledModule(): Promise<RsxCompiledModule> {
15
+ try {
16
+ return (await import(
17
+ '../app/rsx-generated/' + 'rsx-aot-compiled.generated'
18
+ )) as RsxCompiledModule;
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ async function loadPreparsedModule(): Promise<RsxPreparsedModule> {
25
+ try {
26
+ return (await import(
27
+ '../app/rsx-generated/' + 'rsx-aot-preparsed.generated'
28
+ )) as RsxPreparsedModule;
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ export async function initRsx(): Promise<void> {
35
+ if (initialized) {
36
+ return;
37
+ }
38
+
39
+ const preparsedModule = await loadPreparsedModule();
40
+ const compiledModule = await loadCompiledModule();
41
+
42
+ preparsedModule.registerRsxAotParsedExpressionCache?.();
43
+ compiledModule.registerRsxAotCompiledExpressions?.();
44
+ await InjectionContainer.load(RsXExpressionParserModule);
45
+ initialized = true;
46
+ }
@@ -0,0 +1,247 @@
1
+ import { type RowData, type SortDirection, type SortKey } from '@/lib/row-data';
2
+ import { createRowModel, updateRowModel, type RowModel } from '@/lib/row-model';
3
+ import { VirtualTableDataService } from '@/lib/virtual-table-data.service';
4
+
5
+ export type RowView = {
6
+ index: number;
7
+ top: number;
8
+ row: RowModel;
9
+ };
10
+
11
+ export type VirtualTableSnapshot = {
12
+ visibleRows: RowView[];
13
+ totalRows: number;
14
+ poolSize: number;
15
+ rowsInView: number;
16
+ loadedPageCount: number;
17
+ spacerHeight: number;
18
+ sortKey: SortKey;
19
+ sortDirection: SortDirection;
20
+ };
21
+
22
+ const ROW_HEIGHT = 36;
23
+ const PAGE_SIZE = 50;
24
+ const POOL_PAGES = 4;
25
+ const CACHE_PADDING_PAGES = 2;
26
+ const RETAIN_PADDING_PAGES = 4;
27
+
28
+ export class VirtualTableController {
29
+ public rowHeight = ROW_HEIGHT;
30
+ public readonly pageSize = PAGE_SIZE;
31
+ public readonly poolSize = PAGE_SIZE * POOL_PAGES;
32
+ public readonly totalRows: number;
33
+
34
+ private scrollTop = 0;
35
+ private viewportHeight = 520;
36
+ private sortKey: SortKey = 'id';
37
+ private sortDirection: SortDirection = 'asc';
38
+ private spacerHeight: number;
39
+ private rowsInView = Math.max(1, Math.ceil(this.viewportHeight / this.rowHeight));
40
+ private visibleRows: RowView[] = [];
41
+ private snapshot: VirtualTableSnapshot;
42
+
43
+ private readonly listeners = new Set<() => void>();
44
+ private readonly pool = Array.from({ length: PAGE_SIZE * POOL_PAGES }, () =>
45
+ createRowModel(),
46
+ );
47
+ private readonly dataByIndex = new Map<number, RowData>();
48
+ private readonly loadedPages = new Set<number>();
49
+ private readonly pageLoading = new Map<number, Promise<void>>();
50
+ private readonly dataService = new VirtualTableDataService();
51
+
52
+ public constructor() {
53
+ this.totalRows = this.dataService.totalRows;
54
+ this.spacerHeight = this.totalRows * this.rowHeight;
55
+ this.snapshot = {
56
+ visibleRows: this.visibleRows,
57
+ totalRows: this.totalRows,
58
+ poolSize: this.poolSize,
59
+ rowsInView: this.rowsInView,
60
+ loadedPageCount: this.loadedPages.size,
61
+ spacerHeight: this.spacerHeight,
62
+ sortKey: this.sortKey,
63
+ sortDirection: this.sortDirection,
64
+ };
65
+ this.refresh();
66
+ }
67
+
68
+ public subscribe = (listener: () => void): (() => void) => {
69
+ this.listeners.add(listener);
70
+ return () => {
71
+ this.listeners.delete(listener);
72
+ };
73
+ };
74
+
75
+ public getSnapshot = (): VirtualTableSnapshot => {
76
+ return this.snapshot;
77
+ };
78
+
79
+ public setViewportHeight(height: number): void {
80
+ this.viewportHeight = height;
81
+ this.rowsInView = Math.max(1, Math.ceil(this.viewportHeight / this.rowHeight));
82
+ this.refresh();
83
+ }
84
+
85
+ public setRowHeight(height: number): void {
86
+ if (this.rowHeight === height) {
87
+ return;
88
+ }
89
+
90
+ this.rowHeight = height;
91
+ this.spacerHeight = this.totalRows * this.rowHeight;
92
+ this.rowsInView = Math.max(1, Math.ceil(this.viewportHeight / this.rowHeight));
93
+ this.refresh();
94
+ }
95
+
96
+ public setScrollTop(value: number): void {
97
+ this.scrollTop = value;
98
+ this.refresh();
99
+ }
100
+
101
+ public toggleSort(nextKey: SortKey): void {
102
+ if (this.sortKey === nextKey) {
103
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
104
+ } else {
105
+ this.sortKey = nextKey;
106
+ this.sortDirection = 'asc';
107
+ }
108
+
109
+ this.resetLoadedData();
110
+ this.refresh();
111
+ }
112
+
113
+ private refresh(): void {
114
+ const scrollIndex = Math.floor(this.scrollTop / this.rowHeight);
115
+ const bufferTop = Math.max(0, Math.floor((this.poolSize - this.rowsInView) / 2));
116
+ const maxStart = Math.max(0, this.totalRows - this.poolSize);
117
+ const startIndex = Math.min(Math.max(scrollIndex - bufferTop, 0), maxStart);
118
+ const endIndex = Math.min(startIndex + this.poolSize, this.totalRows);
119
+ const startPage = Math.max(
120
+ 0,
121
+ Math.floor(startIndex / this.pageSize) - CACHE_PADDING_PAGES,
122
+ );
123
+ const endPage = Math.min(
124
+ Math.floor((endIndex - 1) / this.pageSize) + CACHE_PADDING_PAGES,
125
+ Math.floor((this.totalRows - 1) / this.pageSize),
126
+ );
127
+
128
+ this.ensurePages(startPage, endPage);
129
+ this.pruneCachedPages(startPage, endPage);
130
+
131
+ const nextRows: RowView[] = [];
132
+ const length = endIndex - startIndex;
133
+
134
+ for (let offset = 0; offset < length; offset += 1) {
135
+ const index = startIndex + offset;
136
+ const target = this.pool[offset];
137
+ updateRowModel(target, this.getRowData(index));
138
+
139
+ nextRows.push({
140
+ index,
141
+ top: index * this.rowHeight,
142
+ row: target,
143
+ });
144
+ }
145
+
146
+ this.visibleRows = nextRows;
147
+ this.snapshot = {
148
+ visibleRows: this.visibleRows,
149
+ totalRows: this.totalRows,
150
+ poolSize: this.poolSize,
151
+ rowsInView: this.rowsInView,
152
+ loadedPageCount: this.loadedPages.size,
153
+ spacerHeight: this.spacerHeight,
154
+ sortKey: this.sortKey,
155
+ sortDirection: this.sortDirection,
156
+ };
157
+ this.emit();
158
+ }
159
+
160
+ private emit(): void {
161
+ for (const listener of this.listeners) {
162
+ listener();
163
+ }
164
+ }
165
+
166
+ private ensurePages(startPage: number, endPage: number): void {
167
+ for (let pageIndex = startPage; pageIndex <= endPage; pageIndex += 1) {
168
+ this.ensurePageLoaded(pageIndex);
169
+ }
170
+ }
171
+
172
+ private ensurePageLoaded(pageIndex: number): void {
173
+ if (this.loadedPages.has(pageIndex) || this.pageLoading.has(pageIndex)) {
174
+ return;
175
+ }
176
+
177
+ const task = this.loadPageAsync(pageIndex).finally(() => {
178
+ this.pageLoading.delete(pageIndex);
179
+ this.loadedPages.add(pageIndex);
180
+ this.refresh();
181
+ });
182
+
183
+ this.pageLoading.set(pageIndex, task);
184
+ }
185
+
186
+ private async loadPageAsync(pageIndex: number): Promise<void> {
187
+ const page = await this.dataService.fetchPage(
188
+ pageIndex,
189
+ this.pageSize,
190
+ this.sortKey,
191
+ this.sortDirection,
192
+ );
193
+ const startIndex = pageIndex * this.pageSize;
194
+
195
+ for (let offset = 0; offset < page.items.length; offset += 1) {
196
+ const item = page.items[offset];
197
+ if (!item) {
198
+ continue;
199
+ }
200
+
201
+ this.dataByIndex.set(startIndex + offset, item);
202
+ }
203
+ }
204
+
205
+ private getRowData(index: number): RowData {
206
+ const cached = this.dataByIndex.get(index);
207
+ if (cached) {
208
+ return cached;
209
+ }
210
+
211
+ return {
212
+ id: index + 1,
213
+ name: 'Loading...',
214
+ price: 0,
215
+ quantity: 0,
216
+ category: 'Pending',
217
+ updatedAt: '--',
218
+ };
219
+ }
220
+
221
+ private resetLoadedData(): void {
222
+ this.dataByIndex.clear();
223
+ this.loadedPages.clear();
224
+ this.pageLoading.clear();
225
+ }
226
+
227
+ private pruneCachedPages(startPage: number, endPage: number): void {
228
+ const minPage = Math.max(0, startPage - RETAIN_PADDING_PAGES);
229
+ const maxPage = Math.min(
230
+ Math.floor((this.totalRows - 1) / this.pageSize),
231
+ endPage + RETAIN_PADDING_PAGES,
232
+ );
233
+
234
+ for (const pageIndex of Array.from(this.loadedPages)) {
235
+ if (pageIndex >= minPage && pageIndex <= maxPage) {
236
+ continue;
237
+ }
238
+
239
+ this.loadedPages.delete(pageIndex);
240
+ const pageStart = pageIndex * this.pageSize;
241
+ const pageEnd = Math.min(pageStart + this.pageSize, this.totalRows);
242
+ for (let index = pageStart; index < pageEnd; index += 1) {
243
+ this.dataByIndex.delete(index);
244
+ }
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ createRowData,
3
+ type RowData,
4
+ type SortDirection,
5
+ type SortKey,
6
+ } from '@/lib/row-data';
7
+
8
+ export type VirtualTablePage = {
9
+ total: number;
10
+ items: RowData[];
11
+ };
12
+
13
+ const TOTAL_ROWS = 1_000_000;
14
+ const REQUEST_DELAY_MS = 120;
15
+ const CATEGORY_COUNT = 4;
16
+ const PRICE_BUCKET_COUNT = 1_000;
17
+ const QUANTITY_BUCKET_COUNT = 100;
18
+ const MAX_CACHED_PAGES = 24;
19
+
20
+ export class VirtualTableDataService {
21
+ private readonly pageCache = new Map<string, VirtualTablePage>();
22
+
23
+ public get totalRows(): number {
24
+ return TOTAL_ROWS;
25
+ }
26
+
27
+ public async fetchPage(
28
+ pageIndex: number,
29
+ pageSize: number,
30
+ sortKey: SortKey,
31
+ sortDirection: SortDirection,
32
+ ): Promise<VirtualTablePage> {
33
+ const cacheKey = `${sortKey}:${sortDirection}:${pageIndex}:${pageSize}`;
34
+ const cached = this.pageCache.get(cacheKey);
35
+ if (cached) {
36
+ return cached;
37
+ }
38
+
39
+ await this.delay(REQUEST_DELAY_MS + (pageIndex % 5) * 35);
40
+
41
+ const startIndex = pageIndex * pageSize;
42
+ const items: RowData[] = [];
43
+ const endIndex = Math.min(startIndex + pageSize, TOTAL_ROWS);
44
+
45
+ for (let visualIndex = startIndex; visualIndex < endIndex; visualIndex += 1) {
46
+ const id = this.getIdAtVisualIndex(visualIndex, sortKey, sortDirection);
47
+ items.push(createRowData(id));
48
+ }
49
+
50
+ const page = { total: TOTAL_ROWS, items };
51
+ this.pageCache.set(cacheKey, page);
52
+ this.trimCache();
53
+ return page;
54
+ }
55
+
56
+ private getIdAtVisualIndex(
57
+ visualIndex: number,
58
+ sortKey: SortKey,
59
+ sortDirection: SortDirection,
60
+ ): number {
61
+ const normalizedIndex =
62
+ sortDirection === 'asc' ? visualIndex : TOTAL_ROWS - 1 - visualIndex;
63
+
64
+ if (sortKey === 'price') {
65
+ return this.getPriceSortedId(normalizedIndex);
66
+ }
67
+
68
+ if (sortKey === 'quantity') {
69
+ return this.getQuantitySortedId(normalizedIndex);
70
+ }
71
+
72
+ if (sortKey === 'category') {
73
+ return this.getCategorySortedId(normalizedIndex);
74
+ }
75
+
76
+ return normalizedIndex + 1;
77
+ }
78
+
79
+ private getPriceSortedId(visualIndex: number): number {
80
+ const groupSize = TOTAL_ROWS / PRICE_BUCKET_COUNT;
81
+ const priceBucket = Math.floor(visualIndex / groupSize);
82
+ const offsetInBucket = visualIndex % groupSize;
83
+
84
+ return priceBucket + offsetInBucket * PRICE_BUCKET_COUNT + 1;
85
+ }
86
+
87
+ private getQuantitySortedId(visualIndex: number): number {
88
+ const groupSize = TOTAL_ROWS / QUANTITY_BUCKET_COUNT;
89
+ const quantityBucket = Math.floor(visualIndex / groupSize);
90
+ const offsetInBucket = visualIndex % groupSize;
91
+ const quantityStride = PRICE_BUCKET_COUNT * QUANTITY_BUCKET_COUNT;
92
+ const quantityBlock = Math.floor(offsetInBucket / PRICE_BUCKET_COUNT);
93
+ const priceBucket = offsetInBucket % PRICE_BUCKET_COUNT;
94
+
95
+ return (
96
+ priceBucket +
97
+ quantityBucket * PRICE_BUCKET_COUNT +
98
+ quantityBlock * quantityStride +
99
+ 1
100
+ );
101
+ }
102
+
103
+ private getCategorySortedId(visualIndex: number): number {
104
+ const groupSize = TOTAL_ROWS / CATEGORY_COUNT;
105
+ const categoryBucket = Math.floor(visualIndex / groupSize);
106
+ const offsetInBucket = visualIndex % groupSize;
107
+
108
+ return categoryBucket + offsetInBucket * CATEGORY_COUNT + 1;
109
+ }
110
+
111
+ private delay(durationMs: number): Promise<void> {
112
+ return new Promise((resolve) => {
113
+ window.setTimeout(resolve, durationMs);
114
+ });
115
+ }
116
+
117
+ private trimCache(): void {
118
+ while (this.pageCache.size > MAX_CACHED_PAGES) {
119
+ const oldestKey = this.pageCache.keys().next().value as string | undefined;
120
+ if (!oldestKey) {
121
+ return;
122
+ }
123
+ this.pageCache.delete(oldestKey);
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,27 @@
1
+ # rsx-vue-example
2
+
3
+ Website & docs: https://www.rsxjs.com/
4
+
5
+ This starter shows how to use RS-X in a Vue 3 application with a million-row
6
+ virtual table that keeps rendering and expression memory bounded.
7
+
8
+ ## Scripts
9
+
10
+ - `npm run dev` runs the RS-X build step and starts Vite
11
+ - `npm run build` generates RS-X artifacts and builds the production app
12
+ - `npm run preview` previews the production build
13
+
14
+ ## Structure
15
+
16
+ - `src/App.vue` contains the app shell and theme toggle
17
+ - `src/components/` contains UI components
18
+ - `src/composables/` contains reusable Vue composables
19
+ - `src/lib/` contains RS-X bootstrap and virtual-table state/data utilities
20
+ - `src/env.d.ts` declares Vue SFC modules for the RS-X build/typecheck pass
21
+
22
+ ## Notes
23
+
24
+ - The demo defaults to dark mode.
25
+ - It uses the `useRsxExpression` composable from `@rs-x/vue`.
26
+ - The generated RS-X cache files in `src/rsx-generated` are created by
27
+ `npm run build:rsx`; they are not checked into the starter template.
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref, watch } from 'vue';
3
+
4
+ import VirtualTableShell from './components/VirtualTableShell.vue';
5
+
6
+ type ThemeMode = 'light' | 'dark';
7
+
8
+ const theme = ref<ThemeMode>('dark');
9
+
10
+ onMounted(() => {
11
+ const storedTheme = window.localStorage.getItem('rsx-theme');
12
+ if (storedTheme === 'light' || storedTheme === 'dark') {
13
+ theme.value = storedTheme;
14
+ }
15
+ });
16
+
17
+ watch(
18
+ theme,
19
+ (nextTheme) => {
20
+ document.documentElement.setAttribute('data-theme', nextTheme);
21
+ document.body.setAttribute('data-theme', nextTheme);
22
+ window.localStorage.setItem('rsx-theme', nextTheme);
23
+ },
24
+ { immediate: true },
25
+ );
26
+
27
+ function toggleTheme(): void {
28
+ theme.value = theme.value === 'dark' ? 'light' : 'dark';
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <main class="app-shell">
34
+ <section class="hero">
35
+ <div class="container">
36
+ <div class="heroGrid">
37
+ <div class="heroLeft">
38
+ <p class="app-eyebrow">RS-X Vue Demo</p>
39
+ <h1 class="hTitle">Virtual Table</h1>
40
+ <p class="hSubhead">
41
+ Million-row scrolling with a fixed RS-X expression pool.
42
+ </p>
43
+ <p class="hSub">
44
+ This demo keeps rendering bounded while streaming pages on demand,
45
+ so scrolling stays smooth without growing expression memory with the
46
+ dataset.
47
+ </p>
48
+
49
+ <div class="heroActions">
50
+ <a
51
+ class="btn btnGhost"
52
+ href="https://www.rsxjs.com/"
53
+ target="_blank"
54
+ rel="noreferrer"
55
+ >
56
+ rs-x
57
+ </a>
58
+ <button
59
+ type="button"
60
+ class="btn btnGhost theme-toggle"
61
+ :aria-label="`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`"
62
+ @click="toggleTheme"
63
+ >
64
+ {{ theme === 'dark' ? 'Light mode' : 'Dark mode' }}
65
+ </button>
66
+ </div>
67
+ </div>
68
+
69
+ <aside class="card heroNote">
70
+ <h2 class="cardTitle">What This Shows</h2>
71
+ <p class="cardText">
72
+ Only a small row-model pool stays alive while pages stream in around
73
+ the viewport. That means one million logical rows without one million
74
+ live bindings.
75
+ </p>
76
+ </aside>
77
+ </div>
78
+ </div>
79
+ </section>
80
+
81
+ <section class="section">
82
+ <div class="container">
83
+ <section class="app-panel card">
84
+ <VirtualTableShell />
85
+ </section>
86
+ </div>
87
+ </section>
88
+ </main>
89
+ </template>