@rs-x/cli 2.0.0-next.16 → 2.0.0-next.18

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 CHANGED
@@ -27,6 +27,12 @@ const REACT_DEMO_TEMPLATE_DIR = path.join(
27
27
  'templates',
28
28
  'react-demo',
29
29
  );
30
+ const VUE_DEMO_TEMPLATE_DIR = path.join(
31
+ __dirname,
32
+ '..',
33
+ 'templates',
34
+ 'vue-demo',
35
+ );
30
36
  const NEXT_DEMO_TEMPLATE_DIR = path.join(
31
37
  __dirname,
32
38
  '..',
@@ -669,6 +675,77 @@ function writeFileWithDryRun(filePath, content, dryRun) {
669
675
  fs.writeFileSync(filePath, content, 'utf8');
670
676
  }
671
677
 
678
+ function stripJsonComments(content) {
679
+ let result = '';
680
+ let inString = false;
681
+ let stringDelimiter = '"';
682
+ let inLineComment = false;
683
+ let inBlockComment = false;
684
+
685
+ for (let index = 0; index < content.length; index += 1) {
686
+ const current = content[index];
687
+ const next = content[index + 1];
688
+
689
+ if (inLineComment) {
690
+ if (current === '\n') {
691
+ inLineComment = false;
692
+ result += current;
693
+ }
694
+ continue;
695
+ }
696
+
697
+ if (inBlockComment) {
698
+ if (current === '*' && next === '/') {
699
+ inBlockComment = false;
700
+ index += 1;
701
+ }
702
+ continue;
703
+ }
704
+
705
+ if (inString) {
706
+ result += current;
707
+ if (current === '\\') {
708
+ index += 1;
709
+ if (index < content.length) {
710
+ result += content[index];
711
+ }
712
+ continue;
713
+ }
714
+ if (current === stringDelimiter) {
715
+ inString = false;
716
+ }
717
+ continue;
718
+ }
719
+
720
+ if (current === '"' || current === "'") {
721
+ inString = true;
722
+ stringDelimiter = current;
723
+ result += current;
724
+ continue;
725
+ }
726
+
727
+ if (current === '/' && next === '/') {
728
+ inLineComment = true;
729
+ index += 1;
730
+ continue;
731
+ }
732
+
733
+ if (current === '/' && next === '*') {
734
+ inBlockComment = true;
735
+ index += 1;
736
+ continue;
737
+ }
738
+
739
+ result += current;
740
+ }
741
+
742
+ return result;
743
+ }
744
+
745
+ function parseJsonc(content) {
746
+ return JSON.parse(stripJsonComments(content.replace(/^\uFEFF/u, '')));
747
+ }
748
+
672
749
  function copyPathWithDryRun(sourcePath, targetPath, dryRun) {
673
750
  if (dryRun) {
674
751
  logInfo(`[dry-run] copy ${sourcePath} -> ${targetPath}`);
@@ -720,7 +797,7 @@ function upsertTypescriptPluginInTsConfig(configPath, dryRun) {
720
797
  return;
721
798
  }
722
799
 
723
- const tsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
800
+ const tsConfig = parseJsonc(fs.readFileSync(configPath, 'utf8'));
724
801
  const compilerOptions = tsConfig.compilerOptions ?? {};
725
802
  const plugins = Array.isArray(compilerOptions.plugins)
726
803
  ? compilerOptions.plugins
@@ -748,6 +825,26 @@ function upsertTypescriptPluginInTsConfig(configPath, dryRun) {
748
825
  fs.writeFileSync(configPath, `${JSON.stringify(tsConfig, null, 2)}\n`, 'utf8');
749
826
  }
750
827
 
828
+ function ensureTsConfigIncludePattern(configPath, pattern, dryRun) {
829
+ if (!fs.existsSync(configPath)) {
830
+ return;
831
+ }
832
+
833
+ const tsConfig = parseJsonc(fs.readFileSync(configPath, 'utf8'));
834
+ const include = Array.isArray(tsConfig.include) ? tsConfig.include : [];
835
+ if (!include.includes(pattern)) {
836
+ include.push(pattern);
837
+ }
838
+ tsConfig.include = include;
839
+
840
+ if (dryRun) {
841
+ logInfo(`[dry-run] patch ${configPath}`);
842
+ return;
843
+ }
844
+
845
+ fs.writeFileSync(configPath, `${JSON.stringify(tsConfig, null, 2)}\n`, 'utf8');
846
+ }
847
+
751
848
  function toFileDependencySpec(fromDir, targetPath) {
752
849
  const relative = path.relative(fromDir, targetPath).replace(/\\/gu, '/');
753
850
  const normalized = relative.startsWith('.') ? relative : `./${relative}`;
@@ -798,6 +895,7 @@ function resolveProjectRsxSpecs(
798
895
  ) {
799
896
  const includeAngularPackage = Boolean(options.includeAngularPackage);
800
897
  const includeReactPackage = Boolean(options.includeReactPackage);
898
+ const includeVuePackage = Boolean(options.includeVuePackage);
801
899
  const versionSpec = options.tag ? options.tag : RSX_PACKAGE_VERSION;
802
900
  const defaults = {
803
901
  '@rs-x/core': versionSpec,
@@ -807,6 +905,7 @@ function resolveProjectRsxSpecs(
807
905
  '@rs-x/typescript-plugin': versionSpec,
808
906
  ...(includeAngularPackage ? { '@rs-x/angular': versionSpec } : {}),
809
907
  ...(includeReactPackage ? { '@rs-x/react': versionSpec } : {}),
908
+ ...(includeVuePackage ? { '@rs-x/vue': versionSpec } : {}),
810
909
  '@rs-x/cli': versionSpec,
811
910
  };
812
911
 
@@ -818,6 +917,7 @@ function resolveProjectRsxSpecs(
818
917
  '@rs-x/typescript-plugin': 'rs-x-typescript-plugin',
819
918
  ...(includeAngularPackage ? { '@rs-x/angular': 'rs-x-angular' } : {}),
820
919
  ...(includeReactPackage ? { '@rs-x/react': 'rs-x-react' } : {}),
920
+ ...(includeVuePackage ? { '@rs-x/vue': 'rs-x-vue' } : {}),
821
921
  '@rs-x/cli': 'rs-x-cli',
822
922
  };
823
923
 
@@ -845,6 +945,11 @@ function resolveProjectRsxSpecs(
845
945
  'rs-x-react': path.join(tarballsDir, 'rs-x-react'),
846
946
  }
847
947
  : {}),
948
+ ...(includeVuePackage
949
+ ? {
950
+ 'rs-x-vue': path.join(tarballsDir, 'rs-x-vue'),
951
+ }
952
+ : {}),
848
953
  'rs-x-cli': path.join(tarballsDir, 'rs-x-cli'),
849
954
  };
850
955
 
@@ -893,6 +998,11 @@ function resolveProjectRsxSpecs(
893
998
  '@rs-x/react': path.join(workspaceRoot, 'rs-x-react'),
894
999
  }
895
1000
  : {}),
1001
+ ...(includeVuePackage
1002
+ ? {
1003
+ '@rs-x/vue': path.join(workspaceRoot, 'rs-x-vue'),
1004
+ }
1005
+ : {}),
896
1006
  '@rs-x/cli': path.join(workspaceRoot, 'rs-x-cli'),
897
1007
  };
898
1008
 
@@ -1687,6 +1797,120 @@ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
1687
1797
  }
1688
1798
  }
