commit 59d4a691d6e600198f948a43f3f92caf8cdc01be Author: Redsandy <34872843+Redsandyg@users.noreply.github.com> Date: Wed Aug 12 11:27:07 2026 +0300 Initialize Vyryi front-end project with React, Vite, and TypeScript. Add essential files including configuration, environment variables, and basic structure. Implement authentication, routing, and API interaction for a Telegram Mini App UI. Include README and styling for a cohesive user experience. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ad82eb2 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://127.0.0.1:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c3d27f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env +.env.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..06b016d --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Vyryi Front + +React (Vite + TypeScript) Telegram Mini App UI for **Vyryi**. + +## Stack + +- React 19 + React Router +- Local auth via backend `POST /auth/dev-login` +- Dark folk visual style matching the project logo + +## Screens + +1. `/` — logo + user agreement gate +2. `/home` — balance, stub deposit, slot stubs +3. `/games/:slug` — game placeholder + +## Quick start + +```bash +cd vyryi-front +npm install +copy .env.example .env +npm run dev +``` + +Open http://127.0.0.1:5173 — backend must be running on port 8000. + +## Env + +``` +VITE_API_URL=http://127.0.0.1:8000 +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..75ffd3d --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + Vyryi + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..31d2450 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2076 @@ +{ + "name": "vyryi-front", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vyryi-front", + "version": "0.0.0", + "dependencies": { + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", + "@react-three/postprocessing": "^3.0.5", + "gsap": "^3.15.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "three": "^0.185.1" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@types/three": "^0.185.4", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz", + "integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz", + "integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz", + "integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz", + "integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz", + "integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz", + "integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz", + "integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz", + "integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz", + "integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz", + "integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz", + "integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz", + "integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz", + "integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz", + "integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz", + "integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz", + "integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz", + "integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz", + "integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz", + "integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@react-three/drei": { + "version": "10.7.8", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.8.tgz", + "integrity": "sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.7.0.tgz", + "integrity": "sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/postprocessing": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.5.tgz", + "integrity": "sha512-rqxoH8cjq51w5yZ7Re3WNPVVDdSsuVwEYA+Nl90/nFLv0MIxYGjMkqhc6akp5+VLNwZD2aqOd8KcwuZW7NeUKQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.10.8", + "n8ao": "^2.0.0" + }, + "peerDependencies": { + "@react-three/fiber": ">=9.7.0", + "postprocessing": "^6.36.0", + "react": "^19.2.0", + "three": ">= 0.182.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.185.4", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.4.tgz", + "integrity": "sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "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/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "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/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, + "node_modules/hls.js": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.18.tgz", + "integrity": "sha512-Rtovq9oJt9HWBqFJF9wZrZM7dRvdGHYNdMMv+wSDGDE2+FIybgs8RPJpR3kya1TZKBHVv2NOgLmn/M0MFjrQ3g==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "license": "MIT" + }, + "node_modules/n8ao": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-2.0.1.tgz", + "integrity": "sha512-MoP1QKUdj4/3cKEnhFl917t0eQwWGER1l43ckz5mNOPyYWGRDGQbVoTOQz/VBvy01in6QVNG+AL6Xxx5AjZBYA==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/oxlint": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz", + "integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.78.0", + "@oxlint/binding-android-arm64": "1.78.0", + "@oxlint/binding-darwin-arm64": "1.78.0", + "@oxlint/binding-darwin-x64": "1.78.0", + "@oxlint/binding-freebsd-x64": "1.78.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", + "@oxlint/binding-linux-arm-musleabihf": "1.78.0", + "@oxlint/binding-linux-arm64-gnu": "1.78.0", + "@oxlint/binding-linux-arm64-musl": "1.78.0", + "@oxlint/binding-linux-ppc64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-musl": "1.78.0", + "@oxlint/binding-linux-s390x-gnu": "1.78.0", + "@oxlint/binding-linux-x64-gnu": "1.78.0", + "@oxlint/binding-linux-x64-musl": "1.78.0", + "@oxlint/binding-openharmony-arm64": "1.78.0", + "@oxlint/binding-win32-arm64-msvc": "1.78.0", + "@oxlint/binding-win32-ia32-msvc": "1.78.0", + "@oxlint/binding-win32-x64-msvc": "1.78.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postprocessing": { + "version": "6.39.4", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.4.tgz", + "integrity": "sha512-oAS/PjAbc/xT3OzjUrGsorJ4J064XwhVD2t0OwKLP/E8QwDMUJ0oOv6ZI1SYMtLchv4g0ySEx+JcLy92+48vlA==", + "license": "Zlib", + "peer": true, + "peerDependencies": { + "three": ">= 0.168.0 < 0.186.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT", + "peer": true + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz", + "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/troika-three-text": { + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.5.tgz", + "integrity": "sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.5", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.5.tgz", + "integrity": "sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa33987 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "vyryi-front", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", + "@react-three/postprocessing": "^3.0.5", + "gsap": "^3.15.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "three": "^0.185.1" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@types/three": "^0.185.4", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..b036856 Binary files /dev/null and b/public/logo.png differ diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..1c4f4dc --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from 'react' +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' +import { AuthProvider, useAuth } from './auth' +import { PRIVACY_DOC, TERMS_DOC } from './legal' +import { AgreementPage } from './pages/AgreementPage' +import { GamePage } from './pages/GamePage' +import { HomePage } from './pages/HomePage' +import { LegalDocumentPage } from './pages/LegalDocumentPage' +import { RootRedirect } from './pages/RootRedirect' +import { GateTransitProvider } from './vfx/GateTransitProvider' + +function BootShell({ children }: { children: ReactNode }) { + const { loading } = useAuth() + if (loading) { + return ( +
+ Vyryi +

