diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx
index 8883611..2fc2583 100644
--- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx
+++ b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx
@@ -30,7 +30,10 @@ import {
import { WithAtom } from "@/components/with-atom"
import { backgroundTaskProgressAtom } from "@/dashboard/state"
import { DirectoryPageContext } from "@/directories/directory-page/context"
-import { DirectoryContentTable } from "@/directories/directory-page/directory-content-table"
+import {
+ DirectoryContentTableWrapper,
+ mockTableAtom,
+} from "@/directories/directory-page/directory-content-table-wrapper"
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
@@ -155,6 +158,7 @@ function RouteComponent() {
fileDragInfoAtom={fileDragInfoAtom}
/>
+ {import.meta.env.DEV && }
@@ -162,8 +166,8 @@ function RouteComponent() {
{/* DirectoryContentContextMenu must wrap div instead of DirectoryContentTable, otherwise radix will throw "event.preventDefault is not a function" error, idk why */}
-
-
+
)
}
+
+function MockTableToggle() {
+ // Always call the hook - mockTableAtom is always defined
+ // (it's a regular atom in production, but we only render in dev mode)
+ const [isEnabled, setIsEnabled] = useAtom(mockTableAtom)
+
+ if (!import.meta.env.DEV) {
+ return null
+ }
+
+ const handleToggle = () => {
+ setIsEnabled((prev) => !prev)
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/drive-web/src/vfs/api.ts b/apps/drive-web/src/vfs/api.ts
index d7b7f19..e1b17fa 100644
--- a/apps/drive-web/src/vfs/api.ts
+++ b/apps/drive-web/src/vfs/api.ts
@@ -115,10 +115,13 @@ export const directoryContentQueryAtom = atomFamily(
{ returns: DirectoryContentResponse },
).then(([_, result]) => result)
: Promise.reject(new Error("No account selected")),
- getNextPageParam: (lastPage, _pages, lastPageParam) => ({
- ...lastPageParam,
- cursor: lastPage.nextCursor ?? "",
- }),
+ getNextPageParam: (lastPage, _pages, lastPageParam) =>
+ lastPage.nextCursor
+ ? {
+ ...lastPageParam,
+ cursor: lastPage.nextCursor,
+ }
+ : null,
})
}),
(paramsA, paramsB) =>
diff --git a/bun.lock b/bun.lock
index a530d92..9182a2f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -49,6 +49,7 @@
"@tanstack/react-query": "^5.87.4",
"@tanstack/react-router": "^1.131.41",
"@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-virtual": "^3.13.13",
"@tanstack/router-devtools": "^1.131.42",
"arktype": "^2.1.28",
"better-auth": "1.3.8",
@@ -499,6 +500,8 @@
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
+ "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.13", "", { "dependencies": { "@tanstack/virtual-core": "3.13.13" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg=="],
+
"@tanstack/router-cli": ["@tanstack/router-cli@1.133.12", "", { "dependencies": { "@tanstack/router-generator": "1.133.12", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-5rBpY1yixbxtuLarXSTXK6mD2Wrluyqy9/LRS1k9o61dLiBi9L4HlYkkXkKtpvOXb4VhxlqgmSg2JwASYCi2ng=="],
"@tanstack/router-core": ["@tanstack/router-core@1.133.13", "", { "dependencies": { "@tanstack/history": "1.133.3", "@tanstack/store": "^0.7.0", "cookie-es": "^2.0.0", "seroval": "^1.3.2", "seroval-plugins": "^1.3.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-zZptdlS/wSkqozb07Y3zX5gas2OapJdjEG6/Id0e/twNefVdR4EY2TK/mgvyhHtKIpCxIcnZz/3opypgeQi9bg=="],
@@ -517,6 +520,8 @@
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
+ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.13", "", {}, "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA=="],
+
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.133.3", "", {}, "sha512-6d2AP9hAjEi8mcIew2RkxBX+wClH1xedhfaYhs8fUiX+V2Cedk7RBD9E9ww2z6BGUYD8Es4fS0OIrzXZWHKGhw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -859,7 +864,7 @@
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
- "@drexa/auth/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
+ "@drexa/auth/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@fileone/web/arktype": ["arktype@2.1.28", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.4" } }, "sha512-LVZqXl2zWRpNFnbITrtFmqeqNkPPo+KemuzbGSY6jvJwCb4v8NsDzrWOLHnQgWl26TkJeWWcUNUeBpq2Mst1/Q=="],
@@ -897,7 +902,7 @@
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
- "@drexa/auth/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
+ "@drexa/auth/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"@fileone/web/arktype/@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
diff --git a/docs/virtualization-testing.md b/docs/virtualization-testing.md
new file mode 100644
index 0000000..0a6cae4
--- /dev/null
+++ b/docs/virtualization-testing.md
@@ -0,0 +1,114 @@
+# Virtualization Testing Guide
+
+This guide explains how to test the virtualization of the `DirectoryContentTable` component with large datasets.
+
+## Quick Start
+
+The app includes a dev-mode-only toggle to switch between the real and mock table. No code changes needed!
+
+1. **Enable Dev Mode**: Make sure you're running in development mode (`bun dev` or `npm run dev`)
+
+2. **Toggle Mock Table**:
+ - Look for the "Mock: OFF/ON" button in the directory page header
+ - Click it to toggle between real and mock data
+ - The toggle state persists in localStorage
+
+3. **Adjust Test Parameters** (optional): Edit `apps/drive-web/src/directories/directory-page/mock-directory-content-table.tsx`:
+ ```tsx
+ const TOTAL_ITEMS = 10_000 // Total number of items to simulate
+ const ITEMS_PER_PAGE = 100 // Items per page (for pagination)
+ const NETWORK_DELAY_MS = 50 // Simulated network delay in milliseconds
+ ```
+
+## How It Works
+
+- The `DirectoryContentTableWrapper` component automatically handles switching between real and mock tables
+- Uses Jotai's `atomWithStorage` to persist the toggle state in localStorage
+- The mock table code is completely tree-shaken in production builds
+- No page reload needed - switching is instant
+
+## Test Configuration
+
+You can adjust these parameters in `mock-directory-content-table.tsx`:
+
+- **TOTAL_ITEMS**: Total number of items to simulate (default: 10,000)
+ - Try: 1,000 for quick tests, 50,000+ for stress testing
+
+- **ITEMS_PER_PAGE**: Number of items per page (default: 100)
+ - This simulates pagination behavior
+
+- **NETWORK_DELAY_MS**: Simulated network delay (default: 50ms)
+ - Set to 0 for instant loading, higher values to test loading states
+
+## What to Test
+
+1. **Scroll Performance**: Scroll through the list and verify smooth scrolling
+2. **Infinite Loading**: Scroll to the bottom and verify more items load automatically
+3. **Selection**: Select multiple items and verify performance
+4. **Virtualization Stats**: Check the browser console for virtualization metrics
+5. **Memory Usage**: Monitor browser DevTools to ensure memory stays reasonable
+
+## Debugging
+
+The mock table component includes:
+- A debug banner showing current stats (total items, rendered rows)
+- Console logging of virtualization metrics (check browser console)
+- Visual indicators for rendered vs total items
+
+## Implementation Details
+
+### Files
+
+- `apps/drive-web/src/directories/directory-page/directory-content-table-wrapper.tsx`
+ - Wrapper component that conditionally loads real or mock table
+ - Uses Jotai atoms for state management
+ - Exports `mockTableAtom` for toggle components
+
+- `apps/drive-web/src/directories/directory-page/mock-directory-content-table.tsx`
+ - Mock table component with all utilities inlined
+ - Generates mock data matching API behavior (directories first, then files)
+ - Includes virtualization stats logging
+
+### Utilities
+
+The `mock-directory-content-table.tsx` file includes all utilities inlined:
+
+- `generateMockDirectoryItems()`: Generate mock data
+- `createMockDirectoryContentQuery()`: Create a mock infinite query
+- `logVirtualizationStats()`: Log virtualization metrics
+
+All utilities are defined within the same file for convenience.
+
+## Example Test Scenarios
+
+### Small Dataset (1,000 items)
+```tsx
+const TOTAL_ITEMS = 1_000
+const ITEMS_PER_PAGE = 100
+```
+
+### Medium Dataset (10,000 items)
+```tsx
+const TOTAL_ITEMS = 10_000
+const ITEMS_PER_PAGE = 100
+```
+
+### Large Dataset (100,000 items)
+```tsx
+const TOTAL_ITEMS = 100_000
+const ITEMS_PER_PAGE = 200
+```
+
+### Stress Test (1,000,000 items)
+```tsx
+const TOTAL_ITEMS = 1_000_000
+const ITEMS_PER_PAGE = 500
+```
+
+## Production Safety
+
+- The mock table code is completely excluded from production builds
+- Vite's static analysis removes all dev-mode code paths
+- The toggle button only appears in development mode
+- No performance impact on production bundles
+