1689
1799
 
1800
+ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
1801
+ const dryRun = Boolean(flags['dry-run']);
1802
+ const tag = resolveInstallTag(flags);
1803
+ const tarballsDir =
1804
+ typeof flags['tarballs-dir'] === 'string'
1805
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
1806
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
1807
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
1808
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
1809
+ : null;
1810
+ const workspaceRoot = findRepoRoot(projectRoot);
1811
+ const rsxSpecs = resolveProjectRsxSpecs(
1812
+ projectRoot,
1813
+ workspaceRoot,
1814
+ tarballsDir,
1815
+ { tag, includeVuePackage: true },
1816
+ );
1817
+
1818
+ const templateFiles = ['README.md', 'src'];
1819
+ for (const entry of templateFiles) {
1820
+ copyPathWithDryRun(
1821
+ path.join(VUE_DEMO_TEMPLATE_DIR, entry),
1822
+ path.join(projectRoot, entry),
1823
+ dryRun,
1824
+ );
1825
+ }
1826
+
1827
+ const staleVueFiles = [
1828
+ path.join(projectRoot, 'public'),
1829
+ path.join(projectRoot, 'src/components/HelloWorld.vue'),
1830
+ path.join(projectRoot, 'src/assets'),
1831
+ ];
1832
+ for (const stalePath of staleVueFiles) {
1833
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
1834
+ }
1835
+
1836
+ const readmePath = path.join(projectRoot, 'README.md');
1837
+ if (fs.existsSync(readmePath)) {
1838
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
1839
+ const nextReadme = readmeSource.replace(
1840
+ /^#\s+rsx-vue-example/mu,
1841
+ `# ${projectName}`,
1842
+ );
1843
+ if (dryRun) {
1844
+ logInfo(`[dry-run] patch ${readmePath}`);
1845
+ } else {
1846
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
1847
+ }
1848
+ }
1849
+
1850
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1851
+ if (!fs.existsSync(packageJsonPath)) {
1852
+ logError(`package.json not found in generated Vue app: ${packageJsonPath}`);
1853
+ process.exit(1);
1854
+ }
1855
+
1856
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1857
+ packageJson.name = projectName;
1858
+ packageJson.private = true;
1859
+ packageJson.version = '0.1.0';
1860
+ packageJson.type = 'module';
1861
+ packageJson.scripts = {
1862
+ 'build:rsx': 'rsx build --project tsconfig.app.json --no-emit --prod',
1863
+ 'typecheck:rsx': 'rsx typecheck --project tsconfig.app.json',
1864
+ dev: 'npm run build:rsx && vite',
1865
+ build: 'npm run build:rsx && vue-tsc -b && vite build',
1866
+ preview: 'vite preview',
1867
+ };
1868
+ packageJson.rsx = {
1869
+ build: {
1870
+ preparse: true,
1871
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1872
+ compiled: true,
1873
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1874
+ compiledResolvedEvaluator: false,
1875
+ },
1876
+ };
1877
+ packageJson.dependencies = {
1878
+ vue: packageJson.dependencies?.vue ?? '^3.5.30',
1879
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
1880
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
1881
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
1882
+ '@rs-x/vue': rsxSpecs['@rs-x/vue'],
1883
+ };
1884
+ packageJson.devDependencies = {
1885
+ ...(packageJson.devDependencies ?? {}),
1886
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
1887
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
1888
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
1889
+ };
1890
+
1891
+ if (dryRun) {
1892
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
1893
+ } else {
1894
+ fs.writeFileSync(
1895
+ packageJsonPath,
1896
+ `${JSON.stringify(packageJson, null, 2)}\n`,
1897
+ 'utf8',
1898
+ );
1899
+ }
1900
+
1901
+ const tsConfigAppPath = path.join(projectRoot, 'tsconfig.app.json');
1902
+ if (fs.existsSync(tsConfigAppPath)) {
1903
+ upsertTypescriptPluginInTsConfig(tsConfigAppPath, dryRun);
1904
+ ensureTsConfigIncludePattern(tsConfigAppPath, 'src/**/*.d.ts', dryRun);
1905
+ }
1906
+
1907
+ if (!Boolean(flags['skip-install'])) {
1908
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Vue starter...`);
1909
+ run(pm, ['install'], { dryRun });
1910
+ logOk('Vue starter dependencies are up to date.');
1911
+ }
1912
+ }
1913
+
1690
1914
  function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
1691
1915
  const dryRun = Boolean(flags['dry-run']);
1692
1916
  const tag = resolveInstallTag(flags);
@@ -1836,11 +2060,7 @@ async function runProjectWithTemplate(template, flags) {
1836
2060
  return;
1837
2061
  }
1838
2062
  if (normalizedTemplate === 'vuejs') {
1839
- runSetupVue({
1840
- ...flags,
1841
- entry: flags.entry ?? 'src/main.ts',
1842
- });
1843
- applyVueRsxTemplate(projectRoot, dryRun);
2063
+ applyVueDemoStarter(projectRoot, projectName, pm, flags);
1844
2064
  }
1845
2065
  });
1846
2066
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rs-x/cli",
3
- "version": "2.0.0-next.16",
3
+ "version": "2.0.0-next.18",
4
4
  "description": "CLI for installing RS-X compiler tooling and VS Code integration",
5
5
  "bin": {
6
6
  "rsx": "./bin/rsx.cjs"
@@ -9,6 +9,10 @@ import { VirtualTableShell } from './virtual-table-shell';
9
9
  type ThemeMode = 'light' | 'dark';
10
10
 
11
11
  function getInitialTheme(): ThemeMode {
12
+ if (typeof window === 'undefined') {
13
+ return 'dark';
14
+ }
15
+
12
16
  const storedTheme = window.localStorage.getItem('rsx-theme');
13
17
  if (storedTheme === 'light' || storedTheme === 'dark') {
14
18
  return storedTheme;
@@ -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>
@@ -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,58 @@
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>{{ snapshot.totalRows }} rows • {{ snapshot.poolSize }} pre-wired models</p>
20
+ </div>
21
+ <div class="toolbar-right">
22
+ <button type="button" @click="controller.toggleSort('price')">Sort by price</button>
23
+ <button type="button" @click="controller.toggleSort('quantity')">Sort by stock</button>
24
+ <button type="button" @click="controller.toggleSort('name')">Sort by name</button>
25
+ </div>
26
+ </section>
27
+
28
+ <div class="table-header">
29
+ <span>ID</span>
30
+ <span>Name</span>
31
+ <span>Category</span>
32
+ <span>Price</span>
33
+ <span>Qty</span>
34
+ <span>Total</span>
35
+ <span>Updated</span>
36
+ </div>
37
+
38
+ <div
39
+ ref="viewport"
40
+ class="table-viewport"
41
+ @scroll="controller.setScrollTop(($event.target as HTMLDivElement).scrollTop)"
42
+ >
43
+ <div class="table-spacer" :style="{ height: `${snapshot.spacerHeight}px` }" />
44
+ <VirtualTableRow
45
+ v-for="item in visibleRows"
46
+ :key="item.index"
47
+ :item="item"
48
+ />
49
+ </div>
50
+
51
+ <div class="table-footer">
52
+ <div>
53
+ Rows in view: {{ snapshot.rowsInView }} • Loaded pages:
54
+ {{ snapshot.loadedPageCount }}
55
+ </div>
56
+ <div>Scroll to stream pages from a 1,000,000-row virtual dataset.</div>
57
+ </div>
58
+ </template>
@@ -0,0 +1,33 @@
1
+ import {
2
+ getCurrentScope,
3
+ onScopeDispose,
4
+ shallowRef,
5
+ type 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
+ }
@@ -0,0 +1,40 @@
1
+ import { getCurrentScope, onMounted, onScopeDispose, type Ref } from 'vue';
2
+
3
+ import type { VirtualTableController } from '../lib/virtual-table-controller';
4
+
5
+ const COMPACT_BREAKPOINT_PX = 720;
6
+ const DEFAULT_ROW_HEIGHT = 36;
7
+ const COMPACT_ROW_HEIGHT = 168;
8
+
9
+ export function useVirtualTableViewport(
10
+ controller: VirtualTableController,
11
+ viewportRef: Ref<HTMLDivElement | null>,
12
+ ): void {
13
+ let observer: ResizeObserver | undefined;
14
+
15
+ onMounted(() => {
16
+ const viewport = viewportRef.value;
17
+ if (!viewport) {
18
+ return;
19
+ }
20
+
21
+ const syncMetrics = (): void => {
22
+ controller.setViewportHeight(viewport.clientHeight);
23
+ controller.setRowHeight(
24
+ viewport.clientWidth <= COMPACT_BREAKPOINT_PX
25
+ ? COMPACT_ROW_HEIGHT
26
+ : DEFAULT_ROW_HEIGHT,
27
+ );
28
+ };
29
+
30
+ syncMetrics();
31
+ observer = new ResizeObserver(syncMetrics);
32
+ observer.observe(viewport);
33
+ });
34
+
35
+ if (getCurrentScope()) {
36
+ onScopeDispose(() => {
37
+ observer?.disconnect();
38
+ });
39
+ }
40
+ }
@@ -0,0 +1,6 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue';
3
+
4
+ const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
5
+ export default component;
6
+ }
@@ -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
+ }