Vyryi

+
+ ) + } + return children +} + +export default function App() { + return ( + + + + + + } /> + } /> + } + /> + + } + /> + } /> + } /> + } /> + + + + + + ) +} diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..ef54845 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,83 @@ +import type { GameStub, TokenResponse, User, Wallet } from './types' + +const API_URL = import.meta.env.VITE_API_URL ?? 'http://127.0.0.1:8000' +const TOKEN_KEY = 'vyryi_token' + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY) +} + +export function setToken(token: string): void { + localStorage.setItem(TOKEN_KEY, token) +} + +export function clearToken(): void { + localStorage.removeItem(TOKEN_KEY) +} + +async function request(path: string, options: RequestInit = {}): Promise { + const headers = new Headers(options.headers) + headers.set('Content-Type', 'application/json') + const token = getToken() + if (token) { + headers.set('Authorization', `Bearer ${token}`) + } + + const response = await fetch(`${API_URL}${path}`, { + ...options, + headers, + }) + + if (!response.ok) { + let detail = `Request failed (${response.status})` + try { + const body = (await response.json()) as { detail?: string } + if (body.detail) detail = body.detail + } catch { + /* ignore */ + } + throw new Error(detail) + } + + return (await response.json()) as T +} + +export const api = { + async devLogin(payload?: { + telegram_id?: number + username?: string + first_name?: string + }): Promise { + return request('/auth/dev-login', { + method: 'POST', + body: JSON.stringify(payload ?? {}), + }) + }, + + getMe(): Promise { + return request('/me') + }, + + acceptAgreement(): Promise { + return request('/me/agreement', { method: 'POST' }) + }, + + getWallet(): Promise { + return request('/wallet') + }, + + deposit(amount: number): Promise { + return request('/wallet/deposit', { + method: 'POST', + body: JSON.stringify({ amount }), + }) + }, + + listGames(): Promise { + return request('/games') + }, + + getGame(slug: string): Promise { + return request(`/games/${slug}`) + }, +} diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/auth.tsx b/src/auth.tsx new file mode 100644 index 0000000..2f2a578 --- /dev/null +++ b/src/auth.tsx @@ -0,0 +1,83 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from 'react' +import { api, clearToken, getToken, setToken } from './api' +import type { User } from './types' + +type AuthState = { + user: User | null + loading: boolean + error: string | null + refresh: () => Promise + acceptAgreement: () => Promise + setUser: (user: User) => void +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const bootstrap = useCallback(async () => { + setLoading(true) + setError(null) + try { + // Local MVP: no Telegram bot yet — always ensure a JWT via dev-login. + if (!getToken()) { + const token = await api.devLogin({ + telegram_id: 100001, + username: 'dev_user', + first_name: 'Путник', + }) + setToken(token.access_token) + } + const me = await api.getMe() + setUser(me) + } catch (err) { + clearToken() + setUser(null) + setError(err instanceof Error ? err.message : 'Не удалось войти') + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + void bootstrap() + }, [bootstrap]) + + const acceptAgreement = useCallback(async () => { + const updated = await api.acceptAgreement() + setUser(updated) + }, []) + + const value = useMemo( + () => ({ + user, + loading, + error, + refresh: bootstrap, + acceptAgreement, + setUser, + }), + [user, loading, error, bootstrap, acceptAgreement], + ) + + return {children} +} + +export function useAuth(): AuthState { + const ctx = useContext(AuthContext) + if (!ctx) { + throw new Error('useAuth must be used within AuthProvider') + } + return ctx +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..13f3f7c --- /dev/null +++ b/src/index.css @@ -0,0 +1,618 @@ +@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;0,700;1,500&family=Source+Sans+3:wght@400;500;600&display=swap'); + +:root { + --bg-deep: #1a1b16; + --bg: #2f3027; + --bg-lift: #3a3b31; + --gold: #a68946; + --gold-soft: #c4a96a; + --burgundy: #8c2b2b; + --eye: #c8d97d; + --cream: #e8e0d0; + --muted: #9a9486; + --ink: #0f100d; + --radius: 10px; + --font-display: 'Cormorant Garamond', 'Palatino Linotype', serif; + --font-body: 'Source Sans 3', 'Segoe UI', sans-serif; + color: var(--cream); + background: var(--bg-deep); + font-family: var(--font-body); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#root { + min-height: 100%; + margin: 0; +} + +body { + min-height: 100vh; + background: + radial-gradient(ellipse 80% 50% at 50% -10%, rgba(166, 137, 70, 0.18), transparent 55%), + radial-gradient(ellipse 60% 40% at 80% 100%, rgba(140, 43, 43, 0.12), transparent 50%), + linear-gradient(165deg, var(--bg-deep) 0%, var(--bg) 45%, #25261f 100%); + background-attachment: fixed; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + opacity: 0.07; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + z-index: 0; +} + +#root { + position: relative; + z-index: 1; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input { + font: inherit; +} + +.brand { + margin: 0; + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(2.8rem, 10vw, 4rem); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--gold-soft); + text-shadow: 0 2px 24px rgba(166, 137, 70, 0.35); + animation: brand-in 0.9s ease-out both; +} + +.brand--sm { + font-size: 1.35rem; + letter-spacing: 0.12em; + animation: none; +} + +.boot { + min-height: 100vh; + display: grid; + place-content: center; + gap: 0.75rem; + text-align: center; +} + +.boot__logo { + width: min(42vw, 180px); + margin: 0 auto; + animation: logo-rise 1.1s ease-out both; + filter: drop-shadow(0 8px 28px rgba(0, 0, 0, 0.55)); +} + +.gate { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1.25rem 3rem; + position: relative; + overflow: hidden; + transform-origin: center center; +} + +.gate__glow { + position: absolute; + width: 280px; + height: 280px; + border-radius: 50%; + background: radial-gradient(circle, rgba(200, 217, 125, 0.16), transparent 70%); + top: 12%; + left: 50%; + transform: translateX(-50%); + animation: pulse-glow 4s ease-in-out infinite; + pointer-events: none; +} + +.gate__content { + width: min(100%, 420px); + text-align: center; + position: relative; +} + +.gate__logo { + width: min(58vw, 220px); + display: block; + margin: 0 auto 0.5rem; + animation: logo-rise 1.1s ease-out both; + filter: drop-shadow(0 10px 32px rgba(0, 0, 0, 0.6)); +} + +.gate__tagline { + margin: 0.35rem 0 1.75rem; + color: var(--muted); + font-size: 1.05rem; + font-style: italic; + font-family: var(--font-display); + animation: fade-up 1s 0.2s ease-out both; +} + +.agreement { + text-align: left; + animation: fade-up 0.9s 0.35s ease-out both; +} + +.agreement__check { + display: flex; + gap: 0.65rem; + align-items: flex-start; + margin-bottom: 1rem; + color: var(--cream); + font-size: 0.95rem; + cursor: pointer; +} + +.agreement__check input { + margin-top: 0.2rem; + accent-color: var(--gold); +} + +.legal-link { + color: var(--gold-soft); + text-decoration: underline; + text-underline-offset: 0.15em; +} + +.legal-link:hover { + color: var(--eye); +} + +.legal-page { + max-width: 560px; + margin: 0 auto; + padding: 1.25rem 1.15rem 2.5rem; +} + +.legal-doc { + margin-top: 1.25rem; +} + +.legal-doc h1 { + margin: 0 0 1rem; + font-family: var(--font-display); + font-size: clamp(1.6rem, 5vw, 2rem); + font-weight: 600; + color: var(--gold-soft); +} + +.legal-doc p { + margin: 0 0 0.9rem; + color: var(--muted); + line-height: 1.55; + font-size: 0.98rem; +} + +.legal-doc p:last-child { + margin-bottom: 0; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + border: none; + border-radius: var(--radius); + padding: 0.85rem 1.1rem; + cursor: pointer; + transition: transform 0.15s ease, filter 0.15s ease, background 0.15s ease, opacity 0.15s ease; +} + +.btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.btn:not(:disabled):active { + transform: translateY(1px); +} + +.btn--gold { + background: linear-gradient(180deg, var(--gold-soft), var(--gold)); + color: var(--ink); + font-weight: 600; + letter-spacing: 0.04em; +} + +.btn--gold:not(:disabled):hover { + filter: brightness(1.06); +} + +.btn--sm { + width: auto; + padding: 0.45rem 0.85rem; + font-size: 0.85rem; +} + +.btn--outline { + background: transparent; + border: 1px solid var(--gold); + color: var(--gold-soft); +} + +.btn--outline:not(:disabled):hover { + background: rgba(166, 137, 70, 0.12); +} + +.btn--ghost { + background: transparent; + color: var(--muted); + margin-top: 0.5rem; +} + +.error { + color: #e8a0a0; + font-size: 0.9rem; + margin: 0 0 0.75rem; +} + +.linkish { + display: inline; + margin-left: 0.5rem; + background: none; + border: none; + color: var(--eye); + cursor: pointer; + text-decoration: underline; + padding: 0; +} + +.home { + max-width: 520px; + margin: 0 auto; + padding: 1.25rem 1.15rem 2.5rem; +} + +.home__top { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.home__brand-row { + display: flex; + align-items: center; + gap: 0.65rem; +} + +.home__mark { + width: 42px; + height: 42px; + object-fit: contain; +} + +.balance-panel { + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + 'label btn' + 'value btn'; + gap: 0.15rem 1rem; + align-items: center; + padding: 0.9rem 1rem; + background: rgba(15, 16, 13, 0.4); + border-bottom: 1px solid rgba(166, 137, 70, 0.4); +} + +.balance-panel__label { + grid-area: label; + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.balance-panel__value { + grid-area: value; + font-family: var(--font-display); + font-size: 2rem; + font-weight: 600; + color: var(--eye); +} + +.balance-panel .btn { + grid-area: btn; +} + +.section-lead { + margin: 0 0 1rem; + color: var(--muted); + font-size: 0.95rem; +} + +.games h2 { + margin: 0 0 0.35rem; + font-family: var(--font-display); + font-size: 1.75rem; + font-weight: 600; + color: var(--gold-soft); +} + +.games__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.game-link { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.25rem 0.75rem; + padding: 0.95rem 0.25rem 1rem; + border-bottom: 1px solid rgba(166, 137, 70, 0.22); + transition: background 0.2s ease, padding-left 0.2s ease; +} + +.game-link:hover { + background: rgba(166, 137, 70, 0.06); + padding-left: 0.35rem; +} + +.game-link__title { + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: 600; +} + +.game-link__status { + color: var(--burgundy); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + align-self: center; +} + +.game-link__desc { + grid-column: 1 / -1; + color: var(--muted); + font-size: 0.9rem; +} + +.toast { + margin: 0 0 1rem; + color: var(--eye); + font-size: 0.9rem; +} + +.modal { + position: fixed; + inset: 0; + background: rgba(10, 10, 8, 0.72); + display: grid; + place-items: center; + padding: 1.25rem; + z-index: 20; + animation: fade-in 0.2s ease; +} + +.modal__panel { + width: min(100%, 360px); + padding: 1.35rem 1.2rem; + background: linear-gradient(180deg, var(--bg-lift), var(--bg)); + border: 1px solid rgba(166, 137, 70, 0.4); + border-radius: var(--radius); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5); +} + +.modal__panel h3 { + margin: 0 0 0.35rem; + font-family: var(--font-display); + font-size: 1.5rem; + color: var(--gold-soft); +} + +.deposit-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.game-page { + max-width: 520px; + margin: 0 auto; + padding: 1.25rem 1.15rem 2.5rem; +} + +.back-link { + color: var(--muted); + font-size: 0.9rem; +} + +.back-link:hover { + color: var(--gold-soft); +} + +.game-stub { + margin-top: 2rem; + text-align: center; +} + +.game-stub__mark { + width: 120px; + margin-bottom: 1rem; + opacity: 0.9; + animation: logo-rise 0.8s ease-out both; +} + +.game-stub h1 { + margin: 0 0 0.5rem; + font-family: var(--font-display); + font-size: 2rem; + color: var(--gold-soft); +} + +.game-stub p { + color: var(--muted); + margin: 0 0 1rem; +} + +.game-stub__badge { + display: inline-block; + color: var(--burgundy); + border: 1px solid rgba(140, 43, 43, 0.5); + padding: 0.35rem 0.85rem; + letter-spacing: 0.12em; + text-transform: uppercase; + font-size: 0.75rem; +} + +@keyframes logo-rise { + from { + opacity: 0; + transform: translateY(18px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes brand-in { + from { + opacity: 0; + letter-spacing: 0.2em; + } + to { + opacity: 1; + letter-spacing: 0.08em; + } +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes pulse-glow { + 0%, + 100% { + opacity: 0.55; + transform: translateX(-50%) scale(1); + } + 50% { + opacity: 1; + transform: translateX(-50%) scale(1.08); + } +} + +@media (min-width: 640px) { + .home__top { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +/* Gate logo fly-in transit */ +html.gate-transit-lock, +html.gate-transit-lock body { + overflow: hidden; + touch-action: none; +} + +.gate-transit { + position: fixed; + inset: 0; + z-index: 9000; + pointer-events: none; + overflow: hidden; +} + +.gate-transit__stage { + position: absolute; + inset: 0; + perspective: 1400px; + perspective-origin: 50% 46%; +} + +.gate-transit__hero { + /* Hidden until JS pins it over .gate__logo — avoids first-frame size flash */ + position: fixed; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + visibility: hidden; + display: block; + margin: 0; + pointer-events: none; + user-select: none; + object-fit: contain; + filter: drop-shadow(0 10px 32px rgba(0, 0, 0, 0.6)); +} + + +.gate-transit__veil { + position: absolute; + inset: 0; + background: #000; + opacity: 0; + z-index: 3; +} + +.home--from-gate { + animation: home-from-void 0.95s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes home-from-void { + from { + opacity: 0; + transform: translateY(18px) scale(0.985); + filter: blur(8px); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .home--from-gate { + animation: fade-in 0.45s ease-out both; + } +} + diff --git a/src/legal.ts b/src/legal.ts new file mode 100644 index 0000000..73fb4c0 --- /dev/null +++ b/src/legal.ts @@ -0,0 +1,23 @@ +export const TERMS_DOC = { + title: 'Пользовательское соглашение', + paragraphs: [ + 'Настоящее Пользовательское соглашение (далее — «Соглашение») регулирует отношения между пользователем и сервисом Vyryi (далее — «Сервис») при использовании Telegram Mini App Vyryi.', + 'Vyryi — развлекательное приложение. Баланс в Сервисе можно пополнять. В текущей версии пополнение реализовано в тестовом режиме (заглушка). Вывод средств недоступен и не предусмотрен.', + 'Игры и иные функции Сервиса носят развлекательный характер. Участвуя в них, пользователь подтверждает, что понимает правила и принимает невозможность вывода денежных средств.', + 'Сервис вправе изменять набор игр, интерфейс, условия начисления и отображения баланса, а также временно ограничивать доступ для технического обслуживания.', + 'Пользователь обязуется не пытаться обойти ограничения Сервиса, не использовать вредоносное ПО и не нарушать работу приложения.', + 'Продолжая использование Сервиса после принятия Соглашения, пользователь подтверждает, что ознакомился с его условиями и полностью их принимает.', + ], +} + +export const PRIVACY_DOC = { + title: 'Политика обработки персональных данных', + paragraphs: [ + 'Настоящий документ описывает, какие данные обрабатывает сервис Vyryi и для каких целей.', + 'В рамках работы приложения могут обрабатываться идентификатор Telegram, имя пользователя, отображаемое имя, данные о принятии соглашений, баланс и история пополнений.', + 'Данные используются для аутентификации, предоставления функций Сервиса, учёта баланса, исполнения пользовательских соглашений и обеспечения безопасности.', + 'Данные хранятся на серверах, необходимых для работы приложения, и не передаются третьим лицам, за исключением случаев, прямо предусмотренных законом или технически необходимых для работы инфраструктуры.', + 'Пользователь может прекратить использование Сервиса. Вопросы по обработке данных можно направить оператору Сервиса после появления публичных контактов поддержки.', + 'Используя Vyryi и принимая соответствующие отметки на стартовом экране, пользователь подтверждает согласие на обработку указанных данных в объёме, необходимом для работы приложения.', + ], +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/pages/AgreementPage.tsx b/src/pages/AgreementPage.tsx new file mode 100644 index 0000000..a0d7730 --- /dev/null +++ b/src/pages/AgreementPage.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { useAuth } from '../auth' +import { useGateTransit } from '../vfx/GateTransitProvider' + +export function AgreementPage() { + const { user, loading, error, acceptAgreement, refresh } = useAuth() + const { start, active } = useGateTransit() + const [termsAccepted, setTermsAccepted] = useState(false) + const [privacyAccepted, setPrivacyAccepted] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [localError, setLocalError] = useState(null) + + const canEnter = termsAccepted && privacyAccepted + + async function onEnter() { + if (!canEnter || submitting || active) return + setSubmitting(true) + setLocalError(null) + + const result = await start(async () => { + if (!user?.agreement_accepted) { + await acceptAgreement() + } + }) + + if (!result.ok) { + setLocalError(result.error ?? 'Ошибка') + setSubmitting(false) + } + } + + return ( +
+
+
+ Vyryi +

Vyryi

+

Врата, за которыми нет возврата.

+ +
+ + + + + {(error || localError) && ( +

+ {localError ?? error} + {error && ( + + )} +

+ )} + + +
+
+
+ ) +} diff --git a/src/pages/GamePage.tsx b/src/pages/GamePage.tsx new file mode 100644 index 0000000..9a07d46 --- /dev/null +++ b/src/pages/GamePage.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react' +import { Link, Navigate, useParams } from 'react-router-dom' +import { api } from '../api' +import { useAuth } from '../auth' +import type { GameStub } from '../types' + +export function GamePage() { + const { slug } = useParams<{ slug: string }>() + const { user, loading } = useAuth() + const [game, setGame] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + if (!slug) return + void api + .getGame(slug) + .then(setGame) + .catch((err: unknown) => setError(err instanceof Error ? err.message : 'Не найдено')) + }, [slug]) + + if (!loading && user && !user.agreement_accepted) { + return + } + + return ( +
+ + ← Назад + + {error &&

{error}

} + {game && ( +
+ +

{game.title}

+

{game.description}

+

В разработке

+
+ )} +
+ ) +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000..16f67c5 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react' +import { Link, Navigate } from 'react-router-dom' +import { api } from '../api' +import { useAuth } from '../auth' +import type { GameStub } from '../types' +import { GATE_FROM_KEY } from '../vfx/gateCaps' + +const DEPOSIT_AMOUNTS = [100, 500, 1000] + +export function HomePage() { + const { user, loading, setUser } = useAuth() + const [games, setGames] = useState([]) + const [depositOpen, setDepositOpen] = useState(false) + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState(null) + const [fromGate, setFromGate] = useState(false) + + useEffect(() => { + void api.listGames().then(setGames).catch(() => setGames([])) + }, []) + + useEffect(() => { + try { + if (sessionStorage.getItem(GATE_FROM_KEY) === '1') { + sessionStorage.removeItem(GATE_FROM_KEY) + setFromGate(true) + } + } catch { + /* ignore */ + } + }, []) + + if (!loading && user && !user.agreement_accepted) { + return + } + + if (!loading && !user) { + return + } + + async function deposit(amount: number) { + setBusy(true) + setMessage(null) + try { + const wallet = await api.deposit(amount) + if (user) { + setUser({ ...user, balance: wallet.balance }) + } + setMessage(`Зачислено ${amount}`) + setDepositOpen(false) + } catch (err) { + setMessage(err instanceof Error ? err.message : 'Ошибка пополнения') + } finally { + setBusy(false) + } + } + + return ( +
+
+
+ + Vyryi +
+
+ Баланс + {user?.balance ?? '—'} + +
+
+ + {message &&

{message}

} + +
+

Слоты

+

Врата ещё закрыты — пока только предвестники.

+
    + {games.map((game) => ( +
  • + + {game.title} + Скоро + {game.description} + +
  • + ))} +
+
+ + {depositOpen && ( +
+
+

Пополнение

+

Тестовая заглушка. Вывод недоступен.

+
+ {DEPOSIT_AMOUNTS.map((amount) => ( + + ))} +
+ +
+
+ )} +
+ ) +} diff --git a/src/pages/LegalDocumentPage.tsx b/src/pages/LegalDocumentPage.tsx new file mode 100644 index 0000000..e7caa3a --- /dev/null +++ b/src/pages/LegalDocumentPage.tsx @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom' + +type LegalDocumentPageProps = { + title: string + paragraphs: string[] +} + +export function LegalDocumentPage({ title, paragraphs }: LegalDocumentPageProps) { + return ( +
+ + ← Назад + +
+

