commit ed37565e253a1594291b74ace626add0f687456e Author: u1 Date: Tue Jan 6 12:33:47 2026 +0100 chore: initial import diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2adba53 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +**/node_modules +dist +**/dist +.env +tokens +**/tokens +*.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46f42c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.env +*.log +tokens/*.json +tokens/*.yml +tokens/*.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..02c27d3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-slim AS build + +WORKDIR /app/apps/visualizer + +COPY apps/visualizer/package.json apps/visualizer/package-lock.json ./ +RUN npm ci + +COPY apps/visualizer/ ./ +RUN npm run build + +FROM node:20-slim + +WORKDIR /app +RUN mkdir -p /tokens /srv + +COPY --from=build /app/apps/visualizer/dist /srv +COPY services/frontend/server.mjs /app/services/frontend/server.mjs + +ENV NODE_ENV=production +ENV PORT=8081 + +EXPOSE 8081 + +CMD ["node", "services/frontend/server.mjs"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2dac11e --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# trade-frontend + +Frontend (SPA) + prosty serwer (basic auth + proxy do `trade-api`). + +## Dev + +W tym repo app jest w `apps/visualizer/`. + +```bash +cd apps/visualizer +npm ci +npm run dev +``` + +## Docker + +```bash +docker build -t trade-frontend . +docker run --rm -p 8081:8081 trade-frontend +``` diff --git a/apps/visualizer/.env.example b/apps/visualizer/.env.example new file mode 100644 index 0000000..9e85877 --- /dev/null +++ b/apps/visualizer/.env.example @@ -0,0 +1,13 @@ +# Default: UI reads ticks from the same-origin API proxy at `/api`. +VITE_API_URL=/api + +# Fallback (optional): query Hasura directly (not recommended in browser). +VITE_HASURA_URL=http://localhost:8080/v1/graphql +# Optional (only if you intentionally query Hasura directly from the browser): +# VITE_HASURA_AUTH_TOKEN=YOUR_JWT +# VITE_HASURA_ADMIN_SECRET=devsecret +VITE_SYMBOL=PUMP-PERP +# Optional: filter by source (leave empty for all) +# VITE_SOURCE=drift_oracle +VITE_POLL_MS=1000 +VITE_LIMIT=300 diff --git a/apps/visualizer/index.html b/apps/visualizer/index.html new file mode 100644 index 0000000..66448ad --- /dev/null +++ b/apps/visualizer/index.html @@ -0,0 +1,13 @@ + + + + + + Trade Visualizer + + +
+ + + + diff --git a/apps/visualizer/package-lock.json b/apps/visualizer/package-lock.json new file mode 100644 index 0000000..38368dd --- /dev/null +++ b/apps/visualizer/package-lock.json @@ -0,0 +1,1735 @@ +{ + "name": "trade-visualizer", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trade-visualizer", + "dependencies": { + "chart.js": "^4.4.1", + "chartjs-adapter-luxon": "^1.3.1", + "lightweight-charts": "^5.0.8", + "luxon": "^3.5.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.6.3", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-luxon": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", + "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0", + "luxon": ">=1.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fancy-canvas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightweight-charts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-5.1.0.tgz", + "integrity": "sha512-jEAYR4ODYeyNZcWUigsoLTl52rbPmgXnvd5FLIv/ZoA/2sSDw63YKnef8n4yhzum7W926yHeFwlm7ididKb7YQ==", + "license": "Apache-2.0", + "dependencies": { + "fancy-canvas": "2.1.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/apps/visualizer/package.json b/apps/visualizer/package.json new file mode 100644 index 0000000..757fbe7 --- /dev/null +++ b/apps/visualizer/package.json @@ -0,0 +1,25 @@ +{ + "name": "trade-visualizer", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "chart.js": "^4.4.1", + "chartjs-adapter-luxon": "^1.3.1", + "lightweight-charts": "^5.0.8", + "luxon": "^3.5.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.6.3", + "vite": "^5.4.8" + } +} diff --git a/apps/visualizer/src/App.tsx b/apps/visualizer/src/App.tsx new file mode 100644 index 0000000..de07e8e --- /dev/null +++ b/apps/visualizer/src/App.tsx @@ -0,0 +1,435 @@ +import { useMemo } from 'react'; +import { useLocalStorageState } from './app/hooks/useLocalStorageState'; +import AppShell from './layout/AppShell'; +import ChartPanel from './features/chart/ChartPanel'; +import { useChartData } from './features/chart/useChartData'; +import TickerBar from './features/tickerbar/TickerBar'; +import Card from './ui/Card'; +import Tabs from './ui/Tabs'; +import MarketHeader from './features/market/MarketHeader'; +import Button from './ui/Button'; +import TopNav from './layout/TopNav'; + +function envNumber(name: string, fallback: number): number { + const v = (import.meta as any).env?.[name]; + if (v == null) return fallback; + const n = Number(v); + return Number.isFinite(n) ? n : fallback; +} + +function envString(name: string, fallback: string): string { + const v = (import.meta as any).env?.[name]; + return v == null ? fallback : String(v); +} + +function formatUsd(v: number | null | undefined): string { + if (v == null || !Number.isFinite(v)) return '—'; + if (v >= 1000) return `$${v.toFixed(0)}`; + if (v >= 1) return `$${v.toFixed(2)}`; + return `$${v.toPrecision(4)}`; +} + +function formatQty(v: number | null | undefined, decimals: number): string { + if (v == null || !Number.isFinite(v)) return '—'; + return v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); +} + +export default function App() { + const markets = useMemo(() => ['PUMP-PERP', 'SOL-PERP', 'BTC-PERP', 'ETH-PERP'], []); + + const [symbol, setSymbol] = useLocalStorageState('trade.symbol', envString('VITE_SYMBOL', 'PUMP-PERP')); + const [source, setSource] = useLocalStorageState('trade.source', envString('VITE_SOURCE', '')); + const [tf, setTf] = useLocalStorageState('trade.tf', envString('VITE_TF', '1m')); + const [pollMs, setPollMs] = useLocalStorageState('trade.pollMs', envNumber('VITE_POLL_MS', 1000)); + const [limit, setLimit] = useLocalStorageState('trade.limit', envNumber('VITE_LIMIT', 300)); + const [showIndicators, setShowIndicators] = useLocalStorageState('trade.showIndicators', true); + const [tab, setTab] = useLocalStorageState<'orderbook' | 'trades'>('trade.sidebarTab', 'orderbook'); + const [bottomTab, setBottomTab] = useLocalStorageState< + 'positions' | 'orders' | 'balances' | 'orderHistory' | 'positionHistory' + >('trade.bottomTab', 'positions'); + const [tradeSide, setTradeSide] = useLocalStorageState<'long' | 'short'>('trade.form.side', 'long'); + const [tradeOrderType, setTradeOrderType] = useLocalStorageState<'market' | 'limit' | 'other'>( + 'trade.form.type', + 'market' + ); + const [tradePrice, setTradePrice] = useLocalStorageState('trade.form.price', 0); + const [tradeSize, setTradeSize] = useLocalStorageState('trade.form.size', 0.1); + + const { candles, indicators, loading, error, refresh } = useChartData({ + symbol, + source: source.trim() ? source : undefined, + tf, + limit, + pollMs, + }); + + const latest = candles.length ? candles[candles.length - 1] : null; + const first = candles.length ? candles[0] : null; + const changePct = + first && latest && first.close > 0 ? ((latest.close - first.close) / first.close) * 100 : null; + + const orderbook = useMemo(() => { + if (!latest) return { asks: [], bids: [], mid: null as number | null }; + const mid = latest.close; + const step = Math.max(mid * 0.00018, 0.0001); + const levels = 14; + + const asksRaw = Array.from({ length: levels }, (_, i) => ({ + price: mid + (i + 1) * step, + size: 0.1 + ((i * 7) % 15) * 0.1, + })); + const bidsRaw = Array.from({ length: levels }, (_, i) => ({ + price: mid - (i + 1) * step, + size: 0.1 + ((i * 5) % 15) * 0.1, + })); + + let askTotal = 0; + const asks = asksRaw + .slice() + .reverse() + .map((r) => { + askTotal += r.size; + return { ...r, total: askTotal }; + }); + + let bidTotal = 0; + const bids = bidsRaw.map((r) => { + bidTotal += r.size; + return { ...r, total: bidTotal }; + }); + + return { asks, bids, mid }; + }, [latest]); + + const trades = useMemo(() => { + const slice = candles.slice(-24).reverse(); + return slice.map((c) => { + const isBuy = c.close >= c.open; + return { + time: c.time, + price: c.close, + size: c.volume ?? null, + side: isBuy ? ('buy' as const) : ('sell' as const), + }; + }); + }, [candles]); + + const effectiveTradePrice = useMemo(() => { + if (tradeOrderType === 'limit') return tradePrice; + return latest?.close ?? tradePrice; + }, [latest?.close, tradeOrderType, tradePrice]); + + const topItems = useMemo( + () => [ + { key: 'BTC', label: 'BTC', changePct: 1.28, active: false }, + { key: 'SOL', label: 'SOL', changePct: 1.89, active: false }, + ], + [] + ); + + const stats = useMemo(() => { + return [ + { + key: 'last', + label: 'Last', + value: formatUsd(latest?.close), + sub: + changePct == null ? ( + '—' + ) : ( + = 0 ? 'pos' : 'neg'}> + {changePct >= 0 ? '+' : ''} + {changePct.toFixed(2)}% + + ), + }, + { key: 'oracle', label: 'Oracle', value: formatUsd(latest?.oracle ?? null) }, + { key: 'funding', label: 'Funding / 24h', value: '—', sub: '—' }, + { key: 'oi', label: 'Open Interest', value: '—' }, + { key: 'vol', label: '24h Volume', value: '—' }, + { key: 'details', label: 'Market Details', value: View }, + ]; + }, [latest?.close, latest?.oracle, changePct]); + + const seriesLabel = useMemo(() => `Candles: Mark (oracle overlay)`, []); + + return ( + } + top={} + main={ +
+ + Source + setSource(e.target.value)} + placeholder="(any)" + /> + + } + stats={stats} + rightSlot={ +
+ + + +
+ } + /> + } + > + {error ?
{error}
: null} +
+ + setShowIndicators((v) => !v)} + seriesLabel={seriesLabel} + /> + + + Positions (next)
}, + { id: 'orders', label: 'Orders', content:
Orders (next)
}, + { id: 'balances', label: 'Balances', content:
Balances (next)
}, + { + id: 'orderHistory', + label: 'Order History', + content:
Order history (next)
, + }, + { + id: 'positionHistory', + label: 'Position History', + content:
Position history (next)
, + }, + ]} + activeId={bottomTab} + onChange={setBottomTab} + /> + + + } + sidebar={ + +
Orderbook
+
{loading ? 'loading…' : latest ? formatUsd(latest.close) : '—'}
+ + } + > + +
+ Price + Size + Total +
+
+ {orderbook.asks.map((r) => ( +
+ {formatQty(r.price, 3)} + {formatQty(r.size, 2)} + {formatQty(r.total, 2)} +
+ ))} +
+ {latest ? formatQty(latest.close, 3) : '—'} + mid +
+ {orderbook.bids.map((r) => ( +
+ {formatQty(r.price, 3)} + {formatQty(r.size, 2)} + {formatQty(r.total, 2)} +
+ ))} +
+ + ), + }, + { + id: 'trades', + label: 'Recent Trades', + content: ( +
+
+ Time + Price + Size +
+
+ {trades.map((t) => ( +
+ + {new Date(t.time * 1000).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })} + + + {formatQty(t.price, 3)} + + {t.size == null ? '—' : formatQty(t.size, 2)} +
+ ))} +
+
+ ), + }, + ]} + activeId={tab} + onChange={setTab} + /> +
+ } + rightbar={ + +
+ + +
+
{symbol}
+ + } + > +
+
+ + +
+ +
+ + + +
+ +
+ + +
+ + + +
+
+ Order Value + {effectiveTradePrice ? formatUsd(effectiveTradePrice * tradeSize) : '—'} +
+
+ Slippage (Dynamic) + +
+
+ Margin Required + +
+
+ Liq. Price + +
+
+
+
+ } + /> + ); +} diff --git a/apps/visualizer/src/app/hooks/useInterval.ts b/apps/visualizer/src/app/hooks/useInterval.ts new file mode 100644 index 0000000..387d743 --- /dev/null +++ b/apps/visualizer/src/app/hooks/useInterval.ts @@ -0,0 +1,15 @@ +import { useEffect, useRef } from 'react'; + +export function useInterval(callback: () => void, delayMs: number | null) { + const cbRef = useRef(callback); + cbRef.current = callback; + + useEffect(() => { + if (delayMs == null) return; + if (!Number.isFinite(delayMs) || delayMs < 0) return; + + const id = window.setInterval(() => cbRef.current(), delayMs); + return () => window.clearInterval(id); + }, [delayMs]); +} + diff --git a/apps/visualizer/src/app/hooks/useLocalStorageState.ts b/apps/visualizer/src/app/hooks/useLocalStorageState.ts new file mode 100644 index 0000000..a15025c --- /dev/null +++ b/apps/visualizer/src/app/hooks/useLocalStorageState.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +export function useLocalStorageState(key: string, initialValue: T) { + const [value, setValue] = useState(() => { + try { + const raw = window.localStorage.getItem(key); + if (raw == null) return initialValue; + return JSON.parse(raw) as T; + } catch { + return initialValue; + } + }); + + useEffect(() => { + try { + window.localStorage.setItem(key, JSON.stringify(value)); + } catch { + // ignore + } + }, [key, value]); + + return [value, setValue] as const; +} + diff --git a/apps/visualizer/src/components/PriceChart.tsx b/apps/visualizer/src/components/PriceChart.tsx new file mode 100644 index 0000000..d6ffe7e --- /dev/null +++ b/apps/visualizer/src/components/PriceChart.tsx @@ -0,0 +1,188 @@ +import { useEffect, useMemo, useRef } from 'react'; +import Chart from 'chart.js/auto'; +import 'chartjs-adapter-luxon'; +import type { DriftTick } from '../lib/hasura'; +import { bollingerBands, ema, lastNonNull, macd, rsi, sma } from '../lib/indicators'; + +type Props = { + ticks: DriftTick[]; +}; + +function toPoints(ticks: DriftTick[], values: Array | number[]) { + return ticks.map((t, i) => ({ + x: t.ts, + y: (values as any)[i] ?? null, + })); +} + +export default function PriceChart({ ticks }: Props) { + const canvasRef = useRef(null); + const chartRef = useRef(null); + + const oracleValues = useMemo(() => ticks.map((t) => t.oracle_price), [ticks]); + const markSeries = useMemo(() => ticks.map((t) => (t.mark_price == null ? null : t.mark_price)), [ticks]); + const basePriceValues = useMemo( + () => ticks.map((t) => (t.mark_price == null ? t.oracle_price : t.mark_price)), + [ticks] + ); + + const sma20 = useMemo(() => sma(basePriceValues, 20), [basePriceValues]); + const ema20 = useMemo(() => ema(basePriceValues, 20), [basePriceValues]); + const rsi14 = useMemo(() => rsi(basePriceValues, 14), [basePriceValues]); + const bb = useMemo(() => bollingerBands(basePriceValues, 20, 2), [basePriceValues]); + const macdOut = useMemo(() => macd(basePriceValues, 12, 26, 9), [basePriceValues]); + + const latest = ticks[ticks.length - 1]; + const valuesPanel = useMemo(() => { + if (!latest) return null; + const mark = latest.mark_price == null ? null : Number(latest.mark_price); + const oracle = latest.oracle_price; + return { + ts: latest.ts, + oracle, + mark, + spread: mark == null ? null : mark - oracle, + sma20: lastNonNull(sma20), + ema20: lastNonNull(ema20), + rsi14: lastNonNull(rsi14), + upper: lastNonNull(bb.upper), + lower: lastNonNull(bb.lower), + macd: lastNonNull(macdOut.macd), + signal: lastNonNull(macdOut.signal), + }; + }, [latest, sma20, ema20, rsi14, bb.upper, bb.lower, macdOut.macd, macdOut.signal]); + + useEffect(() => { + if (!canvasRef.current) return; + if (chartRef.current) return; + + chartRef.current = new Chart(canvasRef.current, { + type: 'line', + data: { + datasets: [ + { label: 'Mark', data: [], borderColor: '#60a5fa', yAxisID: 'price', pointRadius: 0 }, + { + label: 'Oracle', + data: [], + borderColor: '#fb923c', + borderDash: [6, 4], + yAxisID: 'price', + pointRadius: 0, + }, + { label: 'SMA 20', data: [], borderColor: '#f87171', yAxisID: 'price', pointRadius: 0 }, + { label: 'EMA 20', data: [], borderColor: '#34d399', yAxisID: 'price', pointRadius: 0 }, + { label: 'RSI 14', data: [], borderColor: '#c084fc', yAxisID: 'rsi', pointRadius: 0 }, + { label: 'Upper BB', data: [], borderColor: '#fbbf24', yAxisID: 'price', pointRadius: 0 }, + { label: 'Lower BB', data: [], borderColor: '#a3a3a3', yAxisID: 'price', pointRadius: 0 }, + { label: 'MACD', data: [], borderColor: '#22d3ee', yAxisID: 'macd', pointRadius: 0 }, + { label: 'Signal', data: [], borderColor: '#f472b6', yAxisID: 'macd', pointRadius: 0 }, + ], + }, + options: { + responsive: true, + animation: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { + labels: { color: '#e6e9ef' }, + }, + tooltip: { + enabled: true, + }, + }, + scales: { + x: { + type: 'time', + time: { unit: 'second' }, + ticks: { color: '#c7cbd4' }, + grid: { color: 'rgba(255,255,255,0.06)' }, + }, + price: { + type: 'linear', + position: 'right', + ticks: { color: '#c7cbd4' }, + grid: { color: 'rgba(255,255,255,0.06)' }, + }, + rsi: { + type: 'linear', + position: 'left', + min: 0, + max: 100, + ticks: { color: '#c7cbd4' }, + grid: { drawOnChartArea: false }, + }, + macd: { + type: 'linear', + position: 'left', + ticks: { color: '#c7cbd4' }, + grid: { drawOnChartArea: false }, + }, + }, + }, + }); + + return () => { + chartRef.current?.destroy(); + chartRef.current = null; + }; + }, []); + + useEffect(() => { + const chart = chartRef.current; + if (!chart) return; + + const markPts = toPoints(ticks, markSeries); + const oraclePts = toPoints(ticks, oracleValues); + const smaPts = toPoints(ticks, sma20); + const emaPts = toPoints(ticks, ema20); + const rsiPts = toPoints(ticks, rsi14); + const upperPts = toPoints(ticks, bb.upper); + const lowerPts = toPoints(ticks, bb.lower); + const macdPts = toPoints(ticks, macdOut.macd); + const sigPts = toPoints(ticks, macdOut.signal); + + chart.data.datasets[0].data = markPts as any; + chart.data.datasets[1].data = oraclePts as any; + chart.data.datasets[2].data = smaPts as any; + chart.data.datasets[3].data = emaPts as any; + chart.data.datasets[4].data = rsiPts as any; + chart.data.datasets[5].data = upperPts as any; + chart.data.datasets[6].data = lowerPts as any; + chart.data.datasets[7].data = macdPts as any; + chart.data.datasets[8].data = sigPts as any; + + chart.update('none'); + }, [ticks, markSeries, oracleValues, sma20, ema20, rsi14, bb.upper, bb.lower, macdOut.macd, macdOut.signal]); + + return ( +
+ {valuesPanel && ( +
+
+ latest @ {valuesPanel.ts} +
+
Oracle: {valuesPanel.oracle.toFixed(6)}
+
Mark: {valuesPanel.mark?.toFixed(6) ?? '-'}
+
Mark-Oracle: {valuesPanel.spread?.toFixed(6) ?? '-'}
+
SMA 20: {valuesPanel.sma20?.toFixed(6) ?? '-'}
+
EMA 20: {valuesPanel.ema20?.toFixed(6) ?? '-'}
+
RSI 14: {valuesPanel.rsi14?.toFixed(2) ?? '-'}
+
Upper BB: {valuesPanel.upper?.toFixed(6) ?? '-'}
+
Lower BB: {valuesPanel.lower?.toFixed(6) ?? '-'}
+
MACD: {valuesPanel.macd?.toFixed(6) ?? '-'}
+
Signal: {valuesPanel.signal?.toFixed(6) ?? '-'}
+
+ )} + +
+ ); +} diff --git a/apps/visualizer/src/features/chart/ChartIcons.tsx b/apps/visualizer/src/features/chart/ChartIcons.tsx new file mode 100644 index 0000000..74d051b --- /dev/null +++ b/apps/visualizer/src/features/chart/ChartIcons.tsx @@ -0,0 +1,182 @@ +import type { SVGAttributes } from 'react'; + +type IconProps = SVGAttributes & { title?: string }; + +function Svg({ title, children, ...rest }: IconProps) { + return ( + + {title ? {title} : null} + {children} + + ); +} + +export function IconCursor(props: IconProps) { + return ( + + + + ); +} + +export function IconCrosshair(props: IconProps) { + return ( + + + + + ); +} + +export function IconPlus(props: IconProps) { + return ( + + + + ); +} + +export function IconTrendline(props: IconProps) { + return ( + + + + + + + + ); +} + +export function IconFib(props: IconProps) { + return ( + + + + + + ); +} + +export function IconBrush(props: IconProps) { + return ( + + + + + ); +} + +export function IconText(props: IconProps) { + return ( + + + + ); +} + +export function IconSmile(props: IconProps) { + return ( + + + + + + ); +} + +export function IconRuler(props: IconProps) { + return ( + + + + + ); +} + +export function IconZoom(props: IconProps) { + return ( + + + + + + ); +} + +export function IconZoomOut(props: IconProps) { + return ( + + + + + + ); +} + +export function IconLock(props: IconProps) { + return ( + + + + + ); +} + +export function IconEye(props: IconProps) { + return ( + + + + + ); +} + +export function IconTrash(props: IconProps) { + return ( + + + + + + + ); +} + +export function IconResetView(props: IconProps) { + return ( + + + + + ); +} diff --git a/apps/visualizer/src/features/chart/ChartPanel.tsx b/apps/visualizer/src/features/chart/ChartPanel.tsx new file mode 100644 index 0000000..8151989 --- /dev/null +++ b/apps/visualizer/src/features/chart/ChartPanel.tsx @@ -0,0 +1,177 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import type { Candle, ChartIndicators } from '../../lib/api'; +import Card from '../../ui/Card'; +import ChartSideToolbar from './ChartSideToolbar'; +import ChartToolbar from './ChartToolbar'; +import TradingChart from './TradingChart'; +import type { FibAnchor, FibRetracement } from './FibRetracementPrimitive'; +import type { IChartApi } from 'lightweight-charts'; + +type Props = { + candles: Candle[]; + indicators: ChartIndicators; + timeframe: string; + onTimeframeChange: (tf: string) => void; + showIndicators: boolean; + onToggleIndicators: () => void; + seriesLabel: string; +}; + +export default function ChartPanel({ + candles, + indicators, + timeframe, + onTimeframeChange, + showIndicators, + onToggleIndicators, + seriesLabel, +}: Props) { + const [isFullscreen, setIsFullscreen] = useState(false); + const [activeTool, setActiveTool] = useState<'cursor' | 'fib-retracement'>('cursor'); + const [fibStart, setFibStart] = useState(null); + const [fib, setFib] = useState(null); + const [fibDraft, setFibDraft] = useState(null); + const chartApiRef = useRef(null); + const activeToolRef = useRef(activeTool); + const fibStartRef = useRef(fibStart); + const pendingMoveRef = useRef(null); + const rafRef = useRef(null); + + useEffect(() => { + if (!isFullscreen) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsFullscreen(false); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [isFullscreen]); + + useEffect(() => { + document.body.classList.toggle('chartFullscreen', isFullscreen); + return () => document.body.classList.remove('chartFullscreen'); + }, [isFullscreen]); + + const cardClassName = useMemo(() => { + return ['chartCard', isFullscreen ? 'chartCard--fullscreen' : null].filter(Boolean).join(' '); + }, [isFullscreen]); + + useEffect(() => { + activeToolRef.current = activeTool; + if (activeTool !== 'fib-retracement') { + setFibStart(null); + setFibDraft(null); + } + }, [activeTool]); + + useEffect(() => { + fibStartRef.current = fibStart; + }, [fibStart]); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Escape') return; + if (activeToolRef.current !== 'fib-retracement') return; + setFibStart(null); + setFibDraft(null); + setActiveTool('cursor'); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, []); + + useEffect(() => { + return () => { + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + }; + }, []); + + function zoomTime(factor: number) { + const chart = chartApiRef.current; + if (!chart) return; + const ts = chart.timeScale(); + const range = ts.getVisibleLogicalRange(); + if (!range) return; + const from = range.from as number; + const to = range.to as number; + const span = Math.max(5, (to - from) * factor); + const center = (from + to) / 2; + ts.setVisibleLogicalRange({ from: center - span / 2, to: center + span / 2 }); + } + + return ( + <> + {isFullscreen ?
setIsFullscreen(false)} /> : null} + +
+ setIsFullscreen((v) => !v)} + /> +
+
+ zoomTime(0.8)} + onZoomOut={() => zoomTime(1.25)} + onResetView={() => chartApiRef.current?.timeScale().resetTimeScale()} + onClearFib={() => { + setFib(null); + setFibStart(null); + setFibDraft(null); + }} + /> +
+ { + chartApiRef.current = chart; + }} + onChartClick={(p) => { + if (activeTool !== 'fib-retracement') return; + if (!fibStartRef.current) { + fibStartRef.current = p; + setFibStart(p); + setFibDraft({ a: p, b: p }); + return; + } + setFib({ a: fibStartRef.current, b: p }); + setFibStart(null); + fibStartRef.current = null; + setFibDraft(null); + setActiveTool('cursor'); + }} + onChartCrosshairMove={(p) => { + if (activeToolRef.current !== 'fib-retracement') return; + const start = fibStartRef.current; + if (!start) return; + pendingMoveRef.current = p; + if (rafRef.current != null) return; + rafRef.current = window.requestAnimationFrame(() => { + rafRef.current = null; + const move = pendingMoveRef.current; + const start2 = fibStartRef.current; + if (!move || !start2) return; + setFibDraft({ a: start2, b: move }); + }); + }} + /> +
+
+
+ + ); +} diff --git a/apps/visualizer/src/features/chart/ChartSideToolbar.tsx b/apps/visualizer/src/features/chart/ChartSideToolbar.tsx new file mode 100644 index 0000000..f41bcc7 --- /dev/null +++ b/apps/visualizer/src/features/chart/ChartSideToolbar.tsx @@ -0,0 +1,220 @@ +import { useEffect, useMemo, useState } from 'react'; +import ChartToolMenu, { type ToolMenuSection } from './ChartToolMenu'; +import { + IconBrush, + IconCrosshair, + IconCursor, + IconEye, + IconFib, + IconLock, + IconPlus, + IconRuler, + IconSmile, + IconText, + IconResetView, + IconTrash, + IconTrendline, + IconZoom, + IconZoomOut, +} from './ChartIcons'; + +type ActiveTool = 'cursor' | 'fib-retracement'; + +type Props = { + timeframe: string; + activeTool: ActiveTool; + hasFib: boolean; + onToolChange: (tool: ActiveTool) => void; + onZoomIn: () => void; + onZoomOut: () => void; + onResetView: () => void; + onClearFib: () => void; +}; + +export default function ChartSideToolbar({ + timeframe, + activeTool, + hasFib, + onToolChange, + onZoomIn, + onZoomOut, + onResetView, + onClearFib, +}: Props) { + const [openMenu, setOpenMenu] = useState<'fib' | null>(null); + + useEffect(() => { + if (!openMenu) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpenMenu(null); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [openMenu]); + + const fibMenuSections: ToolMenuSection[] = useMemo( + () => [ + { + id: 'fib', + title: 'FIBONACCI', + items: [ + { id: 'fib-retracement', label: 'Fib Retracement', icon: , shortcut: 'Click 2 points' }, + { id: 'fib-tb-ext', label: 'Trend-Based Fib Extension', icon: }, + { id: 'fib-channel', label: 'Fib Channel', icon: }, + { id: 'fib-time-zone', label: 'Fib Time Zone', icon: }, + { id: 'fib-speed-fan', label: 'Fib Speed Resistance Fan', icon: }, + { id: 'fib-tb-time', label: 'Trend-Based Fib Time', icon: }, + { id: 'fib-circles', label: 'Fib Circles', icon: }, + { id: 'fib-spiral', label: 'Fib Spiral', icon: }, + { id: 'fib-speed-arcs', label: 'Fib Speed Resistance Arcs', icon: }, + { id: 'fib-wedge', label: 'Fib Wedge', icon: }, + { id: 'pitchfan', label: 'Pitchfan', icon: }, + ], + }, + { + id: 'gann', + title: 'GANN', + items: [ + { id: 'gann-box', label: 'Gann Box', icon: }, + { id: 'gann-square-fixed', label: 'Gann Square Fixed', icon: }, + { id: 'gann-fan', label: 'Gann Fan', icon: }, + ], + }, + ], + [] + ); + + return ( +
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + {openMenu === 'fib' ? ( + <> +
setOpenMenu(null)} /> + { + if (id === 'fib-retracement') onToolChange('fib-retracement'); + setOpenMenu(null); + }} + /> + + ) : null} +
+ ); +} diff --git a/apps/visualizer/src/features/chart/ChartToolMenu.tsx b/apps/visualizer/src/features/chart/ChartToolMenu.tsx new file mode 100644 index 0000000..e7bcabb --- /dev/null +++ b/apps/visualizer/src/features/chart/ChartToolMenu.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from 'react'; + +export type ToolMenuItem = { + id: string; + label: string; + icon: ReactNode; + shortcut?: string; +}; + +export type ToolMenuSection = { + id: string; + title: string; + items: ToolMenuItem[]; +}; + +type Props = { + timeframeLabel: string; + sections: ToolMenuSection[]; + onSelectItem?: (id: string) => void; +}; + +export default function ChartToolMenu({ timeframeLabel, sections, onSelectItem }: Props) { + return ( +
+
+
{timeframeLabel}
+
+ +
+ {sections.map((s) => ( +
+
{s.title}
+
+ {s.items.map((it) => ( + + ))} +
+
+ ))} +
+
+ ); +} diff --git a/apps/visualizer/src/features/chart/ChartToolbar.tsx b/apps/visualizer/src/features/chart/ChartToolbar.tsx new file mode 100644 index 0000000..71ed170 --- /dev/null +++ b/apps/visualizer/src/features/chart/ChartToolbar.tsx @@ -0,0 +1,51 @@ +import Button from '../../ui/Button'; + +type Props = { + timeframe: string; + onTimeframeChange: (tf: string) => void; + showIndicators: boolean; + onToggleIndicators: () => void; + seriesLabel: string; + isFullscreen: boolean; + onToggleFullscreen: () => void; +}; + +const timeframes = ['1m', '5m', '15m', '1h', '4h', '1D'] as const; + +export default function ChartToolbar({ + timeframe, + onTimeframeChange, + showIndicators, + onToggleIndicators, + seriesLabel, + isFullscreen, + onToggleFullscreen, +}: Props) { + return ( +
+
+ {timeframes.map((tf) => ( + + ))} +
+ +
+ + +
{seriesLabel}
+
+
+ ); +} diff --git a/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts b/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts new file mode 100644 index 0000000..c058add --- /dev/null +++ b/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts @@ -0,0 +1,199 @@ +import type { + IPrimitivePaneRenderer, + IPrimitivePaneView, + ISeriesApi, + ISeriesPrimitive, + SeriesAttachedParameter, + Time, +} from 'lightweight-charts'; + +export type FibAnchor = { + logical: number; + price: number; +}; + +export type FibRetracement = { + a: FibAnchor; + b: FibAnchor; +}; + +type FibLevel = { + ratio: number; + line: string; + fill: string; +}; + +const LEVELS: FibLevel[] = [ + { ratio: 4.236, line: 'rgba(255, 45, 85, 0.95)', fill: 'rgba(255, 45, 85, 0.16)' }, + { ratio: 3.618, line: 'rgba(192, 132, 252, 0.95)', fill: 'rgba(192, 132, 252, 0.14)' }, + { ratio: 2.618, line: 'rgba(239, 68, 68, 0.92)', fill: 'rgba(239, 68, 68, 0.14)' }, + { ratio: 1.618, line: 'rgba(59, 130, 246, 0.92)', fill: 'rgba(59, 130, 246, 0.14)' }, + { ratio: 1.0, line: 'rgba(148, 163, 184, 0.92)', fill: 'rgba(59, 130, 246, 0.10)' }, + { ratio: 0.786, line: 'rgba(96, 165, 250, 0.92)', fill: 'rgba(96, 165, 250, 0.10)' }, + { ratio: 0.618, line: 'rgba(6, 182, 212, 0.92)', fill: 'rgba(6, 182, 212, 0.10)' }, + { ratio: 0.5, line: 'rgba(34, 197, 94, 0.92)', fill: 'rgba(34, 197, 94, 0.09)' }, + { ratio: 0.382, line: 'rgba(245, 158, 11, 0.92)', fill: 'rgba(245, 158, 11, 0.10)' }, + { ratio: 0.236, line: 'rgba(249, 115, 22, 0.92)', fill: 'rgba(249, 115, 22, 0.10)' }, + { ratio: 0.0, line: 'rgba(163, 163, 163, 0.85)', fill: 'rgba(163, 163, 163, 0.06)' }, +]; + +function formatRatio(r: number): string { + if (Number.isInteger(r)) return String(r); + const s = r.toFixed(3); + return s.replace(/0+$/, '').replace(/\.$/, ''); +} + +function formatPrice(p: number): string { + if (!Number.isFinite(p)) return '—'; + if (Math.abs(p) >= 1000) return p.toFixed(0); + if (Math.abs(p) >= 1) return p.toFixed(2); + return p.toPrecision(4); +} + +type State = { + fib: FibRetracement | null; + series: ISeriesApi<'Candlestick', Time> | null; + chart: SeriesAttachedParameter