@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.
- package/bin/rsx.cjs +337 -7
- package/package.json +1 -1
- package/{rs-x-vscode-extension-2.0.0-next.15.vsix → rs-x-vscode-extension-2.0.0-next.17.vsix} +0 -0
- package/templates/next-demo/README.md +26 -0
- package/templates/next-demo/app/globals.css +431 -0
- package/templates/next-demo/app/layout.tsx +22 -0
- package/templates/next-demo/app/page.tsx +5 -0
- package/templates/next-demo/components/demo-app.tsx +114 -0
- package/templates/next-demo/components/virtual-table-row.tsx +40 -0
- package/templates/next-demo/components/virtual-table-shell.tsx +86 -0
- package/templates/next-demo/hooks/use-virtual-table-controller.ts +26 -0
- package/templates/next-demo/hooks/use-virtual-table-viewport.ts +41 -0
- package/templates/next-demo/lib/row-data.ts +35 -0
- package/templates/next-demo/lib/row-model.ts +45 -0
- package/templates/next-demo/lib/rsx-bootstrap.ts +46 -0
- package/templates/next-demo/lib/virtual-table-controller.ts +247 -0
- package/templates/next-demo/lib/virtual-table-data.service.ts +126 -0
- package/templates/vue-demo/README.md +27 -0
- package/templates/vue-demo/src/App.vue +89 -0
- package/templates/vue-demo/src/components/VirtualTableRow.vue +33 -0
- package/templates/vue-demo/src/components/VirtualTableShell.vue +58 -0
- package/templates/vue-demo/src/composables/use-virtual-table-controller.ts +33 -0
- package/templates/vue-demo/src/composables/use-virtual-table-viewport.ts +40 -0
- package/templates/vue-demo/src/env.d.ts +6 -0
- package/templates/vue-demo/src/lib/row-data.ts +35 -0
- package/templates/vue-demo/src/lib/row-model.ts +45 -0
- package/templates/vue-demo/src/lib/rsx-bootstrap.ts +46 -0
- package/templates/vue-demo/src/lib/virtual-table-controller.ts +247 -0
- package/templates/vue-demo/src/lib/virtual-table-data.service.ts +126 -0
- package/templates/vue-demo/src/main.ts +12 -0
- package/templates/vue-demo/src/style.css +440 -0
|
@@ -0,0 +1,431 @@
|
|
|
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
|
+
.table-loading {
|
|
351
|
+
display: grid;
|
|
352
|
+
place-items: center;
|
|
353
|
+
min-height: 520px;
|
|
354
|
+
color: var(--muted);
|
|
355
|
+
font-size: 1rem;
|
|
356
|
+
letter-spacing: 0.03em;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@media (max-width: 920px) {
|
|
360
|
+
.heroGrid {
|
|
361
|
+
grid-template-columns: 1fr;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@media (max-width: 900px) {
|
|
366
|
+
.table-header,
|
|
367
|
+
.table-row {
|
|
368
|
+
grid-template-columns: 72px 1.3fr 1fr 0.8fr 0.7fr 1fr;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.table-header span:last-child,
|
|
372
|
+
.table-row span:last-child {
|
|
373
|
+
display: none;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@media (max-width: 720px) {
|
|
378
|
+
.container {
|
|
379
|
+
padding: 0 var(--sp-4);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.hero {
|
|
383
|
+
padding: calc(var(--sp-8) + 0.5rem) 0 var(--sp-8);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.hTitle {
|
|
387
|
+
font-size: clamp(2.9rem, 14vw, 3.6rem);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.table-toolbar,
|
|
391
|
+
.table-footer {
|
|
392
|
+
flex-direction: column;
|
|
393
|
+
align-items: flex-start;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.table-header {
|
|
397
|
+
display: none;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.table-viewport {
|
|
401
|
+
height: 460px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.table-row {
|
|
405
|
+
height: 168px;
|
|
406
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
407
|
+
align-content: start;
|
|
408
|
+
gap: 10px 16px;
|
|
409
|
+
padding: 14px 16px;
|
|
410
|
+
border: 1px solid var(--border-soft);
|
|
411
|
+
border-radius: 18px;
|
|
412
|
+
margin: 0 8px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.table-row span {
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-direction: column;
|
|
418
|
+
gap: 4px;
|
|
419
|
+
min-width: 0;
|
|
420
|
+
font-size: 0.95rem;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.table-row span::before {
|
|
424
|
+
content: attr(data-label);
|
|
425
|
+
color: var(--muted);
|
|
426
|
+
font-size: 0.72rem;
|
|
427
|
+
font-weight: 700;
|
|
428
|
+
letter-spacing: 0.06em;
|
|
429
|
+
text-transform: uppercase;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import './globals.css';
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: 'RS-X Next.js Demo',
|
|
8
|
+
description:
|
|
9
|
+
'Million-row virtual scrolling with a fixed RS-X expression pool in Next.js.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type RootLayoutProps = Readonly<{
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
export default function RootLayout({ children }: RootLayoutProps) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" data-theme="dark">
|
|
19
|
+
<body data-theme="dark">{children}</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type FC, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { initRsx } from '@/lib/rsx-bootstrap';
|
|
6
|
+
|
|
7
|
+
import { VirtualTableShell } from './virtual-table-shell';
|
|
8
|
+
|
|
9
|
+
type ThemeMode = 'light' | 'dark';
|
|
10
|
+
|
|
11
|
+
function getInitialTheme(): ThemeMode {
|
|
12
|
+
if (typeof window === 'undefined') {
|
|
13
|
+
return 'dark';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const storedTheme = window.localStorage.getItem('rsx-theme');
|
|
17
|
+
if (storedTheme === 'light' || storedTheme === 'dark') {
|
|
18
|
+
return storedTheme;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return 'dark';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DemoApp: FC = () => {
|
|
25
|
+
const [theme, setTheme] = useState<ThemeMode>(() => getInitialTheme());
|
|
26
|
+
const [ready, setReady] = useState(false);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let active = true;
|
|
30
|
+
|
|
31
|
+
void initRsx().then(() => {
|
|
32
|
+
if (active) {
|
|
33
|
+
setReady(true);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
active = false;
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
44
|
+
document.body.setAttribute('data-theme', theme);
|
|
45
|
+
window.localStorage.setItem('rsx-theme', theme);
|
|
46
|
+
}, [theme]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<main className="app-shell">
|
|
50
|
+
<section className="hero">
|
|
51
|
+
<div className="container">
|
|
52
|
+
<div className="heroGrid">
|
|
53
|
+
<div className="heroLeft">
|
|
54
|
+
<p className="app-eyebrow">RS-X React Demo</p>
|
|
55
|
+
<h1 className="hTitle">Virtual Table</h1>
|
|
56
|
+
<p className="hSubhead">
|
|
57
|
+
Million-row scrolling with a fixed RS-X expression pool.
|
|
58
|
+
</p>
|
|
59
|
+
<p className="hSub">
|
|
60
|
+
This demo keeps rendering bounded while streaming pages on demand,
|
|
61
|
+
so scrolling stays smooth without growing expression memory with the
|
|
62
|
+
dataset.
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<div className="heroActions">
|
|
66
|
+
<a
|
|
67
|
+
className="btn btnGhost"
|
|
68
|
+
href="https://www.rsxjs.com/"
|
|
69
|
+
target="_blank"
|
|
70
|
+
rel="noreferrer"
|
|
71
|
+
>
|
|
72
|
+
rs-x
|
|
73
|
+
</a>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="btn btnGhost theme-toggle"
|
|
77
|
+
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
setTheme((currentTheme) =>
|
|
80
|
+
currentTheme === 'dark' ? 'light' : 'dark',
|
|
81
|
+
);
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<aside className="card heroNote">
|
|
90
|
+
<h2 className="cardTitle">What This Shows</h2>
|
|
91
|
+
<p className="cardText">
|
|
92
|
+
Only a small row-model pool stays alive while pages stream in
|
|
93
|
+
around the viewport. That means one million logical rows without
|
|
94
|
+
one million live bindings.
|
|
95
|
+
</p>
|
|
96
|
+
</aside>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
|
|
101
|
+
<section className="section">
|
|
102
|
+
<div className="container">
|
|
103
|
+
<section className="app-panel card">
|
|
104
|
+
{ready ? (
|
|
105
|
+
<VirtualTableShell />
|
|
106
|
+
) : (
|
|
107
|
+
<div className="table-loading">Initializing RS-X…</div>
|
|
108
|
+
)}
|
|
109
|
+
</section>
|
|
110
|
+
</div>
|
|
111
|
+
</section>
|
|
112
|
+
</main>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type FC, memo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useRsxExpression } from '@rs-x/react';
|
|
6
|
+
|
|
7
|
+
import type { RowView } from '@/lib/virtual-table-controller';
|
|
8
|
+
|
|
9
|
+
type VirtualTableRowProps = {
|
|
10
|
+
item: RowView;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const VirtualTableRowComponent: FC<VirtualTableRowProps> = ({ item }) => {
|
|
14
|
+
const id = useRsxExpression(item.row.idExpr);
|
|
15
|
+
const name = useRsxExpression(item.row.nameExpr);
|
|
16
|
+
const category = useRsxExpression(item.row.categoryExpr);
|
|
17
|
+
const price = useRsxExpression(item.row.priceExpr);
|
|
18
|
+
const quantity = useRsxExpression(item.row.quantityExpr);
|
|
19
|
+
const total = useRsxExpression(item.row.totalExpr);
|
|
20
|
+
const updatedAt = useRsxExpression(item.row.updatedAtExpr);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className="table-row"
|
|
25
|
+
style={{ transform: `translateY(${item.top}px)` }}
|
|
26
|
+
>
|
|
27
|
+
<span data-label="ID">#{id ?? 0}</span>
|
|
28
|
+
<span data-label="Name">{name ?? ''}</span>
|
|
29
|
+
<span data-label="Category">{category ?? ''}</span>
|
|
30
|
+
<span data-label="Price">€{price ?? 0}</span>
|
|
31
|
+
<span data-label="Qty">{quantity ?? 0}</span>
|
|
32
|
+
<span data-label="Total" className="total">
|
|
33
|
+
€{total ?? 0}
|
|
34
|
+
</span>
|
|
35
|
+
<span data-label="Updated">{updatedAt ?? '--'}</span>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const VirtualTableRow = memo(VirtualTableRowComponent);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type FC } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useVirtualTableController } from '@/hooks/use-virtual-table-controller';
|
|
6
|
+
import { useVirtualTableViewport } from '@/hooks/use-virtual-table-viewport';
|
|
7
|
+
|
|
8
|
+
import { VirtualTableRow } from './virtual-table-row';
|
|
9
|
+
|
|
10
|
+
export const VirtualTableShell: FC = () => {
|
|
11
|
+
const { controller, snapshot } = useVirtualTableController();
|
|
12
|
+
const viewportRef = useVirtualTableViewport(controller);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<section className="table-toolbar">
|
|
17
|
+
<div className="toolbar-left">
|
|
18
|
+
<h2>Inventory Snapshot</h2>
|
|
19
|
+
<p>
|
|
20
|
+
{snapshot.totalRows} rows • {snapshot.poolSize} pre-wired models
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
<div className="toolbar-right">
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={() => {
|
|
27
|
+
controller.toggleSort('price');
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
Sort by price
|
|
31
|
+
</button>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={() => {
|
|
35
|
+
controller.toggleSort('quantity');
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
Sort by stock
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => {
|
|
43
|
+
controller.toggleSort('name');
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
Sort by name
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<div className="table-header">
|
|
52
|
+
<span>ID</span>
|
|
53
|
+
<span>Name</span>
|
|
54
|
+
<span>Category</span>
|
|
55
|
+
<span>Price</span>
|
|
56
|
+
<span>Qty</span>
|
|
57
|
+
<span>Total</span>
|
|
58
|
+
<span>Updated</span>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
ref={viewportRef}
|
|
63
|
+
className="table-viewport"
|
|
64
|
+
onScroll={(event) => {
|
|
65
|
+
controller.setScrollTop(event.currentTarget.scrollTop);
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className="table-spacer"
|
|
70
|
+
style={{ height: `${snapshot.spacerHeight}px` }}
|
|
71
|
+
/>
|
|
72
|
+
{snapshot.visibleRows.map((item) => (
|
|
73
|
+
<VirtualTableRow key={item.index} item={item} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="table-footer">
|
|
78
|
+
<div>
|
|
79
|
+
Rows in view: {snapshot.rowsInView} • Loaded pages:{' '}
|
|
80
|
+
{snapshot.loadedPageCount}
|
|
81
|
+
</div>
|
|
82
|
+
<div>Scroll to stream pages from a 1,000,000-row virtual dataset.</div>
|
|
83
|
+
</div>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
};
|