{title}

+ {paragraphs.map((text, index) => ( +

{text}

+ ))} +
+
+ ) +} diff --git a/src/pages/RootRedirect.tsx b/src/pages/RootRedirect.tsx new file mode 100644 index 0000000..8546d5b --- /dev/null +++ b/src/pages/RootRedirect.tsx @@ -0,0 +1,12 @@ +import { Navigate } from 'react-router-dom' +import { useAuth } from '../auth' + +/** `/` → home if agreement done, otherwise onboarding. */ +export function RootRedirect() { + const { user, loading } = useAuth() + if (loading) return null + if (user?.agreement_accepted) { + return + } + return +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..695fd8a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,33 @@ +export type User = { + id: number + telegram_id: number | null + username: string | null + first_name: string | null + balance: string + agreement_accepted: boolean + agreement_accepted_at: string | null + created_at: string +} + +export type Deposit = { + id: number + amount: string + created_at: string +} + +export type Wallet = { + balance: string + deposits: Deposit[] +} + +export type GameStub = { + slug: string + title: string + description: string + status: string +} + +export type TokenResponse = { + access_token: string + token_type: string +} diff --git a/src/vfx/GateTransitProvider.tsx b/src/vfx/GateTransitProvider.tsx new file mode 100644 index 0000000..3a3d6a1 --- /dev/null +++ b/src/vfx/GateTransitProvider.tsx @@ -0,0 +1,323 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from 'react' +import { createPortal } from 'react-dom' +import { useNavigate } from 'react-router-dom' +import gsap from 'gsap' +import { GATE_FROM_KEY, prefersReducedMotion, type GateTransitPhase } from './gateCaps' +import { + createLogoFlyTimeline, + createReducedLogoFlyTimeline, + type LogoFlyTargets, +} from './gateTimeline' + +type StartResult = { + ok: boolean + error?: string +} + +type GateTransitContextValue = { + phase: GateTransitPhase + active: boolean + start: (runAccept: () => Promise) => Promise +} + +const GateTransitContext = createContext(null) + +export function useGateTransit() { + const ctx = useContext(GateTransitContext) + if (!ctx) { + throw new Error('useGateTransit must be used within GateTransitProvider') + } + return ctx +} + +function collectUiFade(gate: HTMLElement, logo: HTMLElement | null): HTMLElement[] { + const nodes: HTMLElement[] = [] + const glow = gate.querySelector('.gate__glow') + if (glow) nodes.push(glow) + const content = gate.querySelector('.gate__content') + if (content) { + for (const el of Array.from(content.children)) { + if (el instanceof HTMLElement && el !== logo) nodes.push(el) + } + } + return nodes +} + +function placeHeroOverLogo(hero: HTMLElement, logo: HTMLElement) { + const rect = logo.getBoundingClientRect() + // Geometry only — keep invisible until the original logo is hidden in the same frame + gsap.set(hero, { + position: 'fixed', + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + margin: 0, + zIndex: 2, + opacity: 0, + visibility: 'hidden', + scale: 1, + rotation: 0, + z: 0, + filter: 'drop-shadow(0 10px 32px rgba(0, 0, 0, 0.6))', + objectFit: 'contain', + transformOrigin: '50% 46%', + transformPerspective: 1400, + force3D: true, + willChange: 'transform, filter, opacity', + }) +} + +function revealHeroOverLogo(hero: HTMLElement, logo: HTMLElement) { + gsap.set(logo, { opacity: 0 }) + gsap.set(hero, { opacity: 1, visibility: 'visible' }) +} + +export function GateTransitProvider({ children }: { children: ReactNode }) { + const navigate = useNavigate() + const [phase, setPhase] = useState('idle') + const [showOverlay, setShowOverlay] = useState(false) + + const veilRef = useRef(null) + const stageRef = useRef(null) + const heroRef = useRef(null) + const timelineRef = useRef(null) + const acceptPromiseRef = useRef | null>(null) + const acceptErrorRef = useRef(null) + const acceptOkRef = useRef(false) + const navigatedRef = useRef(false) + const resolveStartRef = useRef<((result: StartResult) => void) | null>(null) + const reducedRef = useRef(false) + + const cleanupDomLocks = useCallback(() => { + document.documentElement.classList.remove('gate-transit-lock') + const gate = document.querySelector('.gate') + const logo = document.querySelector('.gate__logo') + const glow = document.querySelector('.gate__glow') + if (logo) gsap.set(logo, { clearProps: 'opacity' }) + if (glow) gsap.set(glow, { clearProps: 'opacity,filter,transform' }) + if (gate) { + gate.querySelectorAll('.gate__content > *').forEach((el) => { + gsap.set(el, { clearProps: 'opacity,filter,transform' }) + }) + } + }, []) + + const finish = useCallback( + (result: StartResult) => { + timelineRef.current?.kill() + timelineRef.current = null + setShowOverlay(false) + setPhase(result.ok ? 'done' : 'error') + cleanupDomLocks() + resolveStartRef.current?.(result) + resolveStartRef.current = null + window.setTimeout(() => { + setPhase('idle') + acceptPromiseRef.current = null + acceptErrorRef.current = null + acceptOkRef.current = false + navigatedRef.current = false + }, 80) + }, + [cleanupDomLocks], + ) + + const handleBlackout = useCallback(async () => { + setPhase('blackout') + + const waitAccept = async () => { + if (acceptOkRef.current) return true + if (acceptErrorRef.current) return false + if (!acceptPromiseRef.current) return false + try { + await Promise.race([ + acceptPromiseRef.current, + new Promise((_, reject) => { + window.setTimeout(() => reject(new Error('Слишком долго. Попробуйте ещё раз.')), 1500) + }), + ]) + return acceptOkRef.current + } catch (err) { + if (!acceptErrorRef.current) { + acceptErrorRef.current = err instanceof Error ? err.message : 'Ошибка' + } + return false + } + } + + const ok = await waitAccept() + if (!ok) { + finish({ ok: false, error: acceptErrorRef.current ?? 'Ошибка' }) + return + } + + if (!navigatedRef.current) { + navigatedRef.current = true + try { + sessionStorage.setItem(GATE_FROM_KEY, '1') + } catch { + /* ignore */ + } + navigate('/home', { replace: true, state: { fromGate: true } }) + } + }, [finish, navigate]) + + const handleRevealStart = useCallback(() => { + setPhase('revealing') + }, []) + + const handleComplete = useCallback(() => { + finish({ ok: true }) + }, [finish]) + + const startTimeline = useCallback(() => { + const logo = document.querySelector('.gate__logo') + const gate = document.querySelector('.gate') + const hero = heroRef.current + if (!hero || !logo || !gate) { + finish({ ok: false, error: 'Не удалось запустить переход' }) + return + } + + placeHeroOverLogo(hero, logo) + + // One frame with hero invisible + correct box, then atomic swap with the page logo + requestAnimationFrame(() => { + if (timelineRef.current) return + placeHeroOverLogo(hero, logo) + revealHeroOverLogo(hero, logo) + + const targets: LogoFlyTargets = { + hero, + uiFade: collectUiFade(gate, logo), + originalLogo: logo, + veil: veilRef.current, + stage: stageRef.current, + } + + const handlers = { + onBlackout: () => { + void handleBlackout() + }, + onRevealStart: handleRevealStart, + onComplete: handleComplete, + } + + timelineRef.current = reducedRef.current + ? createReducedLogoFlyTimeline(targets, handlers) + : createLogoFlyTimeline(targets, handlers) + }) + }, [finish, handleBlackout, handleComplete, handleRevealStart]) + + useEffect(() => { + if (phase !== 'playing' || !showOverlay) return + + let cancelled = false + let tries = 0 + + const tick = () => { + if (cancelled || timelineRef.current) return + if (!heroRef.current || !document.querySelector('.gate__logo')) { + tries += 1 + if (tries < 30) { + window.setTimeout(tick, 32) + return + } + finish({ ok: false, error: 'Не удалось запустить переход' }) + return + } + startTimeline() + } + + const id = window.setTimeout(tick, 0) + return () => { + cancelled = true + window.clearTimeout(id) + } + }, [phase, showOverlay, startTimeline, finish]) + + const start = useCallback( + (runAccept: () => Promise) => { + if (phase !== 'idle' && phase !== 'done' && phase !== 'error') { + return Promise.resolve({ ok: false, error: 'Переход уже идёт' }) + } + + reducedRef.current = prefersReducedMotion() + setShowOverlay(true) + setPhase('playing') + document.documentElement.classList.add('gate-transit-lock') + + acceptOkRef.current = false + acceptErrorRef.current = null + navigatedRef.current = false + timelineRef.current = null + + const acceptPromise = runAccept() + .then(() => { + acceptOkRef.current = true + }) + .catch((err: unknown) => { + acceptErrorRef.current = err instanceof Error ? err.message : 'Ошибка' + throw err + }) + acceptPromiseRef.current = acceptPromise + void acceptPromise.catch(() => undefined) + + return new Promise((resolve) => { + resolveStartRef.current = resolve + }) + }, + [phase], + ) + + useEffect(() => { + return () => { + timelineRef.current?.kill() + cleanupDomLocks() + } + }, [cleanupDomLocks]) + + const value = useMemo( + () => ({ + phase, + active: phase === 'playing' || phase === 'blackout' || phase === 'revealing', + start, + }), + [phase, start], + ) + + const overlayVisible = + showOverlay && (phase === 'playing' || phase === 'blackout' || phase === 'revealing') + + return ( + + {children} + {overlayVisible && + createPortal( +
+
+ +
+
+
, + document.body, + )} + + ) +} diff --git a/src/vfx/gateCaps.ts b/src/vfx/gateCaps.ts new file mode 100644 index 0000000..458f676 --- /dev/null +++ b/src/vfx/gateCaps.ts @@ -0,0 +1,14 @@ +export type GateTransitPhase = + | 'idle' + | 'playing' + | 'blackout' + | 'revealing' + | 'done' + | 'error' + +export const GATE_FROM_KEY = 'vyryi:from-gate' + +export function prefersReducedMotion(): boolean { + if (typeof window === 'undefined') return false + return window.matchMedia('(prefers-reduced-motion: reduce)').matches +} diff --git a/src/vfx/gateTimeline.ts b/src/vfx/gateTimeline.ts new file mode 100644 index 0000000..81b72e0 --- /dev/null +++ b/src/vfx/gateTimeline.ts @@ -0,0 +1,130 @@ +import gsap from 'gsap' + +export type GateTimelineHandlers = { + onBlackout: () => void + onRevealStart: () => void + onComplete: () => void +} + +export type LogoFlyTargets = { + hero: HTMLElement + uiFade: HTMLElement[] + originalLogo: HTMLElement | null + veil: HTMLElement | null + stage: HTMLElement | null +} + +/** Continuously fly into logo.png (central black arch), then void → home. */ +export function createLogoFlyTimeline(targets: LogoFlyTargets, handlers: GateTimelineHandlers) { + const { hero, uiFade, veil, stage } = targets + + gsap.set(hero, { + transformOrigin: '50% 46%', + transformPerspective: 1400, + force3D: true, + scale: 1, + z: 0, + rotation: 0, + filter: 'brightness(1) contrast(1) drop-shadow(0 10px 32px rgba(0, 0, 0, 0.6))', + }) + + if (stage) { + gsap.set(stage, { perspective: 1400 }) + } + + if (veil) { + gsap.set(veil, { opacity: 0 }) + } + + const tl = gsap.timeline({ + onComplete: () => handlers.onComplete(), + }) + + if (uiFade.length) { + tl.to( + uiFade, + { + opacity: 0, + y: 18, + filter: 'blur(10px)', + duration: 0.4, + stagger: 0.04, + ease: 'power2.out', + }, + 0, + ) + } + + // Dolly into the black arch — image stays on screen the whole time + tl.to( + hero, + { + scale: 26, + z: 280, + rotation: 0.4, + duration: 2.15, + ease: 'power3.in', + force3D: true, + }, + 0.05, + ) + + tl.to( + hero, + { + filter: + 'brightness(0.35) contrast(1.25) drop-shadow(0 10px 32px rgba(0, 0, 0, 0.6))', + duration: 1.1, + ease: 'power2.in', + }, + 1.1, + ) + + if (veil) { + tl.to(veil, { opacity: 1, duration: 0.55, ease: 'power2.in' }, 1.75) + } + + tl.call(() => handlers.onBlackout(), undefined, 2.15) + tl.to({}, { duration: 0.2 }, 2.15) + tl.call(() => handlers.onRevealStart(), undefined, 2.35) + + if (veil) { + tl.to(veil, { opacity: 0, duration: 0.85, ease: 'power2.out' }, 2.35) + } + + tl.to(hero, { opacity: 0, duration: 0.35, ease: 'power1.in' }, 2.4) + + return tl +} + +export function createReducedLogoFlyTimeline( + targets: LogoFlyTargets, + handlers: GateTimelineHandlers, +) { + const { hero, uiFade, veil } = targets + + gsap.set(hero, { transformOrigin: '50% 46%', scale: 1 }) + if (veil) gsap.set(veil, { opacity: 0 }) + + const tl = gsap.timeline({ onComplete: () => handlers.onComplete() }) + + if (uiFade.length) { + tl.to(uiFade, { opacity: 0, duration: 0.3, ease: 'power1.out' }, 0) + } + + tl.to(hero, { scale: 14, duration: 0.85, ease: 'power2.in' }, 0) + if (veil) { + tl.to(veil, { opacity: 1, duration: 0.4, ease: 'power2.in' }, 0.45) + } + + tl.call(() => handlers.onBlackout(), undefined, 0.85) + tl.to({}, { duration: 0.15 }, 0.85) + tl.call(() => handlers.onRevealStart(), undefined, 1.0) + + if (veil) { + tl.to(veil, { opacity: 0, duration: 0.55, ease: 'power2.out' }, 1.0) + } + tl.to(hero, { opacity: 0, duration: 0.25 }, 1.0) + + return tl +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})