@rs-x/cli 2.0.0-next.2 → 2.0.0-next.21

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 (66) hide show
  1. package/README.md +5 -0
  2. package/bin/rsx.cjs +2868 -595
  3. package/package.json +5 -1
  4. package/{rs-x-vscode-extension-2.0.0-next.2.vsix → rs-x-vscode-extension-2.0.0-next.21.vsix} +0 -0
  5. package/scripts/prepare-local-rsx-packages.sh +20 -0
  6. package/scripts/verify-rsx-cli-mutations.sh +296 -0
  7. package/scripts/verify-rsx-projects.sh +220 -0
  8. package/scripts/verify-rsx-setup.sh +190 -0
  9. package/templates/angular-demo/README.md +115 -0
  10. package/templates/angular-demo/src/app/app.component.css +97 -0
  11. package/templates/angular-demo/src/app/app.component.html +58 -0
  12. package/templates/angular-demo/src/app/app.component.ts +52 -0
  13. package/templates/angular-demo/src/app/virtual-table/row-data.ts +35 -0
  14. package/templates/angular-demo/src/app/virtual-table/row-model.ts +45 -0
  15. package/templates/angular-demo/src/app/virtual-table/virtual-table-data.service.ts +136 -0
  16. package/templates/angular-demo/src/app/virtual-table/virtual-table-model.ts +224 -0
  17. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.css +174 -0
  18. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.html +50 -0
  19. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.ts +83 -0
  20. package/templates/angular-demo/src/index.html +11 -0
  21. package/templates/angular-demo/src/main.ts +16 -0
  22. package/templates/angular-demo/src/styles.css +261 -0
  23. package/templates/next-demo/README.md +26 -0
  24. package/templates/next-demo/app/globals.css +431 -0
  25. package/templates/next-demo/app/layout.tsx +22 -0
  26. package/templates/next-demo/app/page.tsx +5 -0
  27. package/templates/next-demo/components/demo-app.tsx +114 -0
  28. package/templates/next-demo/components/virtual-table-row.tsx +40 -0
  29. package/templates/next-demo/components/virtual-table-shell.tsx +86 -0
  30. package/templates/next-demo/hooks/use-virtual-table-controller.ts +26 -0
  31. package/templates/next-demo/hooks/use-virtual-table-viewport.ts +41 -0
  32. package/templates/next-demo/lib/row-data.ts +35 -0
  33. package/templates/next-demo/lib/row-model.ts +45 -0
  34. package/templates/next-demo/lib/rsx-bootstrap.ts +46 -0
  35. package/templates/next-demo/lib/virtual-table-controller.ts +259 -0
  36. package/templates/next-demo/lib/virtual-table-data.service.ts +132 -0
  37. package/templates/react-demo/README.md +113 -0
  38. package/templates/react-demo/index.html +12 -0
  39. package/templates/react-demo/src/app/app.tsx +87 -0
  40. package/templates/react-demo/src/app/hooks/use-virtual-table-controller.ts +24 -0
  41. package/templates/react-demo/src/app/hooks/use-virtual-table-viewport.ts +39 -0
  42. package/templates/react-demo/src/app/virtual-table/row-data.ts +35 -0
  43. package/templates/react-demo/src/app/virtual-table/row-model.ts +45 -0
  44. package/templates/react-demo/src/app/virtual-table/virtual-table-controller.ts +259 -0
  45. package/templates/react-demo/src/app/virtual-table/virtual-table-data.service.ts +132 -0
  46. package/templates/react-demo/src/app/virtual-table/virtual-table-row.tsx +38 -0
  47. package/templates/react-demo/src/app/virtual-table/virtual-table-shell.tsx +84 -0
  48. package/templates/react-demo/src/main.tsx +24 -0
  49. package/templates/react-demo/src/rsx-bootstrap.ts +48 -0
  50. package/templates/react-demo/src/styles.css +422 -0
  51. package/templates/react-demo/tsconfig.json +17 -0
  52. package/templates/react-demo/vite.config.ts +6 -0
  53. package/templates/vue-demo/README.md +27 -0
  54. package/templates/vue-demo/src/App.vue +89 -0
  55. package/templates/vue-demo/src/components/VirtualTableRow.vue +33 -0
  56. package/templates/vue-demo/src/components/VirtualTableShell.vue +71 -0
  57. package/templates/vue-demo/src/composables/use-virtual-table-controller.ts +33 -0
  58. package/templates/vue-demo/src/composables/use-virtual-table-viewport.ts +40 -0
  59. package/templates/vue-demo/src/env.d.ts +10 -0
  60. package/templates/vue-demo/src/lib/row-data.ts +35 -0
  61. package/templates/vue-demo/src/lib/row-model.ts +45 -0
  62. package/templates/vue-demo/src/lib/rsx-bootstrap.ts +46 -0
  63. package/templates/vue-demo/src/lib/virtual-table-controller.ts +259 -0
  64. package/templates/vue-demo/src/lib/virtual-table-data.service.ts +132 -0
  65. package/templates/vue-demo/src/main.ts +13 -0
  66. package/templates/vue-demo/src/style.css +440 -0
