commit 8eeb48fcd3f8803e4a8d54b76a8afe4f5f3b47de Author: Luke Betteridge Date: Wed Apr 8 14:56:52 2026 +0100 Initial commit diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3fa7919 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,32 @@ +- [x] Verify that the copilot-instructions.md file in the .github directory is created. Summary: Created in .github/. + +- [x] Clarify Project Requirements + Summary: Building a React app in the current workspace with Vite and JavaScript. + +- [x] Scaffold the Project + Summary: Created the Vite React project structure manually in the current folder and added the required package, Vite, and entry files. + +- [x] Customize the Project + Summary: Built a Google Drive-inspired file browser with a folder tree, search, list and card views, a details panel, and a local-path settings flow backed by a Node filesystem API. + +- [x] Install Required Extensions + Summary: No extensions were required by the project setup information. + +- [x] Compile the Project + Summary: Installed npm dependencies, checked diagnostics, and verified a successful production build with npm run build. + +- [x] Create and Run Task + Summary: Skipped because the standard npm scripts already cover the development and build workflows. + +- [x] Launch the Project + Summary: Skipped automatic launch because the user did not request a debug session; npm run dev is available on request. + +- [x] Ensure Documentation is Complete + Summary: Added README.md and updated this file to reflect the completed project state without HTML comments. + +Project Notes + +- Stack: Vite + React + JavaScript. +- Main UI: src/App.jsx and src/index.css. +- Local API bridge: server/index.js and src/lib/localDriveApi.js. +- Demo fallback data: src/data/driveData.js. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc04894 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.DS_Store +*.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1938d28 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Driveboard + +Driveboard is a Vite + React app that presents a nested file structure with a Google Drive-inspired browsing experience. + +## Features + +- Browse a mock drive hierarchy from the sidebar tree or the main content area. +- Connect a local directory from the in-app Settings panel by entering a filesystem path. +- Search folders, files, file types, and locations across the whole active source. +- Switch between a compact list view and a card-based browsing view. +- Inspect the currently selected file or folder in the details panel. +- Use a responsive layout that works across desktop and mobile widths. + +## Scripts + +- `npm install` to install dependencies. +- `npm run dev` to start both the Vite frontend and the local filesystem API. +- `npm run build` to create a production build. +- `npm run preview` to preview the production build alongside the local filesystem API. +- `npm run start` to serve the built app and the local filesystem API from Node. + +## Notes + +- The sample fallback data still lives in `src/data/driveData.js`. +- Local filesystem access now runs through the bundled Node API in `server/index.js`. +- The selected directory path is stored in browser local storage and restored on the next launch when the local API is available. +- This avoids the browser upload flow because the app reads the directory from disk through the local backend instead of importing files into the page. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..77f3812 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Driveboard + + +
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..30a8030 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2907 @@ +{ + "name": "self-hosted-drive", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "self-hosted-drive", + "version": "0.1.0", + "dependencies": { + "express": "^5.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.0", + "concurrently": "^9.1.2", + "vite": "^7.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bf9b6fa --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "self-hosted-drive", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "concurrently \"npm:dev:server\" \"npm:dev:client\"", + "dev:client": "vite", + "dev:server": "node server/index.js", + "build": "vite build", + "preview": "concurrently \"npm:dev:server\" \"npm:preview:client\"", + "preview:client": "vite preview", + "start": "node server/index.js" + }, + "dependencies": { + "express": "^5.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.0", + "concurrently": "^9.1.2", + "vite": "^7.0.0" + } +} \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..a957246 --- /dev/null +++ b/server/index.js @@ -0,0 +1,255 @@ +import express from 'express'; +import { existsSync } from 'node:fs'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PORT = Number(process.env.PORT ?? 3001); +const MAX_ENTRIES = Number(process.env.DRIVEBOARD_MAX_ENTRIES ?? 12000); +const LOCAL_OWNER = 'Local file system'; + +const imageExtensions = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'avif']); +const videoExtensions = new Set(['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v']); +const pdfExtensions = new Set(['pdf']); +const archiveExtensions = new Set(['zip', 'rar', '7z', 'tar', 'gz']); +const spreadsheetExtensions = new Set(['xls', 'xlsx', 'csv', 'tsv', 'ods']); +const slideExtensions = new Set(['ppt', 'pptx', 'key']); +const documentExtensions = new Set(['doc', 'docx', 'txt', 'md', 'rtf', 'odt']); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, '..'); +const distPath = path.join(projectRoot, 'dist'); + +function compareDirents(left, right) { + if (left.isDirectory() !== right.isDirectory()) { + return left.isDirectory() ? -1 : 1; + } + + return left.name.localeCompare(right.name); +} + +function createItemId(kind, pathSegments) { + return `${kind}:${pathSegments.join('/')}`; +} + +function inferFileType(fileName) { + const extension = fileName.split('.').pop()?.toLowerCase() ?? ''; + + if (imageExtensions.has(extension)) { + return 'image'; + } + + if (videoExtensions.has(extension)) { + return 'video'; + } + + if (pdfExtensions.has(extension)) { + return 'pdf'; + } + + if (archiveExtensions.has(extension)) { + return 'zip'; + } + + if (spreadsheetExtensions.has(extension)) { + return 'sheet'; + } + + if (slideExtensions.has(extension)) { + return 'slides'; + } + + if (documentExtensions.has(extension)) { + return 'doc'; + } + + return 'file'; +} + +function formatBytes(bytes) { + if (!Number.isFinite(bytes) || bytes <= 0) { + return '0 B'; + } + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + + const digits = value >= 10 || unitIndex === 0 ? 0 : 1; + return `${value.toFixed(digits)} ${units[unitIndex]}`; +} + +function formatTimestamp(timestamp) { + if (!timestamp) { + return 'Unavailable'; + } + + return new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }).format(new Date(timestamp)); +} + +async function readFileNode(fullPath, pathSegments) { + const stat = await fs.stat(fullPath); + + return { + id: createItemId('file', pathSegments), + name: path.basename(fullPath), + kind: 'file', + type: inferFileType(path.basename(fullPath)), + owner: LOCAL_OWNER, + modified: formatTimestamp(stat.mtimeMs), + modifiedMs: stat.mtimeMs, + size: formatBytes(stat.size), + sizeBytes: stat.size, + }; +} + +async function readDirectoryNode(fullPath, pathSegments, state) { + const directoryStat = await fs.stat(fullPath); + const children = []; + let folderCount = 0; + let fileCount = 0; + let sizeBytes = 0; + let latestModified = directoryStat.mtimeMs ?? 0; + + const entries = await fs.readdir(fullPath, { withFileTypes: true }); + entries.sort(compareDirents); + + for (const entry of entries) { + if (state.entryCount >= state.maxEntries) { + state.truncated = true; + break; + } + + if (entry.isSymbolicLink()) { + continue; + } + + const entryFullPath = path.join(fullPath, entry.name); + const entryPathSegments = [...pathSegments, entry.name]; + state.entryCount += 1; + + try { + if (entry.isDirectory()) { + const childFolder = await readDirectoryNode(entryFullPath, entryPathSegments, state); + children.push(childFolder); + folderCount += childFolder.folderCount + 1; + fileCount += childFolder.fileCount; + sizeBytes += childFolder.sizeBytes ?? 0; + latestModified = Math.max(latestModified, childFolder.modifiedMs ?? 0); + continue; + } + + if (entry.isFile()) { + const fileNode = await readFileNode(entryFullPath, entryPathSegments); + children.push(fileNode); + fileCount += 1; + sizeBytes += fileNode.sizeBytes ?? 0; + latestModified = Math.max(latestModified, fileNode.modifiedMs ?? 0); + } + } catch (error) { + state.warnings.push(`Skipped ${entryFullPath}: ${error.message}`); + } + } + + return { + id: pathSegments.length ? createItemId('folder', pathSegments) : 'root', + name: pathSegments[pathSegments.length - 1] || path.basename(fullPath) || fullPath, + kind: 'folder', + owner: LOCAL_OWNER, + modified: formatTimestamp(latestModified), + modifiedMs: latestModified, + size: formatBytes(sizeBytes), + sizeBytes, + folderCount, + fileCount, + children, + }; +} + +async function scanLocalDirectory(directoryPath) { + const resolvedPath = path.resolve(directoryPath); + const stat = await fs.stat(resolvedPath); + + if (!stat.isDirectory()) { + const error = new Error('The provided path is not a directory.'); + error.statusCode = 400; + throw error; + } + + const state = { + entryCount: 0, + maxEntries: MAX_ENTRIES, + truncated: false, + warnings: [], + }; + + const tree = await readDirectoryNode(resolvedPath, [], state); + + return { + tree: { + ...tree, + id: 'root', + name: path.basename(resolvedPath) || resolvedPath, + }, + directoryPath: resolvedPath, + truncated: state.truncated, + scannedEntries: state.entryCount, + warnings: state.warnings.slice(0, 5), + }; +} + +const app = express(); +app.use(express.json({ limit: '1mb' })); + +app.get('/api/health', (_request, response) => { + response.json({ ok: true }); +}); + +app.post('/api/filesystem/scan', async (request, response) => { + const directoryPath = typeof request.body?.directoryPath === 'string' ? request.body.directoryPath.trim() : ''; + + if (!directoryPath) { + response.status(400).json({ error: 'Directory path is required.' }); + return; + } + + try { + const result = await scanLocalDirectory(directoryPath); + response.json(result); + } catch (error) { + if (error?.code === 'ENOENT') { + response.status(404).json({ error: `Directory not found: ${path.resolve(directoryPath)}` }); + return; + } + + if (error?.code === 'EACCES' || error?.code === 'EPERM') { + response.status(403).json({ error: `Permission denied for: ${path.resolve(directoryPath)}` }); + return; + } + + response.status(error?.statusCode ?? 500).json({ error: error?.message ?? 'Unable to scan the requested directory.' }); + } +}); + +if (existsSync(distPath)) { + app.use(express.static(distPath)); + app.get(/^(?!\/api).*/, (_request, response) => { + response.sendFile(path.join(distPath, 'index.html')); + }); +} + +app.listen(PORT, () => { + console.log(`Driveboard local filesystem API listening on http://127.0.0.1:${PORT}`); +}); \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..aae9287 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,1445 @@ +import { startTransition, useDeferredValue, useEffect, useMemo, useState } from 'react'; +import { driveData } from './data/driveData'; +import { formatBytes } from './lib/fileSystemAccess'; +import { + checkLocalApiAvailability, + clearSavedDirectoryPath, + loadSavedDirectoryPath, + saveDirectoryPath, + scanLocalDirectory, +} from './lib/localDriveApi'; + +const ROOT_ID = 'root'; +const DEMO_STORAGE_BYTES = 38.88 * 1024 * 1024 * 1024; +const STORAGE_CAPACITY_BYTES = 200 * 1024 * 1024 * 1024; + +const viewModes = [ + { id: 'list', label: 'List', Icon: ListIcon }, + { id: 'grid', label: 'Grid', Icon: GridIcon }, +]; + +const primaryNavItems = [ + { id: 'home', label: 'Home', Icon: HomeIcon }, + { id: 'drive', label: 'My Drive', Icon: DriveFolderIcon }, +]; + +const secondaryNavItems = [ + { id: 'shared', label: 'Shared with me', Icon: SharedIcon }, + { id: 'recent', label: 'Recent', Icon: RecentIcon }, + { id: 'starred', label: 'Starred', Icon: StarIcon }, + { id: 'spam', label: 'Spam', Icon: SpamIcon }, + { id: 'bin', label: 'Bin', Icon: TrashIcon }, +]; + +const filterChips = ['Type', 'People', 'Modified']; + +const typeLabels = { + doc: 'Document', + sheet: 'Spreadsheet', + slides: 'Presentation', + image: 'Image', + pdf: 'PDF', + video: 'Video', + zip: 'Archive', + file: 'File', +}; + +const previewCopy = { + folder: 'Browse the folder contents, inspect its structure, and jump deeper without leaving the current view.', + doc: 'A draft or reference document stored alongside the rest of the workspace.', + sheet: 'Structured data, planning, or tracking information in tabular form.', + slides: 'A presentation deck prepared for review, handoff, or sharing.', + image: 'A visual asset saved with the rest of the project materials.', + pdf: 'A fixed-format file ready to share or archive.', + video: 'A media file suitable for playback, review, or export.', + zip: 'A bundled archive containing exported or packaged files.', + file: 'A local file captured from the selected path.', +}; + +function defaultNotice(apiReady, apiChecked) { + if (!apiChecked) { + return { + tone: 'info', + title: 'Checking local bridge', + body: 'Looking for the bundled filesystem API before loading a local folder.', + }; + } + + if (!apiReady) { + return { + tone: 'warning', + title: 'Demo source active', + body: 'Start npm run dev or npm run start to browse a real local directory.', + }; + } + + return { + tone: 'info', + title: 'Sample data loaded', + body: 'Open Settings to replace the sample workspace with a local folder path.', + }; +} + +function findItemById(node, targetId) { + if (node.id === targetId) { + return node; + } + + for (const child of node.children ?? []) { + const found = findItemById(child, targetId); + if (found) { + return found; + } + } + + return null; +} + +function findPath(node, targetId, trail = []) { + const nextTrail = [...trail, node]; + if (node.id === targetId) { + return nextTrail; + } + + for (const child of node.children ?? []) { + const found = findPath(child, targetId, nextTrail); + if (found) { + return found; + } + } + + return null; +} + +function flattenItems(node, path = []) { + const nextPath = node.kind === 'folder' ? [...path, node] : path; + const entries = []; + + for (const child of node.children ?? []) { + entries.push({ item: child, path: nextPath }); + entries.push(...flattenItems(child, nextPath)); + } + + return entries; +} + +function sortEntries(entries) { + return [...entries].sort((left, right) => { + if (left.item.kind !== right.item.kind) { + return left.item.kind === 'folder' ? -1 : 1; + } + + return left.item.name.localeCompare(right.item.name); + }); +} + +function countDescendants(node) { + if (typeof node.folderCount === 'number' && typeof node.fileCount === 'number') { + return { folders: node.folderCount, files: node.fileCount }; + } + + return (node.children ?? []).reduce( + (totals, child) => { + if (child.kind === 'folder') { + const childTotals = countDescendants(child); + return { + folders: totals.folders + childTotals.folders + 1, + files: totals.files + childTotals.files, + }; + } + + return { + folders: totals.folders, + files: totals.files + 1, + }; + }, + { folders: 0, files: 0 }, + ); +} + +function typeLabel(type) { + return typeLabels[type] ?? 'File'; +} + +function getPreviewCopy(item) { + if (item.kind === 'folder') { + return previewCopy.folder; + } + + return previewCopy[item.type] ?? previewCopy.file; +} + +function getFootprint(item) { + if (item.kind !== 'folder') { + return item.size; + } + + const totals = countDescendants(item); + if (!totals.folders && !totals.files) { + return 'Empty folder'; + } + + return `${totals.folders} folders, ${totals.files} files`; +} + +function getTableSize(item) { + return item.kind === 'folder' ? '--' : item.size; +} + +function formatLocation(path) { + return path.map((item) => item.name).join(' / '); +} + +function getInitialExpandedFolders(tree) { + if (!tree) { + return new Set([ROOT_ID]); + } + + return new Set([ROOT_ID]); +} + +function getOwnerLabel(sourceMode, item) { + return sourceMode === 'local' ? 'Local' : item.owner; +} + +function getAvatarInitials(label) { + const cleaned = label.trim(); + if (!cleaned) { + return 'DR'; + } + + const parts = cleaned.split(/\s+/).filter(Boolean); + if (parts.length === 1) { + return parts[0].slice(0, 2).toUpperCase(); + } + + return `${parts[0][0] ?? ''}${parts[1][0] ?? ''}`.toUpperCase(); +} + +function App() { + const [driveTree, setDriveTree] = useState(driveData); + const [currentFolderId, setCurrentFolderId] = useState(ROOT_ID); + const [selectedItemId, setSelectedItemId] = useState(ROOT_ID); + const [query, setQuery] = useState(''); + const deferredQuery = useDeferredValue(query); + const [viewMode, setViewMode] = useState('list'); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const [detailsOpen, setDetailsOpen] = useState(false); + const [computersOpen, setComputersOpen] = useState(true); + const [expandedFolders, setExpandedFolders] = useState(() => getInitialExpandedFolders(driveData)); + const [sourceMode, setSourceMode] = useState('demo'); + const [sourceName, setSourceName] = useState('Sample workspace'); + const [sourcePath, setSourcePath] = useState(''); + const [draftPath, setDraftPath] = useState(''); + const [isScanning, setIsScanning] = useState(false); + const [apiReady, setApiReady] = useState(false); + const [apiChecked, setApiChecked] = useState(false); + const [sourceError, setSourceError] = useState(''); + const [connectionNotice, setConnectionNotice] = useState(() => defaultNotice(false, false)); + + function resetExplorerState(tree) { + setCurrentFolderId(ROOT_ID); + setSelectedItemId(ROOT_ID); + setQuery(''); + setDetailsOpen(false); + setExpandedFolders(getInitialExpandedFolders(tree)); + } + + function openSettingsPanel() { + setDetailsOpen(false); + setSettingsOpen(true); + } + + async function loadDirectoryFromPath(nextPath, options = {}) { + const { persist = true, closeSettings = true, skipReadyCheck = false } = options; + const trimmedPath = nextPath.trim(); + + if (!trimmedPath) { + setSourceError('Enter a directory path first.'); + setConnectionNotice({ + tone: 'warning', + title: 'Directory path required', + body: 'Enter a local directory path in Settings before trying to load the filesystem.', + }); + return; + } + + if (!apiReady && !skipReadyCheck) { + const message = 'The local filesystem API is not running. Start the app with npm run dev or npm run start.'; + setSourceError(message); + setConnectionNotice({ + tone: 'error', + title: 'Local API unavailable', + body: message, + }); + return; + } + + setIsScanning(true); + setSourceError(''); + setConnectionNotice({ + tone: 'info', + title: 'Scanning local path', + body: `Reading ${trimmedPath} from the local filesystem.`, + }); + + try { + const result = await scanLocalDirectory(trimmedPath); + const { tree, directoryPath, truncated, scannedEntries } = result; + + if (persist) { + saveDirectoryPath(directoryPath); + } + + startTransition(() => { + setDriveTree(tree); + setSourceMode('local'); + setSourceName(tree.name); + setSourcePath(directoryPath); + setDraftPath(directoryPath); + resetExplorerState(tree); + }); + + setConnectionNotice( + truncated + ? { + tone: 'warning', + title: 'Local path loaded with limits', + body: `Browsing ${directoryPath}. The scan stopped after ${scannedEntries} entries to keep the app responsive.`, + } + : { + tone: 'success', + title: 'Local path connected', + body: `Browsing ${directoryPath} directly from the local filesystem.`, + }, + ); + + if (closeSettings) { + setSettingsOpen(false); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unable to scan the local directory.'; + setSourceError(message); + setConnectionNotice({ + tone: 'error', + title: 'Local path failed', + body: message, + }); + } finally { + setIsScanning(false); + } + } + + async function refreshDirectory() { + if (!sourcePath || isScanning) { + return; + } + + await loadDirectoryFromPath(sourcePath, { persist: true, closeSettings: false }); + } + + function handleSettingsSubmit(event) { + event.preventDefault(); + void loadDirectoryFromPath(draftPath, { persist: true, closeSettings: true }); + } + + function useSampleData() { + setDriveTree(driveData); + setSourceMode('demo'); + setSourceName('Sample workspace'); + setSourcePath(''); + setDraftPath(''); + setSourceError(''); + resetExplorerState(driveData); + clearSavedDirectoryPath(); + setConnectionNotice(defaultNotice(apiReady, true)); + setSettingsOpen(false); + } + + useEffect(() => { + let cancelled = false; + + async function initializeLocalBridge() { + setConnectionNotice(defaultNotice(false, false)); + + const available = await checkLocalApiAvailability(); + if (cancelled) { + return; + } + + setApiReady(available); + setApiChecked(true); + + if (!available) { + setConnectionNotice(defaultNotice(false, true)); + return; + } + + const savedPath = loadSavedDirectoryPath(); + setDraftPath(savedPath); + + if (!savedPath) { + setConnectionNotice(defaultNotice(true, true)); + return; + } + + setIsScanning(true); + try { + const result = await scanLocalDirectory(savedPath); + if (cancelled) { + return; + } + + startTransition(() => { + setDriveTree(result.tree); + setSourceMode('local'); + setSourceName(result.tree.name); + setSourcePath(result.directoryPath); + setDraftPath(result.directoryPath); + resetExplorerState(result.tree); + }); + + setConnectionNotice( + result.truncated + ? { + tone: 'warning', + title: 'Saved path restored with limits', + body: `Browsing ${result.directoryPath}. The restored scan stopped after ${result.scannedEntries} entries to keep the app responsive.`, + } + : { + tone: 'success', + title: 'Saved path restored', + body: `Browsing ${result.directoryPath} directly from the local filesystem.`, + }, + ); + } catch (error) { + if (cancelled) { + return; + } + + const message = error instanceof Error ? error.message : 'Unable to restore the saved local directory.'; + setSourceError(message); + setConnectionNotice({ + tone: 'warning', + title: 'Saved path could not be restored', + body: message, + }); + } finally { + if (!cancelled) { + setIsScanning(false); + } + } + } + + void initializeLocalBridge(); + + return () => { + cancelled = true; + }; + }, []); + + const allEntries = useMemo(() => flattenItems(driveTree), [driveTree]); + const currentFolder = findItemById(driveTree, currentFolderId) ?? driveTree; + const breadcrumbs = findPath(driveTree, currentFolderId) ?? [driveTree]; + const selectedItem = findItemById(driveTree, selectedItemId) ?? currentFolder; + const selectedPath = findPath(driveTree, selectedItem.id) ?? [driveTree]; + const normalizedQuery = deferredQuery.trim().toLowerCase(); + + const searchResults = useMemo(() => { + if (!normalizedQuery) { + return []; + } + + return sortEntries( + allEntries.filter(({ item, path }) => { + const searchable = [ + item.name, + getOwnerLabel(sourceMode, item), + item.kind === 'folder' ? 'folder' : item.type, + path.map((entry) => entry.name).join(' '), + ] + .join(' ') + .toLowerCase(); + + return searchable.includes(normalizedQuery); + }), + ); + }, [allEntries, normalizedQuery, sourceMode]); + + const folderEntries = useMemo(() => { + const children = currentFolder.children ?? []; + return sortEntries(children.map((item) => ({ item, path: breadcrumbs }))); + }, [breadcrumbs, currentFolder]); + + const rootFolders = useMemo( + () => + sortEntries((driveTree.children ?? []).map((item) => ({ item, path: [driveTree] }))) + .filter(({ item }) => item.kind === 'folder') + .map(({ item }) => item), + [driveTree], + ); + + const visibleEntries = normalizedQuery ? searchResults : folderEntries; + const currentFoldersCount = folderEntries.filter(({ item }) => item.kind === 'folder').length; + const currentFilesCount = folderEntries.length - currentFoldersCount; + const rootTotals = useMemo(() => countDescendants(driveTree), [driveTree]); + const storageBytes = sourceMode === 'local' ? driveTree.sizeBytes ?? 0 : DEMO_STORAGE_BYTES; + const storagePercent = Math.max(6, Math.min(100, (storageBytes / STORAGE_CAPACITY_BYTES) * 100)); + const storageLabel = sourceMode === 'local' ? formatBytes(storageBytes) : '38.88 GB'; + const boardTitle = normalizedQuery ? 'Search in Drive' : currentFolder.name; + const boardSummary = normalizedQuery + ? `${visibleEntries.length} result${visibleEntries.length === 1 ? '' : 's'} for "${query.trim()}"` + : `${currentFoldersCount} folder${currentFoldersCount === 1 ? '' : 's'}, ${currentFilesCount} file${currentFilesCount === 1 ? '' : 's'}`; + const activeSourceTag = sourceMode === 'local' ? 'Local path' : 'Sample data'; + + function openFolder(folderId) { + const folder = findItemById(driveTree, folderId); + if (!folder || folder.kind !== 'folder') { + return; + } + + setCurrentFolderId(folderId); + setSelectedItemId(folderId); + setQuery(''); + setSidebarOpen(false); + setExpandedFolders((previous) => { + const next = new Set(previous); + const path = findPath(driveTree, folderId) ?? []; + path.forEach((entry) => { + if (entry.kind === 'folder') { + next.add(entry.id); + } + }); + return next; + }); + } + + function inspectItem(itemId) { + setSelectedItemId(itemId); + setDetailsOpen(true); + } + + function activateEntry(item) { + setSelectedItemId(item.id); + + if (item.kind === 'folder') { + openFolder(item.id); + return; + } + + setDetailsOpen(true); + } + + function toggleFolder(folderId) { + setExpandedFolders((previous) => { + const next = new Set(previous); + if (next.has(folderId)) { + next.delete(folderId); + } else { + next.add(folderId); + } + return next; + }); + } + + return ( +
+ + +
+
+ + + + +
+ + + + + +
+
+ +
+
+
+
+

{boardTitle}

+ +
+ +
+ +
+
+ {viewModes.map(({ id, label, Icon }) => ( + + ))} +
+ +
+
+ +
+ {filterChips.map((label) => ( + + ))} + +
+ +
+ {activeSourceTag} +

{boardSummary}

+

+ {connectionNotice.title} + {connectionNotice.body} +

+ {isScanning ? Scanning... : null} + {normalizedQuery ? ( + + ) : null} +
+ + {visibleEntries.length === 0 ? ( + setQuery('')} /> + ) : viewMode === 'list' ? ( + + ) : ( + + )} +
+
+ + + + setDetailsOpen(false)} + onOpenFolder={(folderId) => { + openFolder(folderId); + setDetailsOpen(false); + }} + /> + + setSettingsOpen(false)} + onDraftPathChange={setDraftPath} + onRefreshDirectory={refreshDirectory} + onSubmit={handleSettingsSubmit} + onUseSampleData={useSampleData} + /> +
+ ); +} + +function SidebarTree({ nodes, currentFolderId, expandedFolders, onOpenFolder, onToggle, depth = 0 }) { + return nodes + .filter((node) => node.kind === 'folder') + .map((node) => { + const childFolders = (node.children ?? []).filter((child) => child.kind === 'folder'); + const expanded = expandedFolders.has(node.id); + + return ( +
+
+ + +
+ + {expanded && childFolders.length ? ( + + ) : null} +
+ ); + }); +} + +function BreadcrumbTrail({ trail, onOpenFolder }) { + return ( + + ); +} + +function FilterChip({ label, onClick }) { + return ( + + ); +} + +function ListView({ entries, selectedItemId, showLocation, sourceMode, onActivate, onInspect }) { + return ( +
+
+ + Name + + + Owner + Date modified + File size + + + Sort + +
+
+ {entries.map(({ item, path }) => { + const owner = getOwnerLabel(sourceMode, item); + + return ( +
+ + + + {owner} + + {item.modified} + {getTableSize(item)} + +
+ ); + })} +
+
+ ); +} + +function CardView({ entries, selectedItemId, showLocation, sourceMode, onActivate, onInspect }) { + return ( +
+ {entries.map(({ item, path }) => { + const owner = getOwnerLabel(sourceMode, item); + return ( +
+
+ + + + +
+ +
+ ); + })} +
+ ); +} + +function EmptyState({ query, onReset }) { + return ( +
+

No results for "{query.trim()}"

+

Try a folder name, file type, owner, or path. Search matches against the full active source.

+ +
+ ); +} + +function DetailsDrawer({ item, open, path, sourceMode, onClose, onOpenFolder }) { + const owner = getOwnerLabel(sourceMode, item); + + return ( + <> +
+ + + ); +} + +function SettingsPanel({ + apiChecked, + apiReady, + draftPath, + isScanning, + open, + rootTotals, + sourceError, + sourceMode, + sourceName, + sourcePath, + onClose, + onDraftPathChange, + onRefreshDirectory, + onSubmit, + onUseSampleData, +}) { + return ( + <> +
+ + + ); +} + +function OwnerAvatar({ label, compact = false }) { + return {getAvatarInitials(label)}; +} + +function ItemIcon({ item, compact = false }) { + const iconType = item.kind === 'folder' ? 'folder' : item.type; + + return ( + + ); +} + +function DriveLogo() { + return ( + + ); +} + +function SearchIcon() { + return ( + + ); +} + +function SlidersIcon() { + return ( + + ); +} + +function ChevronIcon() { + return ( + + ); +} + +function ChevronDownIcon() { + return ( + + ); +} + +function ChevronRightIcon() { + return ( + + ); +} + +function ListIcon() { + return ( + + ); +} + +function GridIcon() { + return ( + + ); +} + +function InfoIcon() { + return ( + + ); +} + +function DotsIcon() { + return ( + + ); +} + +function PlusIcon() { + return ( + + ); +} + +function MenuIcon() { + return ( + + ); +} + +function HomeIcon() { + return ( + + ); +} + +function DriveFolderIcon() { + return ( + + ); +} + +function ComputerIcon() { + return ( + + ); +} + +function SharedIcon() { + return ( + + ); +} + +function RecentIcon() { + return ( + + ); +} + +function StarIcon() { + return ( + + ); +} + +function SpamIcon() { + return ( + + ); +} + +function TrashIcon() { + return ( + + ); +} + +function CloudIcon() { + return ( + + ); +} + +function HelpIcon() { + return ( + + ); +} + +function GearIcon() { + return ( + + ); +} + +function SparkleIcon() { + return ( + + ); +} + +function AppsIcon() { + return ( + + ); +} + +function SortAscendingIcon() { + return ( + + ); +} + +function SortIcon() { + return ( + + ); +} + +function CalendarIcon() { + return ( + + ); +} + +function KeepIcon() { + return ( + + ); +} + +function TasksIcon() { + return ( + + ); +} + +export default App; \ No newline at end of file diff --git a/src/data/driveData.js b/src/data/driveData.js new file mode 100644 index 0000000..6361388 --- /dev/null +++ b/src/data/driveData.js @@ -0,0 +1,297 @@ +export const driveData = { + id: 'root', + name: 'My Drive', + kind: 'folder', + owner: 'me', + modified: 'Today', + children: [ + { + id: 'projects', + name: '01 Projects', + kind: 'folder', + owner: 'me', + modified: '21 Jan', + children: [ + { + id: 'project-alpha', + name: 'Alpha Launch', + kind: 'folder', + owner: 'me', + modified: '24 Jan', + children: [ + { + id: 'alpha-brief', + name: 'Launch Brief', + kind: 'file', + type: 'doc', + owner: 'me', + modified: '24 Jan', + size: '1.2 MB', + }, + { + id: 'alpha-tracker', + name: 'Milestone Tracker', + kind: 'file', + type: 'sheet', + owner: 'me', + modified: '23 Jan', + size: '468 KB', + }, + { + id: 'alpha-deck', + name: 'Launch Deck', + kind: 'file', + type: 'slides', + owner: 'me', + modified: '22 Jan', + size: '4.9 MB', + }, + ], + }, + { + id: 'site-refresh', + name: 'Site Refresh', + kind: 'folder', + owner: 'me', + modified: '22 Jan', + children: [ + { + id: 'site-audit', + name: 'UI Inventory', + kind: 'file', + type: 'pdf', + owner: 'me', + modified: '22 Jan', + size: '2.1 MB', + }, + { + id: 'site-moodboard', + name: 'Reference Shots', + kind: 'file', + type: 'image', + owner: 'me', + modified: '21 Jan', + size: '12.4 MB', + }, + ], + }, + { + id: 'client-handoff', + name: 'Client Handoff.zip', + kind: 'file', + type: 'zip', + owner: 'me', + modified: '21 Jan', + size: '78 MB', + }, + ], + }, + { + id: 'areas', + name: '02 Areas', + kind: 'folder', + owner: 'me', + modified: '21 Jan', + children: [ + { + id: 'finance', + name: 'Finance', + kind: 'folder', + owner: 'me', + modified: '20 Jan', + children: [ + { + id: 'budget-2026', + name: 'Budget 2026', + kind: 'file', + type: 'sheet', + owner: 'me', + modified: '20 Jan', + size: '512 KB', + }, + { + id: 'forecast-pdf', + name: 'Quarter Forecast', + kind: 'file', + type: 'pdf', + owner: 'me', + modified: '19 Jan', + size: '1.9 MB', + }, + ], + }, + { + id: 'operations', + name: 'Operations', + kind: 'folder', + owner: 'me', + modified: '19 Jan', + children: [ + { + id: 'ops-sop', + name: 'SOP Manual', + kind: 'file', + type: 'doc', + owner: 'me', + modified: '19 Jan', + size: '934 KB', + }, + { + id: 'vendor-list', + name: 'Vendor List', + kind: 'file', + type: 'sheet', + owner: 'me', + modified: '18 Jan', + size: '402 KB', + }, + ], + }, + { + id: 'people-ops', + name: 'People Ops', + kind: 'folder', + owner: 'me', + modified: '18 Jan', + children: [ + { + id: 'onboarding-pack', + name: 'Onboarding Pack', + kind: 'file', + type: 'pdf', + owner: 'me', + modified: '18 Jan', + size: '3.6 MB', + }, + ], + }, + ], + }, + { + id: 'resources', + name: '03 Resources', + kind: 'folder', + owner: 'me', + modified: '21 Jan', + children: [ + { + id: 'templates', + name: 'Templates', + kind: 'folder', + owner: 'me', + modified: '17 Jan', + children: [ + { + id: 'project-template', + name: 'Project Template', + kind: 'file', + type: 'doc', + owner: 'me', + modified: '17 Jan', + size: '690 KB', + }, + { + id: 'kickoff-checklist', + name: 'Kickoff Checklist', + kind: 'file', + type: 'doc', + owner: 'me', + modified: '16 Jan', + size: '418 KB', + }, + ], + }, + { + id: 'brand-assets', + name: 'Brand Assets', + kind: 'folder', + owner: 'me', + modified: '16 Jan', + children: [ + { + id: 'logo-pack', + name: 'Logo Pack', + kind: 'file', + type: 'zip', + owner: 'me', + modified: '16 Jan', + size: '24 MB', + }, + { + id: 'team-photo', + name: 'Team Photo', + kind: 'file', + type: 'image', + owner: 'me', + modified: '16 Jan', + size: '8.3 MB', + }, + ], + }, + { + id: 'research-library', + name: 'Research Library', + kind: 'folder', + owner: 'me', + modified: '15 Jan', + children: [ + { + id: 'discovery-notes', + name: 'Discovery Notes', + kind: 'file', + type: 'doc', + owner: 'me', + modified: '15 Jan', + size: '1.1 MB', + }, + ], + }, + ], + }, + { + id: 'archive', + name: '04 Archive', + kind: 'folder', + owner: 'me', + modified: '21 Jan', + children: [ + { + id: 'archive-2025', + name: '2025', + kind: 'folder', + owner: 'me', + modified: '14 Jan', + children: [ + { + id: 'handoff-recording', + name: 'Handoff Recording', + kind: 'file', + type: 'video', + owner: 'me', + modified: '14 Jan', + size: '214 MB', + }, + ], + }, + { + id: 'archive-2024', + name: '2024', + kind: 'folder', + owner: 'me', + modified: '10 Jan', + children: [ + { + id: 'legacy-site', + name: 'Legacy Site Export', + kind: 'file', + type: 'zip', + owner: 'me', + modified: '10 Jan', + size: '62 MB', + }, + ], + }, + ], + }, + ], +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..0248cfd --- /dev/null +++ b/src/index.css @@ -0,0 +1,1364 @@ +:root { + color-scheme: dark; + font-family: Aptos, 'Segoe UI Variable', 'Trebuchet MS', sans-serif; + --app-bg: #202124; + --bg-elevated: #1d1f21; + --board: #121316; + --board-2: #17191c; + --panel: #1b1d20; + --panel-strong: #24272b; + --line: rgba(255, 255, 255, 0.08); + --line-strong: rgba(255, 255, 255, 0.14); + --text: #e8eaed; + --muted: #9aa0a6; + --muted-strong: #bdc1c6; + --blue: #0b57d0; + --blue-soft: rgba(11, 87, 208, 0.18); + --blue-strong: #8ab4f8; + --green: #34a853; + --amber: #fbbc04; + --red: #ea4335; + --shadow: 0 18px 48px rgba(0, 0, 0, 0.38); +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + min-height: 100%; +} + +body { + margin: 0; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(11, 87, 208, 0.18), transparent 28%), + radial-gradient(circle at bottom right, rgba(251, 188, 4, 0.1), transparent 22%), + linear-gradient(180deg, #202124 0%, #1e1f22 100%); +} + +body, +button, +input { + font: inherit; +} + +button, +input { + color: inherit; +} + +button { + border: 0; + background: transparent; +} + +button:disabled { + opacity: 0.56; + cursor: not-allowed; +} + +input { + border: 0; +} + +#root { + min-height: 100vh; +} + +.drive-app { + min-height: 100vh; + display: grid; + grid-template-columns: 228px minmax(0, 1fr) 64px; + gap: 14px; + padding: 12px; +} + +.drive-sidebar, +.drive-board, +.details-drawer, +.settings-panel { + box-shadow: var(--shadow); +} + +.drive-sidebar { + min-height: calc(100vh - 24px); + background: linear-gradient(180deg, rgba(31, 33, 36, 0.96), rgba(24, 26, 29, 0.98)); + border: 1px solid var(--line); + border-radius: 26px; + padding: 18px 10px 18px 12px; + display: flex; + flex-direction: column; + gap: 14px; + animation: rise-in 280ms ease both; +} + +.drive-main { + min-width: 0; + display: grid; + grid-template-rows: auto 1fr; + gap: 12px; + animation: rise-in 340ms ease both; +} + +.drive-toolbar { + display: flex; + align-items: center; + gap: 14px; + padding: 4px 6px; +} + +.drive-brand { + display: flex; + align-items: center; + gap: 10px; + padding: 4px 8px 8px; +} + +.drive-brand strong { + font-size: 1.15rem; + font-weight: 700; +} + +.drive-brand-mark { + width: 34px; + height: 34px; + display: grid; + place-items: center; +} + +.drive-brand-mark svg { + width: 30px; + height: 28px; +} + +.new-button, +.storage-button, +.filter-chip, +.sidebar-toggle, +.mobile-close, +.panel-close-button, +.clear-search-button, +.primary-button, +.secondary-button, +.tree-expander, +.tree-link, +.drive-nav-item, +.toolbar-icon, +.profile-chip, +.board-icon-button, +.row-name-button, +.row-menu-button, +.drive-card-main, +.settings-input { + border-radius: 999px; +} + +.new-button, +.storage-button, +.filter-chip, +.sidebar-toggle, +.mobile-close, +.panel-close-button, +.clear-search-button, +.primary-button, +.secondary-button, +.drive-nav-item, +.tree-expander, +.tree-link, +.toolbar-icon, +.profile-chip, +.board-icon-button, +.row-menu-button { + cursor: pointer; + transition: + background-color 160ms ease, + border-color 160ms ease, + transform 160ms ease, + color 160ms ease; +} + +.new-button:hover, +.storage-button:hover, +.filter-chip:hover, +.sidebar-toggle:hover, +.mobile-close:hover, +.panel-close-button:hover, +.clear-search-button:hover, +.primary-button:hover, +.secondary-button:hover, +.drive-nav-item:hover, +.tree-link:hover, +.tree-expander:hover, +.toolbar-icon:hover, +.profile-chip:hover, +.board-icon-button:hover, +.row-menu-button:hover, +.drive-card-main:hover { + transform: translateY(-1px); +} + +.new-button { + margin: 0 8px 6px; + padding: 14px 18px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + background: #3c4043; + color: #f1f3f4; + font-weight: 700; +} + +.new-button svg { + width: 18px; + height: 18px; +} + +.drive-nav { + display: grid; + gap: 4px; +} + +.drive-nav.secondary { + margin-top: 6px; +} + +.drive-nav-item { + width: 100%; + padding: 10px 14px; + display: flex; + align-items: center; + gap: 12px; + color: var(--muted-strong); + text-align: left; +} + +.drive-nav-item svg { + width: 18px; + height: 18px; + flex: 0 0 auto; +} + +.drive-nav-item.active { + background: var(--blue); + color: white; +} + +.drive-nav-item.passive { + color: var(--muted); + opacity: 0.95; + pointer-events: none; +} + +.nav-chevron { + margin-left: auto; + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.drive-nav-item.expanded .nav-chevron svg { + transform: rotate(180deg); +} + +.drive-tree-block { + margin: 2px 0 8px; + padding: 2px 0 0 2px; +} + +.tree-node { + display: grid; +} + +.tree-row { + --depth: 0; + display: grid; + grid-template-columns: 24px minmax(0, 1fr); + align-items: center; + gap: 4px; + padding-left: calc(var(--depth) * 14px + 10px); +} + +.tree-row.active .tree-link { + background: rgba(138, 180, 248, 0.12); + color: white; +} + +.tree-expander { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted); +} + +.tree-expander svg { + width: 14px; + height: 14px; +} + +.tree-expander.expanded svg { + transform: rotate(90deg); +} + +.tree-expander.empty { + opacity: 0.32; + pointer-events: none; +} + +.tree-link { + min-width: 0; + padding: 7px 10px; + display: flex; + align-items: center; + gap: 10px; + color: var(--muted-strong); + text-align: left; +} + +.tree-link span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.storage-widget { + margin-top: auto; + padding: 16px 14px; + display: grid; + gap: 12px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(36, 39, 43, 0.94), rgba(27, 30, 33, 0.96)); + border: 1px solid var(--line); +} + +.storage-title-row { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--muted-strong); + font-weight: 600; +} + +.storage-title-row svg { + width: 16px; + height: 16px; +} + +.storage-bar { + height: 4px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + overflow: hidden; +} + +.storage-bar span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--blue), #5e97f6); +} + +.storage-copy { + margin: 0; + color: var(--muted); + font-size: 0.92rem; +} + +.storage-button { + justify-self: start; + padding: 10px 14px; + background: transparent; + border: 1px solid var(--line-strong); + color: var(--blue-strong); +} + +.drive-search { + min-width: 0; + flex: 1; + height: 56px; + display: flex; + align-items: center; + gap: 14px; + padding: 0 18px; + border-radius: 999px; + background: rgba(49, 51, 56, 0.96); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.drive-search svg, +.toolbar-icon svg, +.board-icon-button svg, +.row-menu-button svg, +.profile-chip svg, +.search-filter-icon svg { + width: 18px; + height: 18px; +} + +.drive-search input { + min-width: 0; + width: 100%; + background: transparent; + color: var(--text); + outline: none; +} + +.drive-search input::placeholder { + color: var(--muted); +} + +.search-filter-icon { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted); +} + +.toolbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.toolbar-icon, +.profile-chip, +.board-icon-button, +.row-menu-button, +.sidebar-toggle, +.mobile-close, +.panel-close-button { + width: 38px; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted-strong); + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--line); +} + +.toolbar-icon:hover, +.profile-chip:hover, +.board-icon-button:hover, +.row-menu-button:hover, +.sidebar-toggle:hover, +.mobile-close:hover, +.panel-close-button:hover { + background: rgba(255, 255, 255, 0.08); +} + +.board-icon-button.active { + background: var(--blue-soft); + color: var(--blue-strong); + border-color: rgba(138, 180, 248, 0.22); +} + +.sidebar-toggle, +.mobile-close { + display: none; +} + +.profile-chip { + padding: 0; + overflow: hidden; +} + +.drive-board { + min-width: 0; + min-height: calc(100vh - 94px); + background: + linear-gradient(180deg, rgba(18, 19, 22, 0.98), rgba(19, 20, 23, 0.98)), + linear-gradient(90deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)); + border: 1px solid var(--line); + border-radius: 28px; + padding: 20px 20px 10px; + position: relative; + overflow: hidden; +} + +.board-header, +.board-title-row, +.board-header-actions, +.filter-row, +.board-meta, +.drive-card-top, +.drawer-header, +.settings-status-row, +.settings-actions { + display: flex; + align-items: center; +} + +.board-header { + justify-content: space-between; + gap: 18px; + margin-bottom: 14px; +} + +.board-title-stack { + min-width: 0; +} + +.board-title-row { + gap: 6px; +} + +.board-title-row h1, +.drawer-header h2, +.empty-state h3 { + margin: 0; +} + +.board-title-row h1 { + font-size: clamp(1.8rem, 3vw, 2.2rem); + font-weight: 600; + letter-spacing: -0.04em; +} + +.board-title-row svg { + width: 18px; + height: 18px; + color: var(--muted); +} + +.board-header-actions { + gap: 10px; +} + +.view-switch { + display: grid; + grid-auto-flow: column; + gap: 4px; + padding: 4px; + border-radius: 999px; + border: 1px solid var(--line-strong); + background: rgba(255, 255, 255, 0.03); +} + +.view-switch button { + width: 40px; + height: 32px; + border-radius: 999px; + color: var(--muted-strong); + cursor: pointer; +} + +.view-switch button.active { + background: var(--blue); + color: white; +} + +.breadcrumbs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.breadcrumb-item { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + color: var(--muted); + font-size: 0.88rem; +} + +.breadcrumb-item button { + padding: 0; + color: var(--muted); + cursor: pointer; +} + +.breadcrumb-item button:hover, +.breadcrumb-current { + color: var(--muted-strong); +} + +.breadcrumb-item svg { + width: 12px; + height: 12px; +} + +.filter-row { + gap: 10px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.filter-chip { + padding: 9px 14px; + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid var(--line-strong); + color: var(--muted-strong); + background: transparent; +} + +.filter-chip svg { + width: 14px; + height: 14px; +} + +.board-meta { + flex-wrap: wrap; + gap: 12px 14px; + margin-bottom: 10px; +} + +.source-pill, +.status-chip { + padding: 7px 12px; + border-radius: 999px; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.source-pill.info, +.status-chip { + background: rgba(138, 180, 248, 0.12); + color: var(--blue-strong); +} + +.source-pill.success { + background: rgba(52, 168, 83, 0.14); + color: #8ed39e; +} + +.source-pill.warning { + background: rgba(251, 188, 4, 0.14); + color: #fdd663; +} + +.source-pill.error { + background: rgba(234, 67, 53, 0.14); + color: #ff9a91; +} + +.board-summary, +.board-notice, +.drawer-copy p, +.settings-copy, +.settings-note, +.settings-error, +.empty-state p { + margin: 0; + line-height: 1.55; +} + +.board-summary { + color: var(--muted-strong); +} + +.board-notice { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 8px; + color: var(--muted); +} + +.board-notice strong { + color: var(--text); + font-weight: 600; +} + +.clear-search-button, +.secondary-button { + padding: 10px 14px; + border: 1px solid var(--line-strong); + color: var(--muted-strong); +} + +.primary-button { + padding: 12px 18px; + background: var(--blue); + color: white; + font-weight: 700; +} + +.drive-list { + min-width: 0; +} + +.drive-list-head, +.drive-list-row { + display: grid; + grid-template-columns: minmax(0, 2.7fr) 1.15fr 1fr 0.9fr auto; + gap: 12px; + align-items: center; +} + +.drive-list-head { + padding: 14px 8px 12px; + color: var(--muted); + font-size: 0.84rem; + border-bottom: 1px solid var(--line-strong); +} + +.head-name, +.head-sort { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.head-sort { + justify-content: flex-end; +} + +.sort-indicator svg, +.head-sort svg { + width: 14px; + height: 14px; +} + +.drive-list-body { + display: grid; +} + +.drive-list-row { + padding: 4px 8px; + min-height: 48px; + border-bottom: 1px solid var(--line); +} + +.drive-list-row.selected { + background: rgba(11, 87, 208, 0.08); +} + +.drive-list-row:hover { + background: rgba(255, 255, 255, 0.025); +} + +.row-name-button { + min-width: 0; + padding: 10px 6px; + display: flex; + align-items: center; + gap: 12px; + color: var(--text); + text-align: left; +} + +.row-name-copy, +.drive-card-main { + min-width: 0; + display: grid; + gap: 4px; +} + +.row-name-copy strong, +.drive-card-main strong { + font-size: 0.98rem; + font-weight: 500; +} + +.row-name-copy span, +.drive-card-main span { + min-width: 0; + color: var(--muted); + font-size: 0.84rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.owner-cell, +.row-value { + color: var(--muted-strong); + font-size: 0.92rem; +} + +.owner-cell { + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.owner-cell span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; + padding-bottom: 14px; +} + +.drive-card { + padding: 16px; + border-radius: 24px; + border: 1px solid var(--line); + background: linear-gradient(180deg, rgba(29, 31, 34, 0.94), rgba(24, 26, 29, 0.98)); + display: grid; + gap: 16px; +} + +.drive-card.selected { + border-color: rgba(138, 180, 248, 0.18); + box-shadow: 0 0 0 1px rgba(138, 180, 248, 0.08); +} + +.drive-card-top { + justify-content: space-between; +} + +.card-icon-frame, +.drawer-preview-icon { + width: 64px; + height: 64px; + display: grid; + place-items: center; + border-radius: 18px; + background: rgba(255, 255, 255, 0.04); +} + +.drive-card-main { + width: 100%; + padding: 0; + color: var(--text); + text-align: left; +} + +.empty-state { + padding: 28px; + margin-top: 12px; + border-radius: 24px; + border: 1px dashed var(--line-strong); + background: rgba(255, 255, 255, 0.02); + display: grid; + gap: 12px; + justify-items: start; +} + +.item-icon { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted-strong); + flex: 0 0 auto; +} + +.item-icon.compact { + width: 24px; + height: 24px; +} + +.item-icon svg { + width: 22px; + height: 22px; +} + +.item-icon.compact svg { + width: 18px; + height: 18px; +} + +.item-icon-folder { + color: #c7cbd1; +} + +.item-icon-doc { + color: #8ab4f8; +} + +.item-icon-sheet { + color: #81c995; +} + +.item-icon-slides { + color: #fdd663; +} + +.item-icon-image { + color: #f6aea9; +} + +.item-icon-pdf { + color: #ff8a80; +} + +.item-icon-video { + color: #b39ddb; +} + +.item-icon-zip, +.item-icon-file { + color: #b0bec5; +} + +.owner-avatar { + width: 34px; + height: 34px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: linear-gradient(135deg, #d5e3ff, #dff6d8); + color: #1f1f1f; + font-size: 0.7rem; + font-weight: 800; + letter-spacing: 0.04em; +} + +.owner-avatar.compact { + width: 30px; + height: 30px; +} + +.drive-rail { + min-height: calc(100vh - 24px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-radius: 22px; + border: 1px solid var(--line); + background: rgba(27, 29, 32, 0.9); +} + +.rail-stack { + display: grid; + gap: 18px; +} + +.rail-icon { + width: 34px; + height: 34px; + border-radius: 12px; + display: grid; + place-items: center; + color: var(--muted-strong); + background: rgba(255, 255, 255, 0.03); +} + +.rail-icon svg { + width: 18px; + height: 18px; +} + +.rail-calendar { + color: #8ab4f8; + background: rgba(138, 180, 248, 0.12); +} + +.rail-note { + color: #fdd663; + background: rgba(251, 188, 4, 0.12); +} + +.rail-task { + color: #81c995; + background: rgba(52, 168, 83, 0.12); +} + +.rail-divider { + width: 22px; + height: 1px; + background: var(--line-strong); +} + +.rail-plus { + margin-bottom: 6px; +} + +.drawer-scrim, +.settings-scrim { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.35); + opacity: 0; + pointer-events: none; + transition: opacity 180ms ease; + z-index: 18; +} + +.drawer-scrim.open, +.settings-scrim.open { + opacity: 1; + pointer-events: auto; +} + +.details-drawer, +.settings-panel { + position: fixed; + top: 12px; + bottom: 12px; + width: min(360px, calc(100vw - 104px)); + padding: 18px; + background: linear-gradient(180deg, rgba(27, 29, 32, 0.98), rgba(20, 22, 24, 0.98)); + border: 1px solid var(--line); + border-radius: 26px; + overflow: auto; + opacity: 0; + pointer-events: none; + transition: transform 220ms ease, opacity 180ms ease; + z-index: 19; +} + +.details-drawer { + right: 88px; + transform: translateX(calc(100% + 112px)); +} + +.settings-panel { + right: 88px; + transform: translateX(calc(100% + 112px)); + z-index: 20; +} + +.details-drawer.open, +.settings-panel.open { + opacity: 1; + pointer-events: auto; + transform: translateX(0); +} + +.drawer-header { + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; +} + +.drawer-header h2 { + font-size: 1.35rem; + letter-spacing: -0.03em; +} + +.drawer-eyebrow { + margin: 0 0 4px; + color: var(--muted); + font-size: 0.76rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.drawer-preview { + height: 180px; + border-radius: 24px; + display: grid; + place-items: center; + margin-bottom: 18px; + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.preview-folder { + background: linear-gradient(135deg, rgba(138, 180, 248, 0.14), rgba(255, 255, 255, 0.03)); +} + +.preview-doc { + background: linear-gradient(135deg, rgba(138, 180, 248, 0.18), rgba(29, 31, 34, 0.92)); +} + +.preview-sheet { + background: linear-gradient(135deg, rgba(52, 168, 83, 0.18), rgba(29, 31, 34, 0.92)); +} + +.preview-slides { + background: linear-gradient(135deg, rgba(251, 188, 4, 0.18), rgba(29, 31, 34, 0.92)); +} + +.preview-image { + background: linear-gradient(135deg, rgba(244, 180, 171, 0.18), rgba(29, 31, 34, 0.92)); +} + +.preview-pdf { + background: linear-gradient(135deg, rgba(234, 67, 53, 0.18), rgba(29, 31, 34, 0.92)); +} + +.preview-video { + background: linear-gradient(135deg, rgba(179, 157, 219, 0.18), rgba(29, 31, 34, 0.92)); +} + +.preview-zip, +.preview-file { + background: linear-gradient(135deg, rgba(176, 190, 197, 0.18), rgba(29, 31, 34, 0.92)); +} + +.drawer-copy { + margin-bottom: 16px; +} + +.drawer-grid, +.settings-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + margin: 0 0 18px; +} + +.drawer-grid div, +.settings-grid div { + padding: 14px 16px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--line); +} + +.drawer-grid dt, +.settings-grid dt { + margin-bottom: 6px; + color: var(--muted); + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.drawer-grid dd, +.settings-grid dd { + margin: 0; + color: var(--text); + line-height: 1.45; + word-break: break-word; +} + +.settings-card { + display: grid; + gap: 16px; +} + +.settings-status-row { + flex-wrap: wrap; + justify-content: space-between; + gap: 10px; +} + +.settings-status-row strong { + font-size: 1.02rem; +} + +.settings-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.settings-field { + display: grid; + gap: 8px; +} + +.settings-field span { + color: var(--muted); + font-size: 0.76rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.settings-input { + width: 100%; + padding: 14px 16px; + border: 1px solid var(--line-strong); + background: rgba(255, 255, 255, 0.03); + color: var(--text); + outline: none; +} + +.settings-input:focus { + border-color: rgba(138, 180, 248, 0.44); + box-shadow: 0 0 0 4px rgba(11, 87, 208, 0.14); +} + +.settings-actions { + flex-wrap: wrap; + gap: 10px; +} + +.settings-copy, +.settings-note { + color: var(--muted); +} + +.settings-error { + color: #ff8a80; +} + +@keyframes rise-in { + from { + opacity: 0; + transform: translateY(12px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 1200px) { + .drive-app { + grid-template-columns: 228px minmax(0, 1fr); + } + + .drive-rail { + display: none; + } + + .details-drawer, + .settings-panel { + right: 16px; + width: min(360px, calc(100vw - 32px)); + } +} + +@media (max-width: 980px) { + .drive-app { + grid-template-columns: 1fr; + } + + .drive-sidebar { + position: fixed; + top: 12px; + left: 12px; + bottom: 12px; + width: min(300px, calc(100vw - 24px)); + z-index: 21; + transform: translateX(calc(-100% - 20px)); + transition: transform 200ms ease; + } + + .drive-sidebar.open { + transform: translateX(0); + } + + .sidebar-toggle, + .mobile-close { + display: inline-flex; + } + + .mobile-close { + margin-left: auto; + } + + .drive-brand strong { + margin-right: auto; + } + + .drive-board { + min-height: calc(100vh - 96px); + } +} + +@media (max-width: 760px) { + .drive-toolbar { + gap: 10px; + } + + .toolbar-actions { + gap: 8px; + } + + .toolbar-actions .toolbar-icon:not(button) { + display: none; + } + + .board-header { + flex-direction: column; + align-items: stretch; + } + + .board-header-actions { + justify-content: space-between; + } + + .filter-row { + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 4px; + } + + .filter-row::-webkit-scrollbar { + display: none; + } + + .drive-list-head { + display: none; + } + + .drive-list-row { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + 'name menu' + 'owner owner' + 'modified size'; + gap: 6px 12px; + padding: 10px 8px; + } + + .row-name-button { + grid-area: name; + padding: 4px 0; + } + + .row-menu-button { + grid-area: menu; + justify-self: end; + } + + .owner-cell { + grid-area: owner; + } + + .drive-list-row .row-value:nth-of-type(1) { + grid-area: modified; + } + + .drive-list-row .row-value:nth-of-type(2) { + grid-area: size; + justify-self: start; + } + + .settings-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 560px) { + .drive-app { + padding: 8px; + gap: 10px; + } + + .drive-search { + height: 52px; + padding: 0 14px; + } + + .drive-board { + border-radius: 22px; + padding: 18px 16px 8px; + } + + .details-drawer, + .settings-panel { + top: 8px; + right: 8px; + bottom: 8px; + left: 8px; + width: auto; + border-radius: 22px; + } + + .drive-card { + border-radius: 20px; + } +} \ No newline at end of file diff --git a/src/lib/fileSystemAccess.js b/src/lib/fileSystemAccess.js new file mode 100644 index 0000000..e76f7b2 --- /dev/null +++ b/src/lib/fileSystemAccess.js @@ -0,0 +1,372 @@ +const DB_NAME = 'driveboard'; +const STORE_NAME = 'settings'; +const DIRECTORY_HANDLE_KEY = 'directory-handle'; +const DIRECTORY_NAME_KEY = 'directory-name'; + +const LOCAL_OWNER = 'Local file system'; + +const imageExtensions = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'avif']); +const videoExtensions = new Set(['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v']); +const pdfExtensions = new Set(['pdf']); +const archiveExtensions = new Set(['zip', 'rar', '7z', 'tar', 'gz']); +const spreadsheetExtensions = new Set(['xls', 'xlsx', 'csv', 'tsv', 'ods']); +const slideExtensions = new Set(['ppt', 'pptx', 'key']); +const documentExtensions = new Set(['doc', 'docx', 'txt', 'md', 'rtf', 'odt']); + +function openDatabase() { + return new Promise((resolve, reject) => { + if (typeof indexedDB === 'undefined') { + reject(new Error('This browser does not expose IndexedDB.')); + return; + } + + const request = indexedDB.open(DB_NAME, 1); + + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains(STORE_NAME)) { + request.result.createObjectStore(STORE_NAME); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error ?? new Error('Unable to open browser storage.')); + }); +} + +async function readSetting(key) { + const db = await openDatabase(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly'); + const request = transaction.objectStore(STORE_NAME).get(key); + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error ?? new Error('Unable to read browser storage.')); + transaction.oncomplete = () => db.close(); + transaction.onerror = () => reject(transaction.error ?? new Error('Browser storage transaction failed.')); + }); +} + +async function writeSetting(key, value) { + const db = await openDatabase(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const request = transaction.objectStore(STORE_NAME).put(value, key); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error ?? new Error('Unable to write browser storage.')); + transaction.oncomplete = () => db.close(); + transaction.onerror = () => reject(transaction.error ?? new Error('Browser storage transaction failed.')); + }); +} + +async function deleteSetting(key) { + const db = await openDatabase(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const request = transaction.objectStore(STORE_NAME).delete(key); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error ?? new Error('Unable to clear browser storage.')); + transaction.oncomplete = () => db.close(); + transaction.onerror = () => reject(transaction.error ?? new Error('Browser storage transaction failed.')); + }); +} + +function createItemId(kind, pathSegments) { + return `${kind}:${pathSegments.join('/')}`; +} + +function compareChildren(left, right) { + if (left.kind !== right.kind) { + return left.kind === 'folder' ? -1 : 1; + } + + return left.name.localeCompare(right.name); +} + +function inferFileType(fileName) { + const extension = fileName.split('.').pop()?.toLowerCase() ?? ''; + + if (imageExtensions.has(extension)) { + return 'image'; + } + + if (videoExtensions.has(extension)) { + return 'video'; + } + + if (pdfExtensions.has(extension)) { + return 'pdf'; + } + + if (archiveExtensions.has(extension)) { + return 'zip'; + } + + if (spreadsheetExtensions.has(extension)) { + return 'sheet'; + } + + if (slideExtensions.has(extension)) { + return 'slides'; + } + + if (documentExtensions.has(extension)) { + return 'doc'; + } + + return 'file'; +} + +function formatTimestamp(timestamp) { + if (!timestamp) { + return 'Unavailable'; + } + + return new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }).format(new Date(timestamp)); +} + +async function readFileNode(handle, pathSegments) { + const file = await handle.getFile(); + + return { + id: createItemId('file', pathSegments), + name: handle.name, + kind: 'file', + type: inferFileType(handle.name), + owner: LOCAL_OWNER, + modified: formatTimestamp(file.lastModified), + modifiedMs: file.lastModified ?? 0, + size: formatBytes(file.size ?? 0), + sizeBytes: file.size ?? 0, + }; +} + +function createFolderNode(id, name) { + return { + id, + name, + kind: 'folder', + owner: LOCAL_OWNER, + modified: 'Unavailable', + modifiedMs: 0, + size: '0 B', + sizeBytes: 0, + folderCount: 0, + fileCount: 0, + children: [], + }; +} + +function createFileNodeFromFile(file, pathSegments) { + return { + id: createItemId('file', pathSegments), + name: file.name, + kind: 'file', + type: inferFileType(file.name), + owner: LOCAL_OWNER, + modified: formatTimestamp(file.lastModified), + modifiedMs: file.lastModified ?? 0, + size: formatBytes(file.size ?? 0), + sizeBytes: file.size ?? 0, + }; +} + +function finalizeDirectoryNode(node) { + let sizeBytes = 0; + let latestModified = 0; + let folderCount = 0; + let fileCount = 0; + + node.children.forEach((child) => { + if (child.kind === 'folder') { + finalizeDirectoryNode(child); + folderCount += child.folderCount + 1; + fileCount += child.fileCount; + sizeBytes += child.sizeBytes ?? 0; + latestModified = Math.max(latestModified, child.modifiedMs ?? 0); + return; + } + + fileCount += 1; + sizeBytes += child.sizeBytes ?? 0; + latestModified = Math.max(latestModified, child.modifiedMs ?? 0); + }); + + node.children.sort(compareChildren); + node.folderCount = folderCount; + node.fileCount = fileCount; + node.sizeBytes = sizeBytes; + node.size = formatBytes(sizeBytes); + node.modifiedMs = latestModified; + node.modified = formatTimestamp(latestModified); +} + +async function readDirectoryNode(handle, pathSegments = []) { + const children = []; + let sizeBytes = 0; + let latestModified = 0; + let folderCount = 0; + let fileCount = 0; + + for await (const entry of handle.values()) { + const entryPath = [...pathSegments, entry.name]; + + if (entry.kind === 'directory') { + const childFolder = await readDirectoryNode(entry, entryPath); + children.push(childFolder); + folderCount += childFolder.folderCount + 1; + fileCount += childFolder.fileCount; + sizeBytes += childFolder.sizeBytes ?? 0; + latestModified = Math.max(latestModified, childFolder.modifiedMs ?? 0); + continue; + } + + const fileNode = await readFileNode(entry, entryPath); + children.push(fileNode); + fileCount += 1; + sizeBytes += fileNode.sizeBytes ?? 0; + latestModified = Math.max(latestModified, fileNode.modifiedMs ?? 0); + } + + children.sort(compareChildren); + + return { + id: pathSegments.length ? createItemId('folder', pathSegments) : 'root', + name: pathSegments[pathSegments.length - 1] ?? handle.name, + kind: 'folder', + owner: LOCAL_OWNER, + modified: formatTimestamp(latestModified), + modifiedMs: latestModified, + size: formatBytes(sizeBytes), + sizeBytes, + folderCount, + fileCount, + children, + }; +} + +export function supportsDirectoryPicker() { + return typeof window !== 'undefined' && 'showDirectoryPicker' in window; +} + +export async function saveDirectoryHandle(handle) { + await Promise.all([ + writeSetting(DIRECTORY_HANDLE_KEY, handle), + writeSetting(DIRECTORY_NAME_KEY, handle.name), + ]); +} + +export async function loadStoredDirectoryAccess() { + const [handle, name] = await Promise.all([ + readSetting(DIRECTORY_HANDLE_KEY), + readSetting(DIRECTORY_NAME_KEY), + ]); + + return { handle, name }; +} + +export async function clearStoredDirectoryAccess() { + await Promise.all([ + deleteSetting(DIRECTORY_HANDLE_KEY), + deleteSetting(DIRECTORY_NAME_KEY), + ]); +} + +export async function hasDirectoryPermission(handle, request = false) { + if (!handle?.queryPermission) { + return true; + } + + let permission = await handle.queryPermission({ mode: 'read' }); + + if (permission !== 'granted' && request && handle.requestPermission) { + permission = await handle.requestPermission({ mode: 'read' }); + } + + return permission === 'granted'; +} + +export async function buildDriveTreeFromHandle(handle) { + const root = await readDirectoryNode(handle, []); + + return { + ...root, + id: 'root', + name: handle.name, + }; +} + +export function buildDriveTreeFromFileList(fileList) { + const files = Array.from(fileList ?? []); + if (!files.length) { + throw new Error('No files were selected. If you picked an empty folder, use Chrome or Edge for direct folder access.'); + } + + const firstRelativePath = files[0].webkitRelativePath || files[0].name; + const rootName = firstRelativePath.split('/').filter(Boolean)[0] || 'Selected folder'; + const root = createFolderNode('root', rootName); + const folders = new Map([['', root]]); + + files.forEach((file) => { + const relativePath = file.webkitRelativePath || `${rootName}/${file.name}`; + const pathSegments = relativePath.split('/').filter(Boolean); + const [, ...segmentsWithinRoot] = pathSegments; + const fileName = segmentsWithinRoot.pop(); + + if (!fileName) { + return; + } + + let currentFolder = root; + const folderTrail = []; + + segmentsWithinRoot.forEach((segment) => { + folderTrail.push(segment); + const key = folderTrail.join('/'); + + if (!folders.has(key)) { + const nextFolder = createFolderNode(createItemId('folder', [...folderTrail]), segment); + folders.set(key, nextFolder); + currentFolder.children.push(nextFolder); + } + + currentFolder = folders.get(key); + }); + + currentFolder.children.push(createFileNodeFromFile(file, [...segmentsWithinRoot, fileName])); + }); + + finalizeDirectoryNode(root); + root.id = 'root'; + root.name = rootName; + + return root; +} + +export function formatBytes(bytes) { + if (!Number.isFinite(bytes) || bytes <= 0) { + return '0 B'; + } + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + + const digits = value >= 10 || unitIndex === 0 ? 0 : 1; + return `${value.toFixed(digits)} ${units[unitIndex]}`; +} \ No newline at end of file diff --git a/src/lib/localDriveApi.js b/src/lib/localDriveApi.js new file mode 100644 index 0000000..3175d8f --- /dev/null +++ b/src/lib/localDriveApi.js @@ -0,0 +1,58 @@ +const LAST_DIRECTORY_PATH_KEY = 'driveboard:last-directory-path'; + +export async function checkLocalApiAvailability() { + try { + const response = await fetch('/api/health', { + headers: { + Accept: 'application/json', + }, + }); + + return response.ok; + } catch { + return false; + } +} + +export async function scanLocalDirectory(directoryPath) { + const response = await fetch('/api/filesystem/scan', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ directoryPath }), + }); + + const payload = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(payload.error ?? 'Unable to scan the local directory.'); + } + + return payload; +} + +export function loadSavedDirectoryPath() { + if (typeof window === 'undefined') { + return ''; + } + + return window.localStorage.getItem(LAST_DIRECTORY_PATH_KEY) ?? ''; +} + +export function saveDirectoryPath(directoryPath) { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.setItem(LAST_DIRECTORY_PATH_KEY, directoryPath); +} + +export function clearSavedDirectoryPath() { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.removeItem(LAST_DIRECTORY_PATH_KEY); +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..ecf2cca --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..33691dd --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': 'http://127.0.0.1:3001', + }, + }, + preview: { + proxy: { + '/api': 'http://127.0.0.1:3001', + }, + }, +}); \ No newline at end of file