@@ -0,0 +1,422 @@
1
+ :root {
2
+ --font-sans:
3
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
4
+ sans-serif;
5
+ --font-mono:
6
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Courier New',
7
+ monospace;
8
+ --bg: #f6f8fc;
9
+ --surface: rgba(255, 255, 255, 0.86);
10
+ --surface-2: rgba(255, 255, 255, 0.72);
11
+ --surface-solid: #ffffff;
12
+ --text: #0b1324;
13
+ --muted: rgba(11, 19, 36, 0.66);
14
+ --border: rgba(10, 25, 55, 0.14);
15
+ --border-soft: rgba(10, 25, 55, 0.1);
16
+ --brand: #0b66ff;
17
+ --brand-2: #2bb6a9;
18
+ --focus: rgba(11, 102, 255, 0.35);
19
+ --shadow-1:
20
+ 0 1px 2px rgba(16, 24, 40, 0.06), 0 12px 34px rgba(16, 24, 40, 0.1);
21
+ --shadow-2:
22
+ 0 2px 10px rgba(16, 24, 40, 0.08), 0 18px 52px rgba(16, 24, 40, 0.12);
23
+ --page-glow-a: rgba(11, 102, 255, 0.06);
24
+ --page-glow-b: rgba(43, 182, 169, 0.05);
25
+ --hero-section-bg: linear-gradient(
26
+ 180deg,
27
+ rgba(255, 255, 255, 0.72),
28
+ rgba(255, 255, 255, 0.42)
29
+ );
30
+ --r-xl: 1.75rem;
31
+ --sp-2: 0.5rem;
32
+ --sp-3: 0.75rem;
33
+ --sp-4: 1rem;
34
+ --sp-5: 1.25rem;
35
+ --sp-6: 1.5rem;
36
+ --sp-7: 2rem;
37
+ --sp-8: 2.5rem;
38
+ --sp-9: 3rem;
39
+ --sp-10: 4rem;
40
+ --container: 1120px;
41
+ }
42
+
43
+ html[data-theme='dark'],
44
+ body[data-theme='dark'] {
45
+ --bg: #070b14;
46
+ --surface: rgba(12, 18, 35, 0.82);
47
+ --surface-2: rgba(12, 18, 35, 0.68);
48
+ --surface-solid: #0c1223;
49
+ --text: #edf2ff;
50
+ --muted: rgba(237, 242, 255, 0.7);
51
+ --border: rgba(237, 242, 255, 0.16);
52
+ --border-soft: rgba(237, 242, 255, 0.12);
53
+ --brand: #7ab6ff;
54
+ --brand-2: #49d9c8;
55
+ --focus: rgba(122, 182, 255, 0.35);
56
+ --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.22), 0 12px 34px rgba(0, 0, 0, 0.32);
57
+ --shadow-2: 0 2px 10px rgba(0, 0, 0, 0.24), 0 18px 52px rgba(0, 0, 0, 0.34);
58
+ --page-glow-a: rgba(122, 182, 255, 0.1);
59
+ --page-glow-b: rgba(73, 217, 200, 0.08);
60
+ --hero-section-bg: linear-gradient(
61
+ 180deg,
62
+ rgba(12, 18, 35, 0.56),
63
+ rgba(12, 18, 35, 0.24)
64
+ );
65
+ }
66
+
67
+ *,
68
+ *::before,
69
+ *::after {
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ html,
74
+ body,
75
+ #root {
76
+ min-height: 100%;
77
+ }
78
+
79
+ html {
80
+ color-scheme: dark;
81
+ background: var(--bg);
82
+ }
83
+
84
+ html[data-theme='light'] {
85
+ color-scheme: light;
86
+ }
87
+
88
+ body {
89
+ margin: 0;
90
+ font-family: var(--font-sans);
91
+ color: var(--text);
92
+ background-color: var(--bg);
93
+ background:
94
+ radial-gradient(circle at top, var(--page-glow-a), transparent 32%),
95
+ radial-gradient(circle at 85% 12%, var(--page-glow-b), transparent 28%),
96
+ var(--bg);
97
+ transition:
98
+ background 180ms ease,
99
+ color 180ms ease;
100
+ }
101
+
102
+ button,
103
+ input,
104
+ select,
105
+ textarea {
106
+ font: inherit;
107
+ }
108
+
109
+ .container {
110
+ max-width: var(--container);
111
+ margin: 0 auto;
112
+ padding: 0 var(--sp-6);
113
+ }
114
+
115
+ .app-shell {
116
+ min-height: 100vh;
117
+ }
118
+
119
+ .hero {
120
+ position: relative;
121
+ padding: calc(var(--sp-10) + 1rem) 0 calc(var(--sp-8) + 0.5rem);
122
+ background: var(--hero-section-bg);
123
+ }
124
+
125
+ .heroGrid {
126
+ display: grid;
127
+ grid-template-columns: minmax(540px, 1.05fr) minmax(300px, 0.95fr);
128
+ column-gap: var(--sp-6);
129
+ row-gap: var(--sp-8);
130
+ align-items: center;
131
+ }
132
+
133
+ .heroLeft {
134
+ min-width: 0;
135
+ padding-top: var(--sp-4);
136
+ }
137
+
138
+ .app-eyebrow {
139
+ margin: 0 0 var(--sp-4);
140
+ letter-spacing: 0.2em;
141
+ text-transform: uppercase;
142
+ font-size: 0.82rem;
143
+ font-weight: 700;
144
+ color: var(--brand);
145
+ }
146
+
147
+ .hTitle {
148
+ margin: 0 0 var(--sp-5);
149
+ font-size: clamp(3.6rem, 5.2vw, 4.8rem);
150
+ line-height: 0.96;
151
+ letter-spacing: -0.052em;
152
+ }
153
+
154
+ .hSubhead {
155
+ margin: 0 0 var(--sp-2);
156
+ font-size: clamp(1.42rem, 2vw, 1.82rem);
157
+ font-weight: 640;
158
+ letter-spacing: -0.03em;
159
+ line-height: 1.08;
160
+ }
161
+
162
+ .hSub {
163
+ margin: var(--sp-5) 0 0;
164
+ color: var(--muted);
165
+ font-size: 1.1rem;
166
+ line-height: 1.72;
167
+ max-width: 31.5rem;
168
+ }
169
+
170
+ .heroActions {
171
+ display: flex;
172
+ flex-wrap: wrap;
173
+ gap: var(--sp-3);
174
+ margin-top: var(--sp-5);
175
+ }
176
+
177
+ .btn {
178
+ display: inline-flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ gap: var(--sp-2);
182
+ padding: 0.82rem 1.18rem;
183
+ border-radius: 14px;
184
+ border: 1px solid transparent;
185
+ font-weight: 800;
186
+ line-height: 1;
187
+ white-space: nowrap;
188
+ transition:
189
+ transform 160ms ease,
190
+ background 240ms ease,
191
+ border-color 240ms ease,
192
+ box-shadow 240ms ease;
193
+ }
194
+
195
+ .btn:active {
196
+ transform: translateY(1px);
197
+ }
198
+
199
+ .btnGhost {
200
+ background: var(--surface-2);
201
+ color: var(--text);
202
+ border-color: var(--border);
203
+ box-shadow: none;
204
+ text-decoration: none;
205
+ }
206
+
207
+ .btnGhost:hover {
208
+ box-shadow: var(--shadow-1);
209
+ }
210
+
211
+ .card {
212
+ position: relative;
213
+ min-width: 0;
214
+ border: 1px solid var(--border-soft);
215
+ background: linear-gradient(180deg, var(--surface), var(--surface-2));
216
+ border-radius: var(--r-xl);
217
+ padding: var(--sp-6);
218
+ box-shadow: var(--shadow-1);
219
+ display: flex;
220
+ flex-direction: column;
221
+ }
222
+
223
+ .cardTitle {
224
+ margin: 0 0 var(--sp-3);
225
+ font-weight: 700;
226
+ font-size: clamp(1.12rem, 0.96vw, 1.22rem);
227
+ letter-spacing: -0.02em;
228
+ }
229
+
230
+ .cardText {
231
+ margin: 0;
232
+ color: var(--muted);
233
+ line-height: 1.62;
234
+ }
235
+
236
+ .heroNote {
237
+ align-self: center;
238
+ margin-top: 0;
239
+ }
240
+
241
+ .section {
242
+ padding: var(--sp-7) 0 var(--sp-10);
243
+ }
244
+
245
+ .app-panel {
246
+ backdrop-filter: blur(12px);
247
+ }
248
+
249
+ .theme-toggle {
250
+ cursor: pointer;
251
+ }
252
+
253
+ .table-toolbar {
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: space-between;
257
+ gap: 16px;
258
+ padding-bottom: 16px;
259
+ border-bottom: 1px solid var(--border-soft);
260
+ }
261
+
262
+ .toolbar-left h2 {
263
+ margin: 0;
264
+ font-size: 20px;
265
+ }
266
+
267
+ .toolbar-left p {
268
+ margin: 4px 0 0;
269
+ color: var(--muted);
270
+ font-size: 13px;
271
+ }
272
+
273
+ .toolbar-right {
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ gap: 8px;
277
+ }
278
+
279
+ .toolbar-right button {
280
+ border: 1px solid var(--border);
281
+ background: color-mix(in srgb, var(--surface-solid) 88%, var(--brand) 12%);
282
+ color: var(--text);
283
+ padding: 6px 12px;
284
+ border-radius: 999px;
285
+ font-size: 12px;
286
+ cursor: pointer;
287
+ }
288
+
289
+ .toolbar-right button:hover {
290
+ border-color: var(--focus);
291
+ }
292
+
293
+ .table-header {
294
+ display: grid;
295
+ grid-template-columns: 80px 1.4fr 1fr 0.8fr 0.6fr 1fr 0.9fr;
296
+ gap: 8px;
297
+ padding: 12px 8px;
298
+ font-size: 12px;
299
+ font-weight: 600;
300
+ color: var(--muted);
301
+ text-transform: uppercase;
302
+ letter-spacing: 0.06em;
303
+ }
304
+
305
+ .table-viewport {
306
+ position: relative;
307
+ height: 520px;
308
+ overflow: auto;
309
+ border: 1px solid var(--border-soft);
310
+ border-radius: 12px;
311
+ background: color-mix(in srgb, var(--surface-solid) 94%, transparent);
312
+ }
313
+
314
+ .table-spacer {
315
+ width: 100%;
316
+ }
317
+
318
+ .table-row {
319
+ position: absolute;
320
+ top: 0;
321
+ left: 0;
322
+ right: 0;
323
+ height: 36px;
324
+ display: grid;
325
+ grid-template-columns: 80px 1.4fr 1fr 0.8fr 0.6fr 1fr 0.9fr;
326
+ align-items: center;
327
+ gap: 8px;
328
+ padding: 0 8px;
329
+ border-bottom: 1px solid var(--border-soft);
330
+ font-size: 13px;
331
+ }
332
+
333
+ .table-row:nth-of-type(odd) {
334
+ background: color-mix(in srgb, var(--surface-solid) 84%, transparent);
335
+ }
336
+
337
+ .table-row .total {
338
+ font-weight: 600;
339
+ color: var(--brand);
340
+ }
341
+
342
+ .table-footer {
343
+ display: flex;
344
+ justify-content: space-between;
345
+ margin-top: 12px;
346
+ font-size: 12px;
347
+ color: var(--muted);
348
+ }
349
+
350
+ @media (max-width: 920px) {
351
+ .heroGrid {
352
+ grid-template-columns: 1fr;
353
+ }
354
+ }
355
+
356
+ @media (max-width: 900px) {
357
+ .table-header,
358
+ .table-row {
359
+ grid-template-columns: 72px 1.3fr 1fr 0.8fr 0.7fr 1fr;
360
+ }
361
+
362
+ .table-header span:last-child,
363
+ .table-row span:last-child {
364
+ display: none;
365
+ }
366
+ }
367
+
368
+ @media (max-width: 720px) {
369
+ .container {
370
+ padding: 0 var(--sp-4);
371
+ }
372
+
373
+ .hero {
374
+ padding: calc(var(--sp-8) + 0.5rem) 0 var(--sp-8);
375
+ }
376
+
377
+ .hTitle {
378
+ font-size: clamp(2.9rem, 14vw, 3.6rem);
379
+ }
380
+
381
+ .table-toolbar,
382
+ .table-footer {
383
+ flex-direction: column;
384
+ align-items: flex-start;
385
+ }
386
+
387
+ .table-header {
388
+ display: none;
389
+ }
390
+
391
+ .table-viewport {
392
+ height: 460px;
393
+ }
394
+
395
+ .table-row {
396
+ height: 168px;
397
+ grid-template-columns: repeat(2, minmax(0, 1fr));
398
+ align-content: start;
399
+ gap: 10px 16px;
400
+ padding: 14px 16px;
401
+ border: 1px solid var(--border-soft);
402
+ border-radius: 18px;
403
+ margin: 0 8px;
404
+ }
405
+
406
+ .table-row span {
407
+ display: flex;
408
+ flex-direction: column;
409
+ gap: 4px;
410
+ min-width: 0;
411
+ font-size: 0.95rem;
412
+ }
413
+
414
+ .table-row span::before {
415
+ content: attr(data-label);
416
+ color: var(--muted);
417
+ font-size: 0.72rem;
418
+ font-weight: 700;
419
+ letter-spacing: 0.06em;
420
+ text-transform: uppercase;
421
+ }
422
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "baseUrl": "./",
10
+ "plugins": [
11
+ {
12
+ "name": "@rs-x/typescript-plugin"
13
+ }
14
+ ]
15
+ },
16
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"]
17
+ }
@@ -0,0 +1,6 @@
1
+ import react from '@vitejs/plugin-react';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });
@@ -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
46
+ the 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
73
+ around the viewport. That means one million logical rows without
74
+ one million 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>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+
4
+ import { useRsxExpression } from '@rs-x/vue';
5
+
6
+ import type { RowView } from '../lib/virtual-table-controller';
7
+
8
+ const props = defineProps<{ item: RowView }>();
9
+
10
+ const style = computed(() => ({
11
+ transform: `translateY(${props.item.top}px)`,
12
+ }));
13
+
14
+ const id = useRsxExpression(props.item.row.idExpr);
15
+ const name = useRsxExpression(props.item.row.nameExpr);
16
+ const category = useRsxExpression(props.item.row.categoryExpr);
17
+ const price = useRsxExpression(props.item.row.priceExpr);
18
+ const quantity = useRsxExpression(props.item.row.quantityExpr);
19
+ const total = useRsxExpression(props.item.row.totalExpr);
20
+ const updatedAt = useRsxExpression(props.item.row.updatedAtExpr);
21
+ </script>
22
+
23
+ <template>
24
+ <div class="table-row" :style="style">
25
+ <span data-label="ID">#{{ id ?? 0 }}</span>
26
+ <span data-label="Name">{{ name ?? '' }}</span>
27
+ <span data-label="Category">{{ category ?? '' }}</span>
28
+ <span data-label="Price">€{{ price ?? 0 }}</span>
29
+ <span data-label="Qty">{{ quantity ?? 0 }}</span>
30
+ <span data-label="Total" class="total">€{{ total ?? 0 }}</span>
31
+ <span data-label="Updated">{{ updatedAt ?? '--' }}</span>
32
+ </div>
33
+ </template>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ import { computed, useTemplateRef } from 'vue';
3
+
4
+ import { useVirtualTableController } from '../composables/use-virtual-table-controller';
5
+ import { useVirtualTableViewport } from '../composables/use-virtual-table-viewport';
6
+ import VirtualTableRow from './VirtualTableRow.vue';
7
+
8
+ const { controller, snapshot } = useVirtualTableController();
9
+ const viewport = useTemplateRef<HTMLDivElement>('viewport');
10
+ useVirtualTableViewport(controller, viewport);
11
+
12
+ const visibleRows = computed(() => snapshot.value.visibleRows);
13
+ </script>
14
+
15
+ <template>
16
+ <section class="table-toolbar">
17
+ <div class="toolbar-left">
18
+ <h2>Inventory Snapshot</h2>
19
+ <p>
20
+ {{ snapshot.totalRows }} rows • {{ snapshot.poolSize }} pre-wired models
21
+ </p>
22
+ </div>
23
+ <div class="toolbar-right">
24
+ <button type="button" @click="controller.toggleSort('price')">
25
+ Sort by price
26
+ </button>
27
+ <button type="button" @click="controller.toggleSort('quantity')">
28
+ Sort by stock
29
+ </button>
30
+ <button type="button" @click="controller.toggleSort('name')">
31
+ Sort by name
32
+ </button>
33
+ </div>
34
+ </section>
35
+
36
+ <div class="table-header">
37
+ <span>ID</span>
38
+ <span>Name</span>
39
+ <span>Category</span>
40
+ <span>Price</span>
41
+ <span>Qty</span>
42
+ <span>Total</span>
43
+ <span>Updated</span>
44
+ </div>
45
+
46
+ <div
47
+ ref="viewport"
48
+ class="table-viewport"
49
+ @scroll="
50
+ controller.setScrollTop(($event.target as HTMLDivElement).scrollTop)
51
+ "
52
+ >
53
+ <div
54
+ class="table-spacer"
55
+ :style="{ height: `${snapshot.spacerHeight}px` }"
56
+ />
57
+ <VirtualTableRow
58
+ v-for="item in visibleRows"
59
+ :key="item.index"
60
+ :item="item"
61
+ />
62
+ </div>
63
+
64
+ <div class="table-footer">
65
+ <div>
66
+ Rows in view: {{ snapshot.rowsInView }} • Loaded pages:
67
+ {{ snapshot.loadedPageCount }}
68
+ </div>
69
+ <div>Scroll to stream pages from a 1,000,000-row virtual dataset.</div>
70
+ </div>
71
+ </template>
@@ -0,0 +1,33 @@
1
+ import {
2
+ getCurrentScope,
3
+ onScopeDispose,
4
+ type ShallowRef,
5
+ shallowRef,
6
+ } from 'vue';
7
+
8
+ import {
9
+ VirtualTableController,
10
+ type VirtualTableSnapshot,
11
+ } from '../lib/virtual-table-controller';
12
+
13
+ export function useVirtualTableController(): {
14
+ controller: VirtualTableController;
15
+ snapshot: ShallowRef<VirtualTableSnapshot>;
16
+ } {
17
+ const controller = new VirtualTableController();
18
+ const snapshot = shallowRef(controller.getSnapshot());
19
+ const unsubscribe = controller.subscribe(() => {
20
+ snapshot.value = controller.getSnapshot();
21
+ });
22
+
23
+ if (getCurrentScope()) {
24
+ onScopeDispose(() => {
25
+ unsubscribe();
26
+ });
27
+ }
28
+
29
+ return {
30
+ controller,
31
+ snapshot,
32
+ };
33
+ }