diff --git a/.docker/Dockerfile b/.docker/Dockerfile index cae842eb1..df4a4278f 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /go/src/github.com/canopy-network/canopy COPY . /go/src/github.com/canopy-network/canopy RUN make build/wallet +RUN make build/new-wallet RUN make build/explorer RUN go build -a -o bin ./cmd/main/... diff --git a/.docker/compose.yaml b/.docker/compose.yaml index cd6f02ef3..fbef16baf 100644 --- a/.docker/compose.yaml +++ b/.docker/compose.yaml @@ -9,19 +9,19 @@ services: - 50001:50001 # Explorer - 50002:50002 # RPC - 50003:50003 # Admin RPC - - 9001:9001 # TCP P2P - - 6060:6060 # Debug - - 9090:9090 # Metrics + - 9001:9001 # TCP P2P + - 6060:6060 # Debug + - 9090:9090 # Metrics networks: - canopy command: ["start"] volumes: - ./volumes/node_1:/root/.canopy -# deploy: -# resources: -# limits: -# memory: 2G -# cpus: "1.0" + # deploy: + # resources: + # limits: + # memory: 2G + # cpus: "1.0" node-2: container_name: node-2 @@ -33,9 +33,9 @@ services: - 40001:40001 # Explorer - 40002:40002 # RPC - 40003:40003 # Admin RPC - - 9002:9002 # TCP P2P - - 6061:6060 # Debug - - 9091:9091 # Metrics + - 9002:9002 # TCP P2P + - 6061:6060 # Debug + - 9091:9091 # Metrics networks: - canopy command: ["start"] @@ -72,4 +72,4 @@ services: networks: canopy: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..8e6924856 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM golang:1.24-alpine AS builder + +RUN apk update && apk add --no-cache make bash nodejs npm + +ARG BIN_PATH + +WORKDIR /go/src/github.com/canopy-network/canopy +COPY . /go/src/github.com/canopy-network/canopy + +RUN make build/wallet +RUN make build/explorer +RUN CGO_ENABLED=0 GOOS=linux go build -a -o bin ./cmd/auto-update/. + +# Only build if the file at ${BIN_PATH} doesn't already exist +RUN if [ ! -f "${BIN_PATH}" ]; then \ + echo "File ${BIN_PATH} not found. Building it..."; \ + CGO_ENABLED=0 GOOS=linux go build -a -o "${BIN_PATH}" ./cmd/main/...; \ + else \ + echo "File ${BIN_PATH} already exists. Skipping build."; \ + fi + +FROM alpine:3.19 + +RUN apk add --no-cache pigz ca-certificates + +ARG BIN_PATH + +WORKDIR /app +COPY --from=builder /go/src/github.com/canopy-network/canopy/bin ./ +COPY --from=builder /go/src/github.com/canopy-network/canopy/${BIN_PATH} ${BIN_PATH} +RUN chmod +x ${BIN_PATH} +ENTRYPOINT ["/app/bin"] diff --git a/Makefile b/Makefile index 9d21fc26a..e4fa09065 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Variables GO_BIN_DIR := ~/go/bin CLI_DIR := ./cmd/main/... -WALLET_DIR := ./cmd/rpc/web/wallet +WALLET_DIR := ./cmd/rpc/web/wallet-new EXPLORER_DIR := ./cmd/rpc/web/explorer DOCKER_DIR := ./.docker/compose.yaml @@ -34,6 +34,9 @@ build/canopy-full: build/wallet build/explorer build/canopy build/wallet: npm install --prefix $(WALLET_DIR) && npm run build --prefix $(WALLET_DIR) +## build/new-wallet: alias for build/wallet (for backward compatibility) +build/new-wallet: build/wallet + ## build/explorer: build the canopy's explorer project build/explorer: npm install --prefix $(EXPLORER_DIR) && npm run build --prefix $(EXPLORER_DIR) diff --git a/cmd/rpc/server.go b/cmd/rpc/server.go index ff60be951..e4cf7448f 100644 --- a/cmd/rpc/server.go +++ b/cmd/rpc/server.go @@ -37,7 +37,7 @@ const ( ContentType = "Content-MessageType" ApplicationJSON = "application/json; charset=utf-8" - walletStaticDir = "web/wallet/out" + walletStaticDir = "web/wallet-new/out" explorerStaticDir = "web/explorer/out" ) @@ -305,10 +305,10 @@ func (h logHandler) Handle(resp http.ResponseWriter, req *http.Request, p httpro //go:embed all:web/explorer/out var explorerFS embed.FS -//go:embed all:web/wallet/out +//go:embed all:web/wallet-new/out var walletFS embed.FS -// runStaticFileServer creates a web server serving static files +// runStaticFileServer creates a web server serving static files with SPA fallback func (s *Server) runStaticFileServer(fileSys fs.FS, dir, port string, conf lib.Config) { // Attempt to get a sub-filesystem rooted at the specified directory distFS, err := fs.Sub(fileSys, dir) @@ -322,20 +322,13 @@ func (s *Server) runStaticFileServer(fileSys fs.FS, dir, port string, conf lib.C // Define a handler function for the root path mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // serve `index.html` with dynamic config injection - if r.URL.Path == "/" || r.URL.Path == "/index.html" { + requestedPath := r.URL.Path + // Helper function to serve index.html with config injection + serveIndexHTML := func() { // Construct the file path for `index.html` filePath := path.Join(dir, "index.html") - // Open the file and defer closing until the function exits - data, e := fileSys.Open(filePath) - if e != nil { - http.NotFound(w, r) - return - } - defer data.Close() - // Read the content of `index.html` into a byte slice htmlBytes, e := fs.ReadFile(fileSys, filePath) if e != nil { @@ -350,11 +343,39 @@ func (s *Server) runStaticFileServer(fileSys fs.FS, dir, port string, conf lib.C w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusOK) w.Write([]byte(injectedHTML)) + } + + // Serve index.html for root path + if requestedPath == "/" || requestedPath == "/index.html" { + serveIndexHTML() + return + } + + // Check if the requested path has a file extension (indicates static asset) + // Common static asset extensions: .js, .css, .svg, .png, .jpg, .jpeg, .gif, .ico, .woff, .woff2, .ttf, .eot, .map + ext := path.Ext(requestedPath) + isStaticAsset := ext != "" + + if isStaticAsset { + // Try to serve the static asset from the file system + // Remove leading slash for fs.Open + assetPath := strings.TrimPrefix(requestedPath, "/") + + // Check if the file exists in the embedded filesystem + if _, err := distFS.Open(assetPath); err == nil { + // File exists, serve it + http.FileServer(http.FS(distFS)).ServeHTTP(w, r) + return + } + + // Static asset not found, return 404 + http.NotFound(w, r) return } - // For all other requests, serve the files directly from the file system - http.FileServer(http.FS(distFS)).ServeHTTP(w, r) + // For all other requests (no file extension = HTML navigation), + // serve index.html to enable SPA client-side routing + serveIndexHTML() }) // Start the HTTP server in a new goroutine and listen on the specified port diff --git a/cmd/rpc/web/wallet-new/.env.example b/cmd/rpc/web/wallet-new/.env.example new file mode 100644 index 000000000..9a8414c4f --- /dev/null +++ b/cmd/rpc/web/wallet-new/.env.example @@ -0,0 +1,16 @@ +# Vite Base Path Configuration +# This sets the base URL path for the application in production builds +# +# Examples: +# - For deployment at https://example.com/wallet/ use: VITE_BASE_PATH=/wallet/ +# - For deployment at https://wallet.example.com/ use: VITE_BASE_PATH=/ +# - For deployment at root domain use: VITE_BASE_PATH=/ +# +# Default: /wallet/ +VITE_BASE_PATH=/wallet/ +VITE_BASE_URL=/wallet + +# Node Environment +# Options: development | production +# Default: development +VITE_NODE_ENV=development diff --git a/cmd/rpc/web/wallet-new/.gitignore b/cmd/rpc/web/wallet-new/.gitignore new file mode 100644 index 000000000..da1c77f62 --- /dev/null +++ b/cmd/rpc/web/wallet-new/.gitignore @@ -0,0 +1,10 @@ +node_modules +out +vite.config.ts.* +.idea +dist +*.tsbuildinfo + +# Compiled JS files (TypeScript generates these) +src/**/*.js +src/**/*.jsx diff --git a/cmd/rpc/web/wallet-new/README.md b/cmd/rpc/web/wallet-new/README.md new file mode 100644 index 000000000..ff9d60cf2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/README.md @@ -0,0 +1,447 @@ +# Canopy Wallet + +A modern, **config-first blockchain wallet** built with React, TypeScript, and Tailwind CSS. The wallet features a dynamic, configuration-driven architecture where blockchain interactions, UI forms, and data sources are defined through JSON configuration files rather than hardcoded application logic. + +## 🌟 Features + +### Core Functionality +- **Multi-Account Management**: Create, import, and manage multiple blockchain accounts +- **Transaction Management**: Send, receive, and track transactions with real-time status updates +- **Staking Operations**: Stake, unstake, pause/unpause validators with comprehensive management tools +- **Governance Participation**: Vote on proposals and create new governance proposals +- **Real-time Monitoring**: Monitor node performance, network peers, system resources, and logs + +### Architecture Highlights +- **Config-First Approach**: All blockchain interactions defined in `chain.json` and `manifest.json` +- **Data Source (DS) Pattern**: Centralized API configuration and caching +- **Dynamic Form Generation**: Transaction forms generated from JSON configuration +- **Real-time Updates**: Live data updates using React Query with configurable intervals +- **Responsive Design**: Modern UI with dark theme and responsive layouts + +## 🏗️ Architecture Overview + +### Config-First Design +The wallet operates on a **config-first** principle where blockchain-specific configurations are externalized into JSON files: + +``` +public/plugin/canopy/ +├── chain.json # RPC endpoints, data sources, parameters +└── manifest.json # Transaction forms, UI definitions, actions +``` + +### Data Source (DS) Pattern +All API calls use a centralized DS system defined in `chain.json`: + +```json +{ + "ds": { + "height": { + "source": { "base": "rpc", "path": "/v1/query/height", "method": "POST" }, + "selector": "", + "coerce": { "response": { "": "int" } } + }, + "admin": { + "consensusInfo": { + "source": { "base": "admin", "path": "/v1/admin/consensus-info", "method": "GET" } + } + } + } +} +``` + +### Component Structure +``` +src/ +├── app/ +│ ├── pages/ # Main application pages +│ └── providers/ # React context providers +├── components/ +│ ├── dashboard/ # Dashboard widgets +│ ├── monitoring/ # Node monitoring components +│ ├── staking/ # Staking management UI +│ └── ui/ # Reusable UI components +├── hooks/ # Custom React hooks +├── core/ # Core utilities and DS system +└── manifest/ # Manifest parsing and types +``` + +## 🚀 Getting Started + +### Prerequisites +- Node.js 18+ and npm/pnpm +- A running Canopy node with RPC and Admin endpoints + +### Installation + +1. **Clone the repository** + ```bash + git clone + cd canopy-wallet + ``` + +2. **Install dependencies** + ```bash + npm install + # or + pnpm install + ``` + +3. **Configure your node connection** + + Edit `public/plugin/canopy/chain.json`: + ```json + { + "rpc": { + "base": "http://your-node-ip:50002", + "admin": "http://your-node-ip:50003" + } + } + ``` + + +4. Copy environment file: + ```bash + cp .env.example .env + ``` + + +5. **Start the development server** + ```bash + npm run dev + ``` + +6. **Open your browser** + ``` + http://localhost:5173 + ``` + +## 📁 Configuration Files + +### chain.json +Defines blockchain-specific configuration: + +- **RPC Endpoints**: Base and admin API URLs +- **Data Sources**: API endpoint definitions with caching strategies +- **Fee Configuration**: Transaction fee parameters and providers +- **Network Parameters**: Chain ID, denomination, explorer URLs +- **Session Settings**: Unlock timeouts and security preferences + +### manifest.json +Defines dynamic UI and transaction forms: + +- **Actions**: Transaction templates (send, stake, governance) +- **Form Fields**: Dynamic form generation with validation +- **UI Mapping**: Icons, labels, and transaction categorization +- **Payload Construction**: Data transformation for API calls + +## 🖥️ Main Features + +### Dashboard +- Account balance overview with 24h change tracking +- Recent transaction history with status indicators +- Quick action buttons for common operations +- Network status and validator information + +### Account Management +- Create new accounts with secure key generation +- Import existing accounts from private keys +- Export account information and QR codes +- Multi-account switching and management + +### Staking +- Comprehensive validator management +- Real-time staking statistics and rewards tracking +- Bulk operations for multiple validators +- Performance metrics and chain participation + +### Governance +- View active proposals with voting status +- Cast votes with detailed proposal information +- Create new governance proposals +- Track voting history and participation + +### Monitoring +- **Real-time Node Status**: Sync status, block height, consensus information +- **Network Peers**: Connected peers, network topology +- **System Resources**: CPU, memory, disk usage monitoring +- **Live Logs**: Real-time log streaming with export functionality +- **Performance Metrics**: Block production, network I/O, system health + +## 🔧 Development + +### Project Structure +- **React 18**: Modern React with hooks and concurrent features +- **TypeScript**: Full type safety throughout the application +- **Tailwind CSS**: Utility-first styling with custom design system +- **React Router**: Client-side routing with protected routes +- **React Query**: Server state management with caching +- **Framer Motion**: Smooth animations and transitions +- **Zustand**: Lightweight state management + +### Key Development Patterns + +#### Data Fetching +All data fetching uses the DS pattern through custom hooks: +```typescript +const dsFetch = useDSFetcher(); +const data = await dsFetch('admin.consensusInfo'); +``` + +#### Form Handling +Forms are generated dynamically from manifest configuration: +```typescript +const { openAction } = useActionModal(); +openAction('send'); // Opens send transaction form +``` + +#### Error Handling +Consistent error handling with user-friendly messages: +```typescript +const { data, error, isLoading } = useQuery({ + queryKey: ['nodeData'], + queryFn: () => dsFetch('admin.consensusInfo'), + retry: 2, + retryDelay: 1000, +}); +``` + +### Adding New Features + +1. **Define Data Sources**: Add new DS endpoints in `chain.json` +2. **Create Hooks**: Build custom hooks for data fetching +3. **Build Components**: Create UI components using design system +4. **Add Actions**: Define new transaction types in `manifest.json` + +### Environment Variables +```bash +VITE_DEFAULT_CHAIN=canopy +VITE_CONFIG_MODE=embedded +VITE_NODE_ENV=development +``` + +## 🛠️ Deployment + +### Production Build +```bash +npm run build +``` + +### Configuration for Production +1. Update `chain.json` with production RPC endpoints +2. Configure proper CORS settings on your node +3. Set appropriate session timeouts and security parameters +4. Ensure SSL/TLS is configured for secure connections + +### Docker Deployment +The wallet can be deployed alongside Canopy nodes: +```bash +# Build the application +npm run build + +# Serve with any static file server +npx serve -s dist -p 3000 +``` + +## 🔐 Security + +### Key Management +- Private keys are encrypted with user passwords +- Keys stored locally in browser secure storage +- Session-based key unlocking with configurable timeouts + +### Network Security +- All API calls over HTTPS in production +- CORS configuration required on node endpoints +- Session timeout and re-authentication for sensitive operations + +### Best Practices +- Regular password changes recommended +- Backup recovery phrases securely +- Use hardware wallets for large amounts +- Verify transaction details before signing + +## 📚 API Reference + +### Core Hooks +- `useAccountData()`: Account balances and information +- `useNodeData()`: Node status and monitoring data +- `useValidators()`: Validator information and staking data +- `useTransactions()`: Transaction history and status + +### DS Endpoints +All API endpoints are defined in `chain.json` under the `ds` section: +- **Query endpoints**: Height, accounts, validators, transactions +- **Admin endpoints**: Consensus info, peer info, logs, resources +- **Transaction endpoints**: Send, stake, governance operations + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes following the existing patterns +4. Add appropriate tests and documentation +5. Commit your changes (`git commit -m 'Add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +### Code Style +- Use TypeScript for all new code +- Follow existing naming conventions +- Add JSDoc comments for complex functions +- Use the established DS pattern for API calls +- Maintain responsive design principles + +## 📄 License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## 🆘 Support + +For support and questions: +- Check the documentation in `/docs` +- Review existing issues on GitHub +- Join our community discussions +- Contact the development team + +## 🗂️ Configuration Examples + +### Basic Node Configuration +```json +{ + "chainId": "1", + "displayName": "Canopy", + "rpc": { + "base": "http://localhost:50002", + "admin": "http://localhost:50003" + }, + "denom": { + "base": "ucnpy", + "symbol": "CNPY", + "decimals": 6 + } +} +``` + +### Simple Transaction Action +```json +{ + "id": "send", + "title": "Send", + "icon": "Send", + "form": { + "fields": [ + { + "id": "output", + "name": "output", + "type": "text", + "label": "Recipient Address", + "required": true + } + ] + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-send", + "method": "POST" + } +} +``` + + +## Building for Production + +### Environment Configuration + +The build process uses the `VITE_BASE_PATH` environment variable to configure the deployment path. + +**Default production path**: `/wallet/` + +To customize the base path, create a `.env` file: + +```bash +# For deployment at https://example.com/wallet/ +VITE_BASE_PATH=/wallet/ + +# For deployment at root domain https://wallet.example.com/ +VITE_BASE_PATH=/ + +# For custom subdirectory +VITE_BASE_PATH=/my-custom-path/ +``` + +### Build Commands + +```bash +# Production build (uses /wallet/ by default) +npm run build + +# Build with custom base path +VITE_BASE_PATH=/custom/ npm run build + +# Preview production build +npm run preview +``` + +The build output will be in the `out/` directory. + +## Deployment + +### Docker Build + +The wallet is automatically built during the Docker image build process via the Makefile: + +```bash +# From project root +make build/wallet +``` + +This is automatically called by the Dockerfile. + +### Manual Deployment + +1. Build the wallet: + ```bash + npm run build + ``` + +2. The compiled assets will be embedded in the Go binary during the build process via `//go:embed` directives. + +### Reverse Proxy Configuration + +When deploying behind a reverse proxy (like Traefik), ensure the proxy is configured to strip the path prefix: + +**Example Traefik Configuration:** + +```yaml +http: + middlewares: + strip-wallet-prefix: + stripPrefix: + prefixes: + - "/wallet" + forceSlash: false + + routers: + wallet: + rule: "Host(`example.com`) && PathPrefix(`/wallet`)" + service: wallet + middlewares: + - strip-wallet-prefix +``` + +This ensures that requests to `/wallet/assets/file.js` are forwarded to the Go server as `/assets/file.js`. + +## Troubleshooting + +### Assets not loading in production + +**Problem**: CSS and JS files return 404 or wrong MIME type. + +**Solution**: +1. Verify `VITE_BASE_PATH` matches your deployment path +2. Ensure reverse proxy is configured to strip the path prefix +3. Rebuild the Docker image after changing the base path + + +**Built with ❤️ for the Canopy ecosystem** + diff --git a/cmd/rpc/web/wallet-new/index.html b/cmd/rpc/web/wallet-new/index.html new file mode 100644 index 000000000..d35596e56 --- /dev/null +++ b/cmd/rpc/web/wallet-new/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + Wallet + + +
+ + + diff --git a/cmd/rpc/web/wallet-new/netifly.toml b/cmd/rpc/web/wallet-new/netifly.toml new file mode 100644 index 000000000..b35fcf8ca --- /dev/null +++ b/cmd/rpc/web/wallet-new/netifly.toml @@ -0,0 +1,36 @@ +[build] + base = "cmd/rpc/web/wallet-new" + publish = "dist" + command = "npm run build" + +[build.environment] + NODE_VERSION = "20" + NPM_FLAGS = "--legacy-peer-deps" + VITE_NODE_ENV = "production" + +# Redirects for SPA (Single Page Application) +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +# Headers for security and performance +[[headers]] + for = "/*" + [headers.values] + X-Frame-Options = "DENY" + X-XSS-Protection = "1; mode=block" + X-Content-Type-Options = "nosniff" + Referrer-Policy = "strict-origin-when-cross-origin" + +# Cache static assets +[[headers]] + for = "/assets/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +# Cache service worker +[[headers]] + for = "/sw.js" + [headers.values] + Cache-Control = "public, max-age=0, must-revalidate" \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/package-lock.json b/cmd/rpc/web/wallet-new/package-lock.json new file mode 100644 index 000000000..0cb5a5696 --- /dev/null +++ b/cmd/rpc/web/wallet-new/package-lock.json @@ -0,0 +1,5417 @@ +{ + "name": "canopy-wallet", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "canopy-wallet", + "version": "0.0.1", + "dependencies": { + "@number-flow/react": "^0.5.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/themes": "^3.2.1", + "@tanstack/react-query": "^5.52.1", + "chart.js": "^4.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.23.22", + "lucide-react": "^0.544.0", + "qrcode.react": "^4.2.0", + "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", + "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", + "react-router-dom": "^7.9.1", + "tailwind-merge": "^2.5.2", + "viem": "^2.17.0", + "zod": "^3.23.8", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "vite": "^5.4.8" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@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.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@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.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "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.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "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/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@number-flow/react": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@number-flow/react/-/react-0.5.10.tgz", + "integrity": "sha512-a8Wh5eNITn7Km4xbddAH7QH8eNmnduR6k34ER1hkHSGO4H2yU1DDnuAWLQM99vciGInFODemSc0tdxrXkJEpbA==", + "license": "MIT", + "dependencies": { + "esm-env": "^1.1.4", + "number-flow": "0.5.8" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "license": "MIT" + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@radix-ui/themes": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-3.2.1.tgz", + "integrity": "sha512-WJL2YKAGItkunwm3O4cLTFKCGJTfAfF6Hmq7f5bCo1ggqC9qJQ/wfg/25AAN72aoEM1yqXZQ+pslsw48AFR0Xg==", + "license": "MIT", + "dependencies": { + "@radix-ui/colors": "^3.0.0", + "classnames": "^2.3.2", + "radix-ui": "^1.1.3", + "react-remove-scroll-bar": "^2.3.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "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.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz", + "integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz", + "integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz", + "integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz", + "integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz", + "integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz", + "integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz", + "integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz", + "integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz", + "integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz", + "integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz", + "integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz", + "integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz", + "integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz", + "integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz", + "integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz", + "integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz", + "integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz", + "integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz", + "integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz", + "integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz", + "integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz", + "integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.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==", + "devOptional": 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/abitype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz", + "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", + "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "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.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "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/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.224", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", + "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "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/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.23.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", + "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.21", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "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/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/motion-dom": { + "version": "12.23.21", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", + "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/number-flow": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.5.8.tgz", + "integrity": "sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==", + "license": "MIT", + "dependencies": { + "esm-env": "^1.1.4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/ox": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz", + "integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "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-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "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/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.2.tgz", + "integrity": "sha512-i2TPp4dgaqrOqiRGLZmqh2WXmbdFknUyiCRmSKs0hf6fWXkTKg5h56b+9F22NbGRAMxjYfqQnpi63egzD2SuZA==", + "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.9.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.2.tgz", + "integrity": "sha512-pagqpVJnjZOfb+vIM23eTp7Sp/AAJjOgaowhP1f1TWOdk5/W8Uk8d/M/0wfleqx7SgjitjNPPsKeCZE1hTSp3w==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz", + "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", + "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.52.2", + "@rollup/rollup-android-arm64": "4.52.2", + "@rollup/rollup-darwin-arm64": "4.52.2", + "@rollup/rollup-darwin-x64": "4.52.2", + "@rollup/rollup-freebsd-arm64": "4.52.2", + "@rollup/rollup-freebsd-x64": "4.52.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.2", + "@rollup/rollup-linux-arm-musleabihf": "4.52.2", + "@rollup/rollup-linux-arm64-gnu": "4.52.2", + "@rollup/rollup-linux-arm64-musl": "4.52.2", + "@rollup/rollup-linux-loong64-gnu": "4.52.2", + "@rollup/rollup-linux-ppc64-gnu": "4.52.2", + "@rollup/rollup-linux-riscv64-gnu": "4.52.2", + "@rollup/rollup-linux-riscv64-musl": "4.52.2", + "@rollup/rollup-linux-s390x-gnu": "4.52.2", + "@rollup/rollup-linux-x64-gnu": "4.52.2", + "@rollup/rollup-linux-x64-musl": "4.52.2", + "@rollup/rollup-openharmony-arm64": "4.52.2", + "@rollup/rollup-win32-arm64-msvc": "4.52.2", + "@rollup/rollup-win32-ia32-msvc": "4.52.2", + "@rollup/rollup-win32-x64-gnu": "4.52.2", + "@rollup/rollup-win32-x64-msvc": "4.52.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.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/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.37.8", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.37.8.tgz", + "integrity": "sha512-mL+5yvCQbRIR6QvngDQMfEiZTfNWfd+/QL5yFaOoYbpH3b1Q2ddwF7YG2eI2AcYSh9LE1gtUkbzZLFUAVyj4oQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.1.0", + "isows": "1.0.7", + "ox": "0.9.6", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "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" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "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 + } + } + } + } +} diff --git a/cmd/rpc/web/wallet-new/package.json b/cmd/rpc/web/wallet-new/package.json new file mode 100644 index 000000000..5e8928b4b --- /dev/null +++ b/cmd/rpc/web/wallet-new/package.json @@ -0,0 +1,47 @@ +{ + "name": "canopy-wallet", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@number-flow/react": "^0.5.10", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/themes": "^3.2.1", + "@tanstack/react-query": "^5.52.1", + "chart.js": "^4.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.23.22", + "lucide-react": "^0.544.0", + "qrcode.react": "^4.2.0", + "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", + "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", + "react-router-dom": "^7.9.1", + "tailwind-merge": "^2.5.2", + "viem": "^2.17.0", + "zod": "^3.23.8", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "baseline-browser-mapping": "^2.9.11", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "vite": "^5.4.8" + } +} diff --git a/cmd/rpc/web/wallet-new/pnpm-lock.yaml b/cmd/rpc/web/wallet-new/pnpm-lock.yaml new file mode 100644 index 000000000..7f5ef6875 --- /dev/null +++ b/cmd/rpc/web/wallet-new/pnpm-lock.yaml @@ -0,0 +1,3955 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@number-flow/react': + specifier: ^0.5.10 + version: 0.5.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/themes': + specifier: ^3.2.1 + version: 3.2.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.52.1 + version: 5.87.4(react@18.3.1) + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + framer-motion: + specifier: ^12.23.22 + version: 12.23.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lucide-react: + specifier: ^0.544.0 + version: 0.544.0(react@18.3.1) + qrcode.react: + specifier: ^4.2.0 + version: 4.2.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-chartjs-2: + specifier: ^5.3.0 + version: 5.3.0(chart.js@4.5.1)(react@18.3.1) + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-hot-toast: + specifier: ^2.6.0 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^7.9.1 + version: 7.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.5.2 + version: 2.6.0 + viem: + specifier: ^2.17.0 + version: 2.37.6(typescript@5.9.2)(zod@3.25.76) + zod: + specifier: ^3.23.8 + version: 3.25.76 + zustand: + specifier: ^4.5.2 + version: 4.5.7(@types/react@18.3.24)(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.4 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.24) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.20) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.21(postcss@8.5.6) + baseline-browser-mapping: + specifier: ^2.9.11 + version: 2.9.11 + postcss: + specifier: ^8.4.47 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.10 + version: 3.4.17 + typescript: + specifier: ^5.5.4 + version: 5.9.2 + vite: + specifier: ^5.4.8 + version: 5.4.20 + +packages: + + '@adraffy/ens-normalize@1.11.0': + resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@number-flow/react@0.5.10': + resolution: {integrity: sha512-a8Wh5eNITn7Km4xbddAH7QH8eNmnduR6k34ER1hkHSGO4H2yU1DDnuAWLQM99vciGInFODemSc0tdxrXkJEpbA==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@radix-ui/colors@3.0.0': + resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@radix-ui/themes@3.2.1': + resolution: {integrity: sha512-WJL2YKAGItkunwm3O4cLTFKCGJTfAfF6Hmq7f5bCo1ggqC9qJQ/wfg/25AAN72aoEM1yqXZQ+pslsw48AFR0Xg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: 16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.50.2': + resolution: {integrity: sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.50.2': + resolution: {integrity: sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.50.2': + resolution: {integrity: sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.50.2': + resolution: {integrity: sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.50.2': + resolution: {integrity: sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.50.2': + resolution: {integrity: sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.50.2': + resolution: {integrity: sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.50.2': + resolution: {integrity: sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.50.2': + resolution: {integrity: sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.50.2': + resolution: {integrity: sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.50.2': + resolution: {integrity: sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.50.2': + resolution: {integrity: sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.50.2': + resolution: {integrity: sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.50.2': + resolution: {integrity: sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.50.2': + resolution: {integrity: sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.50.2': + resolution: {integrity: sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.50.2': + resolution: {integrity: sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.50.2': + resolution: {integrity: sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.2': + resolution: {integrity: sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.50.2': + resolution: {integrity: sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.50.2': + resolution: {integrity: sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==} + cpu: [x64] + os: [win32] + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@tanstack/query-core@5.87.4': + resolution: {integrity: sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw==} + + '@tanstack/react-query@5.87.4': + resolution: {integrity: sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA==} + peerDependencies: + react: ^18 || ^19 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.24': + resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + abitype@1.1.0: + resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.0: + resolution: {integrity: sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001741: + resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.218: + resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + framer-motion@12.23.22: + resolution: {integrity: sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.544.0: + resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + motion-dom@12.23.21: + resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + number-flow@0.5.8: + resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + ox@0.9.3: + resolution: {integrity: sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-chartjs-2@5.3.0: + resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.9.1: + resolution: {integrity: sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.9.1: + resolution: {integrity: sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.50.2: + resolution: {integrity: sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + viem@2.37.6: + resolution: {integrity: sha512-b+1IozQ8TciVQNdQUkOH5xtFR0z7ZxR8pyloENi/a+RA408lv4LoX12ofwoiT3ip0VRhO5ni1em//X0jn/eW0g==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + 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 + +snapshots: + + '@adraffy/ens-normalize@1.11.0': {} + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@number-flow/react@0.5.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + esm-env: 1.2.2 + number-flow: 0.5.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@radix-ui/colors@3.0.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-context@1.1.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-form@0.1.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-label': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/rect@1.1.1': {} + + '@radix-ui/themes@3.2.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/colors': 3.0.0 + classnames: 2.5.1 + radix-ui: 1.4.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.50.2': + optional: true + + '@rollup/rollup-android-arm64@4.50.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.50.2': + optional: true + + '@rollup/rollup-darwin-x64@4.50.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.50.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.50.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.50.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.50.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.50.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.50.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.50.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.50.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.50.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.50.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.50.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.50.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.50.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.50.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.50.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.50.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.2': + optional: true + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@tanstack/query-core@5.87.4': {} + + '@tanstack/react-query@5.87.4(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.87.4 + react: 18.3.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/estree@1.0.8': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.24)': + dependencies: + '@types/react': 18.3.24 + + '@types/react@18.3.24': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.20)': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.20 + transitivePeerDependencies: + - supports-color + + abitype@1.1.0(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.26.0 + caniuse-lite: 1.0.30001741 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.11: {} + + binary-extensions@2.3.0: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.0: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001741 + electron-to-chromium: 1.5.218 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.0) + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001741: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + classnames@2.5.1: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.0.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.218: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + esbuild@0.21.5: + 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 + + escalade@3.2.0: {} + + esm-env@1.2.2: {} + + eventemitter3@5.0.1: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fraction.js@4.3.7: {} + + framer-motion@12.23.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.23.21 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + goober@2.1.18(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + isows@1.0.7(ws@8.18.3): + dependencies: + ws: 8.18.3 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.544.0(react@18.3.1): + dependencies: + react: 18.3.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + motion-dom@12.23.21: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-releases@2.0.21: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + number-flow@0.5.8: + dependencies: + esm-env: 1.2.2 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + ox@0.9.3(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-js@4.0.1(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@4.0.2(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.1 + optionalDependencies: + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + qrcode.react@4.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + + queue-microtask@1.2.3: {} + + radix-ui@1.4.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-form': 0.1.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + react-chartjs-2@5.3.0(chart.js@4.5.1)(react@18.3.1): + dependencies: + chart.js: 4.5.1 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + csstype: 3.1.3 + goober: 2.1.18(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.24)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + react-remove-scroll@2.7.1(@types/react@18.3.24)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.24)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.24)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + + react-router-dom@7.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-style-singleton@2.2.3(@types/react@18.3.24)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.50.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.50.2 + '@rollup/rollup-android-arm64': 4.50.2 + '@rollup/rollup-darwin-arm64': 4.50.2 + '@rollup/rollup-darwin-x64': 4.50.2 + '@rollup/rollup-freebsd-arm64': 4.50.2 + '@rollup/rollup-freebsd-x64': 4.50.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.2 + '@rollup/rollup-linux-arm-musleabihf': 4.50.2 + '@rollup/rollup-linux-arm64-gnu': 4.50.2 + '@rollup/rollup-linux-arm64-musl': 4.50.2 + '@rollup/rollup-linux-loong64-gnu': 4.50.2 + '@rollup/rollup-linux-ppc64-gnu': 4.50.2 + '@rollup/rollup-linux-riscv64-gnu': 4.50.2 + '@rollup/rollup-linux-riscv64-musl': 4.50.2 + '@rollup/rollup-linux-s390x-gnu': 4.50.2 + '@rollup/rollup-linux-x64-gnu': 4.50.2 + '@rollup/rollup-linux-x64-musl': 4.50.2 + '@rollup/rollup-openharmony-arm64': 4.50.2 + '@rollup/rollup-win32-arm64-msvc': 4.50.2 + '@rollup/rollup-win32-ia32-msvc': 4.50.2 + '@rollup/rollup-win32-x64-msvc': 4.50.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + set-cookie-parser@2.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.6.0: {} + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + typescript@5.9.2: {} + + update-browserslist-db@1.1.3(browserslist@4.26.0): + dependencies: + browserslist: 4.26.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@18.3.24)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + use-sidecar@1.1.3(@types/react@18.3.24)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + viem@2.37.6(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3) + ox: 0.9.3(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + vite@5.4.20: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.50.2 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + ws@8.18.3: {} + + yallist@3.1.1: {} + + yaml@2.8.1: {} + + zod@3.25.76: {} + + zustand@4.5.7(@types/react@18.3.24)(react@18.3.1): + dependencies: + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + react: 18.3.1 diff --git a/cmd/rpc/web/wallet-new/postcss.config.js b/cmd/rpc/web/wallet-new/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/cmd/rpc/web/wallet-new/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/cmd/rpc/web/wallet-new/public/logo.svg b/cmd/rpc/web/wallet-new/public/logo.svg new file mode 100644 index 000000000..476cefae0 --- /dev/null +++ b/cmd/rpc/web/wallet-new/public/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json b/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json new file mode 100644 index 000000000..d9eca04cd --- /dev/null +++ b/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json @@ -0,0 +1,246 @@ +{ + "version": "1", + "chainId": "1", + "displayName": "Canopy", + "denom": { + "base": "ucnpy", + "symbol": "CNPY", + "decimals": 6 + }, + "rpc": { + "base": "/rpc", + "admin": "/adminrpc" + }, + "explorer": "/explorer", + "address": { + "format": "evm" + }, + "ds": { + "height": { + "source": { "base": "rpc", "path": "/v1/query/height", "method": "POST" }, + "selector": "", + "coerce": { "response": { "": "int" } } + + }, + "account": { + "source": { "base": "rpc", "path": "/v1/query/account", "method": "POST" }, + "body": { "height": 0, "address": "{{account.address}}" }, + "coerce": { "body": { "height": "number" }, "response": { "amount": "number" } }, + "selector": "" + }, + "accountByHeight": { + "source": { "base": "rpc", "path": "/v1/query/account", "method": "POST", + "encoding": "text" + }, + "body": { "height": "{{height}}", "address": "{{address}}" }, + "coerce": { "ctx": { "height": "number" }, "body": { "height": "number" }, "response": { "amount": "number" }}, + "selector": "amount" + }, + "keystore": { + "source": { "base": "admin", "path": "/v1/admin/keystore", "method": "GET" }, + "selector": "" + }, + "keystoreNewKey": { + "source": { "base": "admin", "path": "/v1/admin/keystore-new-key", "method": "POST" }, + "body": { "nickname": "{{nickname}}", "password": "{{password}}" } + }, + "keystoreGet": { + "source": { "base": "admin", "path": "/v1/admin/keystore-get", "method": "POST" }, + "body": { "address": "{{address}}", "password": "{{password}}", "nickname": "{{nickname}}", "submit": true } + }, + "keystoreDelete": { + "source": { "base": "admin", "path": "/v1/admin/keystore-delete", "method": "POST" }, + "body": { "nickname": "{{nickname}}" } + }, + "validator": { + "source": { "base": "rpc", "path": "/v1/query/validator", "method": "POST" }, + "body": { "height": "0", "address": "{{account.address}}" }, + "coerce": { "body": { "height": "int" } }, + "selector": "" + }, + "validatorByHeight": { + "source": { "base": "rpc", "path": "/v1/query/validator", "method": "POST" }, + "body": { "height": "{{height}}", "address": "{{address}}" }, + "coerce": { "ctx": { "height": "number" }, "body": { "height": "int" } }, + "selector": "" + }, + "validators": { + "source": { "base": "rpc", "path": "/v1/query/validators", "method": "POST" }, + "body": { "height": 0, "pageNumber": 1, "perPage": 1000 }, + "coerce": { "body": { "height": "int" } }, + "selector": "results" + }, + "validatorSet": { + "source": { "base": "rpc", "path": "/v1/query/validator-set", "method": "POST" }, + "body": { "height": "{{height}}", "id": "{{committeeId}}" }, + "coerce": { "body": { "height": "int", "id": "int" } }, + "selector": "" + }, + "txs": { + "sent": { + "source": { "base": "rpc", "path": "/v1/query/txs-by-sender", "method": "POST" }, + "body": { "pageNumber": "{{page}}", "perPage": "{{perPage}}", "address": "{{account.address}}" }, + "selector": "", + + "page": { + "strategy": "page", + "param": { "page": "pageNumber", "perPage": "perPage" }, + "response": { + "items": "results", + "totalPages": "paging.totalPages" + }, + "defaults": { "perPage": 20, "startPage": 1 } + } + }, + + "received": { + "source": { "base": "rpc", "path": "/v1/query/txs-by-rec", "method": "POST" }, + "body": { "pageNumber": "{{page}}", "perPage": "{{perPage}}", "address": "{{account.address}}" }, + "selector": "", + "page": { + "strategy": "page", + "param": { "page": "pageNumber", "perPage": "perPage" }, + "response": { "items": "results" }, + "defaults": { "perPage": 20, "startPage": 1 } + } + }, + "failed": { + "source": { "base": "rpc", "path": "/v1/query/failed-txs", "method": "POST" }, + "body": { "pageNumber": "{{page}}", "perPage": "{{perPage}}", "address": "{{account.address}}" }, + "selector": "", + "page": { + "strategy": "page", + "param": { "page": "pageNumber", "perPage": "perPage" }, + "response": { "items": "results" }, + "defaults": { "perPage": 20, "startPage": 1 } + } + } + }, + + "activity": { + "all": { + "source": { "base": "rpc", "path": "/v1/query/activity", "method": "POST" }, + "body": { "cursor": "{{cursor}}", "limit": "{{limit}}", "address": "{{account.address}}" }, + "selector": "", + "page": { + "strategy": "cursor", + "param": { "cursor": "cursor", "limit": "limit" }, + "response": { "items": "items", "nextCursor": "next" }, + "defaults": { "limit": 50 } + } + } + }, + "params": { + "source": { "base": "rpc", "path": "/v1/query/params", "method": "POST", "encoding": "text" }, + "body": "{\"height\":0,\"address\":\"\"}" + }, + "gov": { + "proposals": { + "source": { "base": "rpc", "path": "/v1/gov/proposals", "method": "GET" }, + "selector": "" + } + }, + "events": { + "byAddress": { + "source": { "base": "rpc", "path": "/v1/query/events-by-address", "method": "POST" }, + "body": { "height": "{{height}}", "address": "{{address}}", "pageNumber": "{{page}}", "perPage": "{{perPage}}" }, + "coerce": { "ctx": { "height": "number" }, "body": { "height": "int", "pageNumber": "int", "perPage": "int" } }, + "selector": "results", + "page": { + "strategy": "page", + "param": { "page": "pageNumber", "perPage": "perPage" }, + "response": { "items": "results", "totalPages": "paging.totalPages" }, + "defaults": { "perPage": 100, "startPage": 1, "height": 0 } + } + }, + "byHeight": { + "source": { "base": "rpc", "path": "/v1/query/events-by-height", "method": "POST" }, + "body": { "height": "{{height}}", "pageNumber": "{{page}}", "perPage": "{{perPage}}" }, + "coerce": { "ctx": { "height": "int" }, "body": { "height": "int", "pageNumber": "int", "perPage": "int" } }, + "selector": "results", + "page": { + "strategy": "page", + "param": { "page": "pageNumber", "perPage": "perPage" }, + "response": { "items": "results" }, + "defaults": { "perPage": 100, "startPage": 1 } + } + } + }, + "lastProposers": { + "source": { "base": "rpc", "path": "/v1/query/last-proposers", "method": "POST" }, + "body": { "height": 0, "count": "{{count}}" }, + "coerce": { "body": { "height": "int", "count": "int" } }, + "selector": "" + }, + "admin": { + "consensusInfo": { + "source": { "base": "admin", "path": "/v1/admin/consensus-info", "method": "GET" }, + "selector": "" + }, + "peerInfo": { + "source": { "base": "admin", "path": "/v1/admin/peer-info", "method": "GET" }, + "selector": "" + }, + "resourceUsage": { + "source": { "base": "admin", "path": "/v1/admin/resource-usage", "method": "GET" }, + "selector": "" + }, + "log": { + "source": { "base": "admin", "path": "/v1/admin/log", "method": "GET", "encoding": "text" }, + "selector": "" + }, + "config": { + "source": { "base": "admin", "path": "/v1/admin/config", "method": "GET" }, + "selector": "" + }, + "peerBook": { + "source": { "base": "admin", "path": "/v1/admin/peer-book", "method": "GET" }, + "selector": "" + } + } + }, + "params": { + "sources": [ + { + "id": "networkParams", + "base": "rpc", + "path": "/v1/query/params", + "method": "POST", + "encoding": "text", + "body": "{\"height\":0,\"address\":\"\"}" + } + ], + "avgBlockTimeSec": 50, + "refresh": { + "staleTimeMs": 3000, + "refetchIntervalMs": 3000 + } + }, + "fees": { + "denom": "{{chain.denom.base}}", + "refreshMs": 500000, + "providers": [ + { + "type": "query", + "base": "rpc", + "path": "/v1/query/params", + "method": "POST", + "encoding": "text", + "body": "{\"height\":0,\"address\":\"\"}", + "selector": "fee" + } + ], + "buckets": { + "avg": { + "multiplier": 1.0, + "default": true + } + } + }, + "features": ["staking", "gov"], + "session": { + "unlockTimeoutSec": 900, + "rePromptSensitive": false, + "persistAcrossTabs": false + } +} diff --git a/cmd/rpc/web/wallet-new/public/plugin/canopy/manifest.json b/cmd/rpc/web/wallet-new/public/plugin/canopy/manifest.json new file mode 100644 index 000000000..3e873beff --- /dev/null +++ b/cmd/rpc/web/wallet-new/public/plugin/canopy/manifest.json @@ -0,0 +1,1575 @@ +{ + "ui": { + "tx": { + "typeMap": { + "send": "Send", + "editStake": "Edit Stake", + "stake": "Stake", + "unstake": "Unstake", + "receive": "Receive", + "vote": "Vote", + "certificateResults": "Certificate Results", + "unpause": "Unpause", + "pause": "Pause", + "createProposal": "Create Proposal" + }, + "typeIconMap": { + "editStake": "Lock", + "send": "Send", + "stake": "Lock", + "certificateResults": "ShieldCheck", + "unstake": "Unlock", + "unpause": "Play", + "pause": "Pause", + "receive": "Download", + "vote": "Vote", + "createProposal": "FileText" + }, + "fundsWay": { + "editStake": "out", + "send": "out", + "stake": "out", + "unstake": "in", + "receive": "in", + "vote": "neutral", + "createProposal": "out" + } + } + }, + "actions": [ + { + "id": "send", + "title": "Send", + "icon": "Send", + "relatedActions": [ + "receive" + ], + "ds": { + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 0, + "refetchOnMount": "always", + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[40rem]" + } + } + }, + "tags": [ + "quick" + ], + "form": { + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "From Address", + "value": "{{account.address}}", + "readOnly": true + }, + { + "id": "output", + "name": "output", + "type": "text", + "label": "To Address", + "required": true, + "length.min": 1, + "validation": { + "messages": { + "required": "Destination address is required", + "length.min": "Invalid destination address" + } + }, + "features": [ + { + "id": "pasteBtn", + "op": "paste" + } + ] + }, + { + "id": "asset", + "name": "asset", + "type": "text", + "label": "Asset", + "value": "{{chain.displayName}} (Balance: {{formatToCoin<{{ds.account.amount}}>}})", + "autoPopulate": "always", + "readOnly": true + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Amount", + "required": true, + "min": 0.000001, + "max": "{{ fromMicroDenom<{{ds.account?.amount ?? 0}}> }}", + "validation": { + "messages": { + "required": "Amount is required", + "min": "Amount must be greater than 0", + "max": "Insufficient balance. Maximum available: {{max}} {{chain.denom.symbol}}" + } + }, + "help": "Available balance: {{formatToCoin<{{ds.account?.amount ?? 0}}>}} {{chain.denom.symbol}}", + "features": [ + { + "id": "maxBtn", + "op": "set", + "field": "amount", + "label": "Max", + "value": "{{ fromMicroDenom<{{(ds.account?.amount ?? 0) - fees.raw.sendFee}}> }}" + } + ] + } + ], + "info": { + "items": [ + { + "label": "Network Fee", + "value": "{{formatToCoin<{{fees.raw.sendFee}}>}} {{chain.denom.symbol}}", + "icon": "Coins" + }, + { + "label": "Estimation time", + "value": "≈ 20", + "icon": "Timer" + } + ] + }, + "summary": { + "title": "Summary", + "items": [ + { + "label": "Receiving Address", + "value": "{{form.address}}" + }, + { + "label": "Asset", + "value": "{{chain.denom.symbol}} (Balance: {{formatToCoin<{{ds.account.amount}}>}})" + } + ] + }, + "confirmation": { + "title": "Confirmations", + "summary": [ + { + "label": "From", + "value": "{{shortAddress<{{account.address}}>}}" + }, + { + "label": "To", + "value": "{{shortAddress<{{form.output}}>}}" + }, + { + "label": "Amount", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Fee", + "value": "{{formatToCoin<{{fees.amount}}>}} {{chain.denom.symbol}}" + } + ], + "btn": { + "icon": "Send", + "label": "Send" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "output": { + "value": "{{form.output}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "delegate": { + "value": false, + "coerce": "boolean" + }, + "earlyWithdrawal": { + "value": false, + "coerce": "boolean" + }, + "pubKey": { + "value": "", + "coerce": "string" + }, + "committees": { + "value": "", + "coerce": "string" + }, + "signer": { + "value": "", + "coerce": "string" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{fees.raw.sendFee}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-send", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Send!", + "description": "{{result}}", + "actions": [ + { + "type": "link", + "label": "Explorer", + "href": "{{chain.explorer.tx}}/{{result}}", + "newTab": true + } + ] + } + } + }, + { + "id": "receive", + "title": "Receive", + "icon": "Scan", + "relatedActions": [ + "send" + ], + "ds": { + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 30000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "variant": "modal", + "icon": "Send", + "hideSubmit": true, + "slots": { + "modal": { + "className": "sm:max-w-[20rem] md:max-w-[24rem]" + } + } + }, + "tags": [ + "quick" + ], + "form": { + "layout": { + "aside": { + "show": true, + "width": "w-[10rem]" + } + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Receiving Address", + "value": "{{account.address}}", + "readOnly": true, + "features": [ + { + "id": "copyBtn", + "op": "copy", + "from": "{{account.address}}" + } + ] + }, + { + "id": "asset", + "name": "asset", + "type": "text", + "label": "Asset", + "autoPopulate": "always", + "value": "{{chain.denom.symbol}} (Balance: {{formatToCoin<{{ds.account.amount}}>}})", + "readOnly": true + } + ], + "info": { + "title": "Details", + "items": [ + { + "label": "Only send {{chain.denom.symbol}} to this address.", + "icon": "Coins" + }, + { + "label": "Allow 20 seconds for confirmation.", + "icon": "Timer" + } + ] + } + } + }, + { + "id": "stake", + "title": "Stake", + "icon": "Lock", + "ds": { + "account": { + "account": { + "address": "{{form.signerResponsible === 'operator' ? form.operator : form.output}}" + }, + "__options": { + "enabled": "{{ (form.signerResponsible === 'operator' && form.operator) || (form.signerResponsible === 'reward' && form.output) }}" + } + }, + "validator": { + "account": { + "address": "{{form.operator}}" + }, + "__options": { + "enabled": "{{ form.operator }}" + } + }, + "keystore": {}, + "__options": { + "staleTimeMs": 30000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "watch": [ + "form.operator", + "form.output", + "form.signerResponsible" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "" + } + } + }, + "tags": [ + "quick" + ], + "form": { + "wizard": { + "steps": [ + { + "id": "setup", + "title": "Setup" + }, + { + "id": "committees", + "title": "Committees" + } + ] + }, + "fields": [ + { + "id": "operator", + "span": { + "base": 12 + }, + "name": "operator", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Staking (Operator) Address", + "placeholder": "Select Staking Address", + "map": "{{ Object.keys(ds.keystore?.addressMap || {})?.map(k => ({ label: k.slice(0, 6) + \"...\" + String(k ?? \"\")?.slice(-6) + ' (' + ds.keystore.addressMap[k].keyNickname +') ' , value: k }))}}", + "step": "setup" + }, + { + "id": "validatorInfo", + "name": "validatorInfo", + "type": "dynamicHtml", + "html": "

Validator Information

Staked Amount:

{{formatToCoin<{{ds.validator.stakedAmount ?? 0}}>}} {{chain.denom.symbol}}

Committees:

{{ds.validator.committees ?? 'N/A'}}

Type:

{{ ds.validator.delegate ? 'Delegation' : 'Validation' }}

", + "showIf": "{{ form.operator && ds.validator }}", + "step": "setup", + "span": { + "base": 12 + } + }, + { + "id": "output", + "span": { + "base": 12 + }, + "name": "output", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Rewards Address", + "placeholder": "Select Rewards Address", + "value": "{{ ds.validator ? ds.validator.output : '' }}", + "autoPopulate": "once", + "map": "{{ Object.keys(ds.keystore?.addressMap || {})?.map(k => ({ label: k.slice(0, 6) + \"...\" + String(k ?? \"\")?.slice(-6) + ' (' + ds.keystore.addressMap[k].keyNickname +') ' , value: k }))}}", + "step": "setup" + }, + { + "id": "signerResponsible", + "name": "signerResponsible", + "type": "option", + "label": "Signer Address", + "required": true, + "value": "operator", + "inLine": true, + "borders": false, + "options": [ + { + "label": "Operator", + "value": "operator", + "help": "Staked Address" + }, + { + "label": "Reward", + "value": "reward", + "help": "Output Address" + } + ], + "step": "setup" + }, + { + "id": "signerBalance", + "name": "signerBalance", + "type": "dynamicHtml", + "html": "

Signer Account Balance

{{formatToCoin<{{ds.account.amount ?? 0}}>}} {{chain.denom.symbol}}

Available balance for transaction fees and additional stake

", + "showIf": "{{ form.signerResponsible && form.operator && form.output }}", + "step": "setup", + "span": { + "base": 12 + } + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "placeholder": "0.00", + "label": "{{ ds.validator ? 'New Stake Amount' : 'Amount' }}", + "value": "{{ ds.validator ? fromMicroDenom<{{ds.validator.stakedAmount}}> : 0 }}", + "autoPopulate": "once", + "required": true, + "min": "{{ fromMicroDenom<{{ds.validator?.stakedAmount ?? 0}}> }}", + "max": "{{ fromMicroDenom<{{(ds.account?.amount ?? 0) + (ds.validator?.stakedAmount ?? 0)}}> }}", + "help": "{{ ds.validator ? '' : 'Minimum stake amount applies' }}", + "validation": { + "min": "{{ fromMicroDenom<{{ds.validator?.stakedAmount ?? 0}}> }}", + "messages": { + "min": "Stakes can only increase. Current stake: {{min}} {{chain.denom.symbol}}", + "max": "You cannot send more than your balance {{max}} {{chain.denom.symbol}}" + } + }, + "features": [ + { + "id": "max", + "op": "set", + "field": "amount", + "value": "{{ fromMicroDenom<{{(ds.account?.amount ?? 0) + (ds.validator?.stakedAmount ?? 0) - fees.raw.stakeFee}}> }}" + } + ], + "span": { + "base": 12 + }, + "step": "setup" + }, + { + "id": "currentStakeInfo", + "name": "currentStakeInfo", + "type": "dynamicHtml", + "html": "
Current:{{numberToLocaleString<{{fromMicroDenom<{{ds.validator.stakedAmount}}>}}>}} CNPY
↑ Increase only
", + "showIf": "{{ ds.validator }}", + "step": "setup", + "span": { + "base": 12 + } + }, + { + "id": "isDelegate", + "name": "isDelegate", + "type": "optionCard", + "label": "Stake Type", + "required": true, + "value": "{{ ds.validator ? ds.validator.delegate : false }}", + "autoPopulate": "once", + "options": [ + { + "label": "Validation", + "value": false, + "help": "Run your own validator", + "toolTip": "This will run your own validator on the network." + }, + { + "label": "Delegation", + "value": true, + "help": "Delegate to committee", + "toolTip": "This will delegate your tokens to a committee." + } + ], + "step": "setup" + }, + { + "id": "isAutocompound", + "name": "isAutocompound", + "type": "switch", + "help": "Automatically restake rewards", + "label": "Autocompound", + "value": "{{ ds.validator ? ds.validator.compound === true : false }}", + "autoPopulate": "once", + "step": "setup" + }, + { + "id": "selectCommittees", + "name": "selectCommittees", + "type": "tableSelect", + "label": "Select Committees", + "required": true, + "help": "Select committees you want to delegate to. Maximum 15 committees per validator.", + "validation": { + "max": 15, + "messages": { + "required": "Please select at least one committee", + "max": "Maximum 15 committees allowed per validator" + } + }, + "multiple": true, + "rowKey": "id", + "selectMode": "action", + "value": "{{ ds.validator?.committees ?? [] }}", + "autoPopulate": "once", + "rows": [ + { + "id": 1, + "name": "Canopy", + "minStake": "1 CNPY" + }, + { + "id": 2, + "name": "Canary", + "minStake": "1 CNPY" + } + ], + "columns": [ + { + "title": "Committee", + "type": "committee", + "align": "left" + }, + { + "title": "Staked Amount", + "type": "html", + "html": "{{ (() => { const isStaked = ds.validator?.committees?.includes(row.id); const isSelected = Array.isArray(form.selectCommittees) && (form.selectCommittees.includes(row.id) || form.selectCommittees.includes(String(row.id))); const amt = Number(form.amount) || 0; if (isStaked) { const current = ds.validator.stakedAmount / 1000000; const diff = amt - current; return '' + current.toLocaleString('en-US', {maximumFractionDigits: 3}) + ' CNPY + ' + (diff > 0 ? diff : 0).toLocaleString('en-US', {maximumFractionDigits: 3}) + ' CNPY'; } else if (isSelected) { return '' + amt.toLocaleString('en-US', {maximumFractionDigits: 3}) + ' CNPY'; } else { return '0 CNPY'; } })() }}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ ds.validator?.committees?.includes(row.id) ? 'Staked' : 'Stake' }}", + "disabledIf": "{{ ds.validator?.committees?.includes(row.id) }}", + "emit": { + "op": "select" + } + }, + "step": "committees" + }, + { + "id": "manualCommittees", + "name": "manualCommittees", + "type": "text", + "label": "Committees Summary", + "placeholder": "1,2,3", + "help": "{{ ds.validator ? 'Current committees (from validator)' : 'Enter comma separated committee ids' }}", + "value": "{{ Array.isArray(form.selectCommittees) ? form.selectCommittees.join(',') : (ds.validator?.committees ? (Array.isArray(ds.validator.committees) ? ds.validator.committees.join(',') : ds.validator.committees) : '') }}", + "readOnly": true, + "step": "committees" + }, + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "showIf": "{{ form.isDelegate && form.isDelegate !== true }}", + "value": "{{ ds.validator?.netAddress ?? '' }}", + "autoPopulate": "once", + "placeholder": "tcp://127.0.0.1:xxxx", + "help": "Put the url of the validator you want to delegate to.", + "step": "committees" + }, + { + "id": "txFee", + "name": "txFee", + "type": "amount", + "label": "Transaction Fee", + "value": "{{ fromMicroDenom<{{fees.raw.stakeFee}}> }}", + "autoPopulate": "always", + "required": true, + "min": "{{ fromMicroDenom<{{fees.raw.stakeFee}}> }}", + "validation": { + "messages": { + "required": "Transaction fee is required", + "min": "Transaction fee cannot be less than the network minimum: {{min}} {{chain.denom.symbol}}" + } + }, + "help": "Network minimum fee: {{ fromMicroDenom<{{fees.raw.stakeFee}}> }} {{chain.denom.symbol}}", + "step": "committees" + } + ], + "confirmation": { + "title": "Confirmations", + "summary": [ + { + "label": "Staking (Operator) Address", + "value": "{{shortAddress<{{form.operator}}>}}" + }, + { + "label": "Rewards Address", + "value": "{{shortAddress<{{form.output}}>}}" + }, + { + "label": "Signer Address", + "value": "{{form.signerResponsible == 'operator' ? 'Operator Address' : 'Rewards Address'}} | {{shortAddress<{{form.signerResponsible == 'operator' ? form.operator : form.output}}>}}" + }, + { + "label": "{{ ds.validator ? 'Edit Stake' : 'New Stake' }}", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Transaction Type", + "value": "{{ds.validator ? 'Edit Stake' : 'New Stake'}}" + }, + { + "label": "Committees IDs", + "value": "{{form?.selectCommittees?.filter(c => c !== '').map(c => c).join(',')}}" + }, + { + "label": "Net Address", + "value": "{{form?.isDelegate && form?.isDelegate != 'true' ? form.validatorAddress : ''}}" + }, + { + "label": "Transaction Fee", + "value": "{{numberToLocaleString<{{form.txFee}}>}} {{chain.denom.symbol}}" + } + ], + "btns": { + "submit": { + "icon": "Lock", + "label": "Stake" + } + } + } + }, + "payload": { + "address": { + "value": "{{form.operator}}", + "coerce": "string" + }, + "pubKey": { + "value": "{{ ds.validator ? '' : account.pubKey }}", + "coerce": "string" + }, + "committees": { + "value": "{{form?.selectCommittees?.filter(c => c !== '').map(c => c).join(',')}}", + "coerce": "string" + }, + "netAddress": { + "value": "{{form?.isDelegate && form?.isDelegate != 'true' ? form.validatorAddress : ''}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "delegate": { + "value": "{{ ds.validator ? false : form.isDelegate }}", + "coerce": "boolean" + }, + "earlyWithdrawal": { + "value": "{{!form.isAutocompound}}", + "coerce": "boolean" + }, + "output": { + "value": "{{form.output}}", + "coerce": "string" + }, + "signer": { + "value": "{{form.signerResponsible === 'operator' ? form.operator : form.output}}", + "coerce": "string" + }, + "memo": { + "value": "", + "coerce": "string" + }, + "fee": { + "value": "{{ toMicroDenom<{{form.txFee}}> }}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "{{ ds.validator ? '/v1/admin/tx-edit-stake' : '/v1/admin/tx-stake' }}", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Send!", + "description": "{{result}}", + "actions": [ + { + "type": "link", + "label": "Explorer", + "href": "{{chain.explorer.tx}}/{{result}}", + "newTab": true + } + ] + } + } + }, + { + "id": "vote", + "title": "Vote on Proposal", + "icon": "Vote", + "ds": { + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 30000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[40rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "proposalId", + "name": "proposalId", + "type": "text", + "label": "Proposal ID", + "required": true, + "validation": { + "messages": { + "required": "Proposal ID is required" + } + } + }, + { + "id": "vote", + "name": "vote", + "type": "option", + "label": "Your Vote", + "required": true, + "options": [ + { + "label": "Yes", + "value": "yes", + "help": "Vote in favor of the proposal" + }, + { + "label": "No", + "value": "no", + "help": "Vote against the proposal" + }, + { + "label": "Abstain", + "value": "abstain", + "help": "Abstain from voting" + } + ] + }, + { + "id": "voterAddress", + "name": "voterAddress", + "type": "text", + "label": "Voter Address", + "value": "{{account.address}}", + "readOnly": true + } + ], + "confirmation": { + "title": "Confirm Vote", + "summary": [ + { + "label": "Proposal ID", + "value": "{{form.proposalId}}" + }, + { + "label": "Your Vote", + "value": "{{form.vote}}" + }, + { + "label": "Voter Address", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "CheckCircle", + "label": "Submit Vote" + } + } + }, + "payload": { + "proposalId": { + "value": "{{form.proposalId}}", + "coerce": "number" + }, + "vote": { + "value": "{{form.vote}}", + "coerce": "string" + }, + "voterAddress": { + "value": "{{account.address}}", + "coerce": "string" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/gov-vote", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Vote Submitted!", + "description": "Your vote has been recorded", + "actions": [] + } + } + }, + { + "id": "createProposal", + "title": "Create Proposal", + "icon": "FileText", + "ds": { + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 30000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[45rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "title", + "name": "title", + "type": "text", + "label": "Proposal Title", + "required": true, + "validation": { + "messages": { + "required": "Title is required" + } + } + }, + { + "id": "description", + "name": "description", + "type": "textarea", + "label": "Description", + "required": true, + "rows": 5, + "validation": { + "messages": { + "required": "Description is required" + } + } + }, + { + "id": "proposerAddress", + "name": "proposerAddress", + "type": "text", + "label": "Proposer Address", + "value": "{{account.address}}", + "readOnly": true + }, + { + "id": "deposit", + "name": "deposit", + "type": "amount", + "label": "Deposit Amount", + "required": true, + "min": 0, + "validation": { + "messages": { + "required": "Deposit is required", + "min": "Deposit must be greater than 0" + } + }, + "help": "Minimum deposit required to submit proposal" + } + ], + "confirmation": { + "title": "Confirm Proposal", + "summary": [ + { + "label": "Title", + "value": "{{form.title}}" + }, + { + "label": "Description", + "value": "{{form.description}}" + }, + { + "label": "Deposit", + "value": "{{numberToLocaleString<{{form.deposit}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Proposer", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "Send", + "label": "Submit Proposal" + } + } + }, + "payload": { + "title": { + "value": "{{form.title}}", + "coerce": "string" + }, + "description": { + "value": "{{form.description}}", + "coerce": "string" + }, + "proposerAddress": { + "value": "{{account.address}}", + "coerce": "string" + }, + "deposit": { + "value": "{{toMicroDenom<{{form.deposit}}>}}", + "coerce": "number" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/gov-create-proposal", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Proposal Created!", + "description": "Your proposal has been submitted", + "actions": [] + } + } + }, + { + "id": "pauseValidator", + "title": "Pause Validator", + "icon": "Pause", + "ds": { + "validator": { + "account": { + "address": "{{account.address}}" + } + }, + "fees": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 0, + "refetchOnMount": true + } + }, + "form": { + "fields": [ + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "readOnly": true, + "help": "The address of the validator to pause" + }, + { + "id": "signerAddress", + "name": "signerAddress", + "type": "text", + "label": "Signer Address", + "value": "{{account.address}}", + "required": true, + "readOnly": true, + "help": "The address that will sign this transaction" + }, + { + "id": "pauseInfo", + "name": "pauseInfo", + "type": "dynamicHtml", + "html": "

Pause Duration Limit

Maximum pause duration: 4,380 blocks (~24.3 hours)

If not unpaused within this period, the validator will be automatically unstaked.

", + "span": { + "base": 12 + } + }, + { + "id": "memo", + "name": "memo", + "type": "text", + "label": "Memo (Optional)", + "required": false, + "placeholder": "Add a note about this pause action" + }, + { + "id": "txFee", + "name": "txFee", + "type": "amount", + "label": "Transaction Fee", + "value": "{{ fromMicroDenom<{{fees.raw.pauseFee}}> }}", + "autoPopulate": "always", + "required": true, + "min": "{{ fromMicroDenom<{{fees.raw.pauseFee}}> }}", + "validation": { + "messages": { + "required": "Transaction fee is required", + "min": "Transaction fee cannot be less than the network minimum: {{min}} {{chain.denom.symbol}}" + } + }, + "help": "Network minimum fee: {{ fromMicroDenom<{{fees.raw.pauseFee}}> }} {{chain.denom.symbol}}" + } + ], + "confirmation": { + "title": "Confirm Pause Validator", + "summary": [ + { + "label": "Validator Address", + "value": "{{shortAddress<{{form.validatorAddress}}>}}" + }, + { + "label": "Signer Address", + "value": "{{shortAddress<{{form.signerAddress}}>}}" + }, + { + "label": "Memo", + "value": "{{form.memo || 'No memo'}}" + }, + { + "label": "Transaction Fee", + "value": "{{numberToLocaleString<{{form.txFee}}>}} {{chain.denom.symbol}}" + } + ], + "btn": { + "icon": "Pause", + "label": "Pause Validator" + } + } + }, + "payload": { + "address": { + "value": "{{form.validatorAddress}}", + "coerce": "string" + }, + "pubKey": { + "value": "", + "coerce": "string" + }, + "netAddress": { + "value": "", + "coerce": "string" + }, + "committees": { + "value": "", + "coerce": "string" + }, + "amount": { + "value": "0", + "coerce": "number" + }, + "delegate": { + "value": "false", + "coerce": "boolean" + }, + "earlyWithdrawal": { + "value": "false", + "coerce": "boolean" + }, + "output": { + "value": "", + "coerce": "string" + }, + "signer": { + "value": "{{form.signerAddress}}", + "coerce": "string" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.txFee}}>}}", + "coerce": "number" + }, + "submit": { + "value": "true", + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-pause", + "method": "POST" + } + }, + { + "id": "unpauseValidator", + "title": "Unpause Validator", + "icon": "Play", + "ds": { + "validator": { + "account": { + "address": "{{account.address}}" + } + }, + "fees": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 0, + "refetchOnMount": true + } + }, + "form": { + "fields": [ + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "readOnly": true, + "help": "The address of the validator to unpause" + }, + { + "id": "signerAddress", + "name": "signerAddress", + "type": "text", + "label": "Signer Address", + "value": "{{account.address}}", + "required": true, + "readOnly": true, + "help": "The address that will sign this transaction" + }, + { + "id": "memo", + "name": "memo", + "type": "text", + "label": "Memo (Optional)", + "required": false, + "placeholder": "Add a note about this unpause action" + }, + { + "id": "txFee", + "name": "txFee", + "type": "amount", + "label": "Transaction Fee", + "value": "{{ fromMicroDenom<{{fees.raw.unpauseFee}}> }}", + "autoPopulate": "always", + "required": true, + "min": "{{ fromMicroDenom<{{fees.raw.unpauseFee}}> }}", + "validation": { + "messages": { + "required": "Transaction fee is required", + "min": "Transaction fee cannot be less than the network minimum: {{min}} {{chain.denom.symbol}}" + } + }, + "help": "Network minimum fee: {{ fromMicroDenom<{{fees.raw.unpauseFee}}> }} {{chain.denom.symbol}}" + } + ], + "confirmation": { + "title": "Confirm Unpause Validator", + "summary": [ + { + "label": "Validator Address", + "value": "{{shortAddress<{{form.validatorAddress}}>}}" + }, + { + "label": "Signer Address", + "value": "{{shortAddress<{{form.signerAddress}}>}}" + }, + { + "label": "Memo", + "value": "{{form.memo || 'No memo'}}" + }, + { + "label": "Transaction Fee", + "value": "{{numberToLocaleString<{{form.txFee}}>}} {{chain.denom.symbol}}" + } + ], + "btn": { + "icon": "Play", + "label": "Unpause Validator" + } + } + }, + "payload": { + "address": { + "value": "{{form.validatorAddress}}", + "coerce": "string" + }, + "pubKey": { + "value": "", + "coerce": "string" + }, + "netAddress": { + "value": "", + "coerce": "string" + }, + "committees": { + "value": "", + "coerce": "string" + }, + "amount": { + "value": "0", + "coerce": "number" + }, + "delegate": { + "value": "false", + "coerce": "boolean" + }, + "earlyWithdrawal": { + "value": "false", + "coerce": "boolean" + }, + "output": { + "value": "", + "coerce": "string" + }, + "signer": { + "value": "{{form.signerAddress}}", + "coerce": "string" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.txFee}}>}}", + "coerce": "number" + }, + "submit": { + "value": "true", + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-unpause", + "method": "POST" + } + }, + { + "id": "unstake", + "title": "Unstake", + "icon": "Unlock", + "ds": { + "validator": { + "account": { + "address": "{{form.validatorAddress}}" + }, + "__options": { + "enabled": "{{ form.validatorAddress }}" + } + }, + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 0, + "refetchOnMount": true + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[40rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "readOnly": true, + "help": "The address of the validator to unstake" + }, + { + "id": "validatorInfo", + "name": "validatorInfo", + "type": "dynamicHtml", + "html": "

Validator Information

Current Stake:

{{formatToCoin<{{ds.validator.stakedAmount ?? 0}}>}} {{chain.denom.symbol}}

Committees:

{{ds.validator.committees ?? 'N/A'}}

Type:

{{ ds.validator.delegate ? 'Delegation' : 'Validation' }}

", + "showIf": "{{ form.validatorAddress && ds.validator }}", + "span": { + "base": 12 + } + }, + { + "id": "signerAddress", + "name": "signerAddress", + "type": "text", + "label": "Signer Address", + "value": "{{account.address}}", + "required": true, + "readOnly": true, + "help": "The address that will sign this transaction" + }, + { + "id": "earlyWithdrawal", + "name": "earlyWithdrawal", + "type": "optionCard", + "label": "Withdrawal Type", + "required": true, + "value": false, + "autoPopulate": "once", + "options": [ + { + "label": "Normal Unstake", + "value": false, + "help": "Wait ~40 seconds (2 blocks)", + "toolTip": "Unstake following the normal unstaking period of 2 blocks (~40 seconds). No penalties applied." + }, + { + "label": "Early Withdrawal", + "value": true, + "help": "Immediate withdrawal with penalty", + "toolTip": "Withdraw immediately but pay an early withdrawal penalty fee." + } + ] + }, + { + "id": "earlyWithdrawalWarning", + "name": "earlyWithdrawalWarning", + "type": "dynamicHtml", + "html": "

20% Early Withdrawal Penalty

Early withdrawal incurs a 20% reduction of your staked amount.

Current stake:{{formatToCoin<{{ds.validator?.stakedAmount ?? 0}}>}} CNPY
You will receive:{{formatToCoin<{{(ds.validator?.stakedAmount ?? 0) * 0.8}}>}} CNPY

Funds available immediately after confirmation.

", + "showIf": "{{ form.earlyWithdrawal === true && ds.validator }}", + "span": { + "base": 12 + } + }, + { + "id": "memo", + "name": "memo", + "type": "text", + "label": "Memo (Optional)", + "required": false, + "placeholder": "Add a note about this unstake action" + }, + { + "id": "txFee", + "name": "txFee", + "type": "amount", + "label": "Transaction Fee", + "value": "{{ fromMicroDenom<{{fees.raw.unstakeFee}}> }}", + "autoPopulate": "always", + "required": true, + "min": "{{ fromMicroDenom<{{fees.raw.unstakeFee}}> }}", + "validation": { + "messages": { + "required": "Transaction fee is required", + "min": "Transaction fee cannot be less than the network minimum: {{min}} {{chain.denom.symbol}}" + } + }, + "help": "Network minimum fee: {{ fromMicroDenom<{{fees.raw.unstakeFee}}> }} {{chain.denom.symbol}}" + } + ], + "confirmation": { + "title": "Confirm Unstake", + "summary": [ + { + "label": "Validator Address", + "value": "{{shortAddress<{{form.validatorAddress}}>}}" + }, + { + "label": "Current Stake", + "value": "{{formatToCoin<{{ds.validator.stakedAmount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Withdrawal Type", + "value": "{{ form.earlyWithdrawal ? 'Early Withdrawal (with penalty)' : 'Normal Unstake' }}" + }, + { + "label": "Signer Address", + "value": "{{shortAddress<{{form.signerAddress}}>}}" + }, + { + "label": "Memo", + "value": "{{form.memo || 'No memo'}}" + }, + { + "label": "Transaction Fee", + "value": "{{numberToLocaleString<{{form.txFee}}>}} {{chain.denom.symbol}}" + } + ], + "btn": { + "icon": "Unlock", + "label": "Unstake Validator" + } + } + }, + "payload": { + "address": { + "value": "{{form.validatorAddress}}", + "coerce": "string" + }, + "pubKey": { + "value": "", + "coerce": "string" + }, + "netAddress": { + "value": "", + "coerce": "string" + }, + "committees": { + "value": "", + "coerce": "string" + }, + "amount": { + "value": "0", + "coerce": "number" + }, + "delegate": { + "value": "false", + "coerce": "boolean" + }, + "earlyWithdrawal": { + "value": "{{form.earlyWithdrawal}}", + "coerce": "boolean" + }, + "output": { + "value": "", + "coerce": "string" + }, + "signer": { + "value": "{{form.signerAddress}}", + "coerce": "string" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.txFee}}>}}", + "coerce": "number" + }, + "submit": { + "value": "true", + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-unstake", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Unstake Successful!", + "description": "{{ form.earlyWithdrawal ? 'Your stake has been withdrawn immediately with early withdrawal penalty.' : 'Your validator has been unstaked. Funds will be available after the unstaking period.' }}", + "actions": [ + { + "type": "link", + "label": "View Transaction", + "href": "{{chain.explorer.tx}}/{{result}}", + "newTab": true + } + ] + }, + "onError": { + "variant": "error", + "title": "Unstake Failed", + "description": "{{result.error || 'An error occurred while processing your unstake request.'}}", + "sticky": true + } + } + } + ] +} diff --git a/cmd/rpc/web/wallet-new/src/actions/ActionRunner.tsx b/cmd/rpc/web/wallet-new/src/actions/ActionRunner.tsx new file mode 100644 index 000000000..f1033f313 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ActionRunner.tsx @@ -0,0 +1,791 @@ +// ActionRunner.tsx +import React from "react"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import FormRenderer from "./FormRenderer"; +import { useResolvedFees } from "@/core/fees"; +import { useSession, attachIdleRenew } from "@/state/session"; +import UnlockModal from "../components/UnlockModal"; +import useDebouncedValue from "../core/useDebouncedValue"; +import { + getFieldsFromAction, + normalizeFormForAction, + buildPayloadFromAction, +} from "@/core/actionForm"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { template, templateBool } from "@/core/templater"; +import { resolveToastFromManifest } from "@/toast/manifestRuntime"; +import { useToast } from "@/toast/ToastContext"; +import { + genericResultMap, + pauseValidatorMap, + unpauseValidatorMap, +} from "@/toast/mappers"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { cx } from "@/ui/cx"; +import { motion } from "framer-motion"; +import { ToastTemplateOptions } from "@/toast/types"; +import { useActionDs } from "./useActionDs"; + +type Stage = "form" | "confirm" | "executing" | "result"; + +export default function ActionRunner({ + actionId, + onFinish, + className, + prefilledData, +}: { + actionId: string; + onFinish?: () => void; + className?: string; + prefilledData?: Record; +}) { + const toast = useToast(); + + const [formHasErrors, setFormHasErrors] = React.useState(false); + const [stage, setStage] = React.useState("form"); + const [form, setForm] = React.useState>( + prefilledData || {}, + ); + // Reduce debounce time from 250ms to 100ms for better responsiveness + // especially important for prefilledData and DS-dependent fields + const debouncedForm = useDebouncedValue(form, 100); + const [txRes, setTxRes] = React.useState(null); + const [localDs, setLocalDs] = React.useState>({}); + // Track which fields have been auto-populated at least once + // Initialize with prefilled field names to prevent auto-populate from overriding them + const [autoPopulatedOnce, setAutoPopulatedOnce] = React.useState>( + new Set(prefilledData ? Object.keys(prefilledData) : []), + ); + // Track which fields were programmatically prefilled (from prefilledData or modules) + // These fields should hide paste button even when they have values + const [programmaticallyPrefilled, setProgrammaticallyPrefilled] = React.useState>( + new Set(prefilledData ? Object.keys(prefilledData) : []), + ); + + const { manifest, chain, params, isLoading } = useConfig(); + const { selectedAccount } = useAccounts?.() ?? { selectedAccount: undefined }; + const session = useSession(); + + const action = React.useMemo( + () => manifest?.actions.find((a) => a.id === actionId), + [manifest, actionId], + ); + + // NEW: Load action-level DS (replaces per-field DS for better performance) + const actionDsConfig = React.useMemo(() => (action as any)?.ds, [action]); + + // Build context for DS (without ds itself to avoid circular dependency) + // Use form (not debounced) for DS context to ensure immediate reactivity with prefilledData + // The DS hook itself handles debouncing internally where needed + const dsCtx = React.useMemo( + () => ({ + form: form, + chain, + account: selectedAccount + ? { + address: selectedAccount.address, + nickname: selectedAccount.nickname, + pubKey: selectedAccount.publicKey, + } + : undefined, + params, + }), + [form, chain, selectedAccount, params], + ); + + const { ds: actionDs } = useActionDs( + actionDsConfig, + dsCtx, + actionId, + selectedAccount?.address, + ); + + // Merge action-level DS with field-level DS (for backwards compatibility) + const mergedDs = React.useMemo( + () => ({ + ...actionDs, + ...localDs, + }), + [actionDs, localDs], + ); + const feesResolved = useResolvedFees(chain?.fees, { + actionId: action?.id, + bucket: "avg", + ctx: { chain }, + }); + + const ttlSec = chain?.session?.unlockTimeoutSec ?? 900; + React.useEffect(() => { + attachIdleRenew(ttlSec); + }, [ttlSec]); + + const requiresAuth = + (action?.auth?.type ?? + (action?.submit?.base === "admin" ? "sessionPassword" : "none")) === + "sessionPassword"; + const [unlockOpen, setUnlockOpen] = React.useState(false); + + // Check if submit button should be hidden (for view-only actions like "receive") + const hideSubmit = (action as any)?.ui?.hideSubmit ?? false; + + /** + * Helper function for modules/components to mark fields as programmatically prefilled + * This will hide the paste button for those fields + * + * Usage example in a custom component: + * ```tsx + * // When programmatically setting a value + * setVal('output', someAddress); + * ctx.__markFieldsAsPrefilled(['output']); + * ``` + * + * @param fieldNames - Array of field names to mark as programmatically prefilled + */ + const markFieldsAsPrefilled = React.useCallback((fieldNames: string[]) => { + setProgrammaticallyPrefilled((prev) => { + const newSet = new Set(prev); + fieldNames.forEach((name) => newSet.add(name)); + return newSet; + }); + }, []); + + /** + * Helper function to unmark fields (allow paste button again) + * Use this when user manually clears the field + * + * @param fieldNames - Array of field names to unmark + */ + const unmarkFieldsAsPrefilled = React.useCallback((fieldNames: string[]) => { + setProgrammaticallyPrefilled((prev) => { + const newSet = new Set(prev); + fieldNames.forEach((name) => newSet.delete(name)); + return newSet; + }); + }, []); + + const templatingCtx = React.useMemo( + () => ({ + form: debouncedForm, + layout: (action as any)?.form?.layout, + chain, + account: selectedAccount + ? { + address: selectedAccount.address, + nickname: selectedAccount.nickname, + pubKey: selectedAccount.publicKey, + } + : undefined, + fees: { + ...feesResolved, + }, + params: { + ...params, + }, + ds: mergedDs, // Use merged DS (action-level + field-level) + session: { password: session?.password }, + // Unique scope for this action instance to prevent cache collisions + __scope: `action:${actionId}:${selectedAccount?.address || "no-account"}`, + // Track programmatically prefilled fields (hide paste button for these) + __programmaticallyPrefilled: programmaticallyPrefilled, + // Helper functions for custom components + __markFieldsAsPrefilled: markFieldsAsPrefilled, + __unmarkFieldsAsPrefilled: unmarkFieldsAsPrefilled, + }), + [ + debouncedForm, + chain, + selectedAccount, + feesResolved, + session?.password, + params, + mergedDs, + actionId, + programmaticallyPrefilled, + markFieldsAsPrefilled, + unmarkFieldsAsPrefilled, + ], + ); + + const infoItems = React.useMemo( + () => + (action?.form as any)?.info?.items?.map((it: any) => ({ + label: + typeof it.label === "string" + ? template(it.label, templatingCtx) + : it.label, + icon: it.icon, + value: + typeof it.value === "string" + ? template(it.value, templatingCtx) + : it.value, + })) ?? [], + [action, templatingCtx], + ); + + const rawSummary = React.useMemo(() => { + const formSum = (action as any)?.form?.confirmation?.summary; + return Array.isArray(formSum) ? formSum : []; + }, [action]); + + const summaryTitle = React.useMemo(() => { + const title = (action as any)?.form?.confirmation?.title; + return typeof title === "string" ? template(title, templatingCtx) : title; + }, [action, templatingCtx]); + + const resolvedSummary = React.useMemo(() => { + return rawSummary.map((item: any) => ({ + label: + typeof item.label === "string" + ? template(item.label, templatingCtx) + : item.label, + icon: item.icon, // opcional + value: + typeof item.value === "string" + ? template(item.value, templatingCtx) + : item.value, + })); + }, [rawSummary, templatingCtx]); + + const hasSummary = resolvedSummary.length > 0; + + const confirmBtn = React.useMemo(() => { + const btn = + (action as any)?.form?.confirmation?.btns?.submit ?? + (action as any)?.form?.confirmation?.btn ?? + {}; + return { + label: + typeof btn.label === "string" + ? template(btn.label, templatingCtx) + : (btn.label ?? "Confirm"), + icon: btn.icon ?? undefined, + }; + }, [action, templatingCtx]); + + const isReady = React.useMemo(() => !!action && !!chain, [action, chain]); + + const didInitToastRef = React.useRef(false); + React.useEffect(() => { + if (!action || !isReady) return; + if (didInitToastRef.current) return; + const t = resolveToastFromManifest(action, "onInit", templatingCtx); + if (t) toast.neutral(t); + didInitToastRef.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [action, isReady]); + + const normForm = React.useMemo( + () => normalizeFormForAction(action as any, debouncedForm), + [action, debouncedForm], + ); + const payload = React.useMemo( + () => + buildPayloadFromAction(action as any, { + form: normForm, + chain, + session: { password: session.password }, + account: selectedAccount + ? { + address: selectedAccount.address, + nickname: selectedAccount.nickname, + pubKey: selectedAccount.publicKey, + } + : undefined, + fees: { + ...feesResolved, + }, + ds: mergedDs, + }), + [ + action, + normForm, + chain, + session.password, + feesResolved, + selectedAccount, + mergedDs, + ], + ); + + const host = React.useMemo(() => { + if (!action || !chain) return ""; + return action?.submit?.base === "admin" + ? (chain.rpc.admin ?? chain.rpc.base ?? "") + : (chain.rpc.base ?? ""); + }, [action, chain]); + + const doExecute = React.useCallback(async () => { + if (!isReady) return; + if (requiresAuth && !session.isUnlocked()) { + setUnlockOpen(true); + return; + } + const before = resolveToastFromManifest( + action, + "onBeforeSubmit", + templatingCtx, + ); + if (before) toast.neutral(before); + setStage("executing"); + const submitPath = + typeof action!.submit?.path === "string" + ? template(action!.submit.path, templatingCtx) + : action!.submit?.path; + const res = await fetch(host + submitPath, { + method: action!.submit?.method, + headers: action!.submit?.headers ?? { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }).then((r) => r.json()); + + + setTxRes(res); + + // Fix success detection - handle both string (tx hash) and object responses + const isSuccess = + typeof res === "string" // If response is a string (tx hash), it's a success + ? true + : res?.ok === true || + (res?.status && res.status >= 200 && res.status < 300) || + (!res?.error && !res?.ok && res?.status !== false); + const key = isSuccess ? "onSuccess" : "onError"; + const t = resolveToastFromManifest(action, key as any, templatingCtx, res); + + if (t) { + toast.toast(t); + } else { + // Select appropriate mapper based on action ID + let mapper = genericResultMap; + if (action?.id === "pauseValidator") { + mapper = pauseValidatorMap; + } else if (action?.id === "unpauseValidator") { + mapper = unpauseValidatorMap; + } + + toast.fromResult({ + result: typeof res === "string" ? res : { ...res, ok: isSuccess }, + ctx: templatingCtx, + map: (r, c) => mapper(r, c), + fallback: { + title: "Processed", + variant: "neutral", + ctx: templatingCtx, + } as ToastTemplateOptions, + }); + } + const fin = resolveToastFromManifest( + action, + "onFinally", + templatingCtx, + res, + ); + if (fin) toast.info(fin); + + // Close modal/finish action after execution with a small delay + // to allow toast to be visible before modal closes + setTimeout(() => { + if (onFinish) { + onFinish(); + } else { + // If no onFinish callback, reset to form stage + setStage("form"); + setStepIdx(0); + } + }, 500); + }, [isReady, requiresAuth, session, host, action, payload]); + + const onContinue = React.useCallback(() => { + if (formHasErrors) { + // opcional: mostrar toast o vibrar el botón + return; + } + if (hasSummary) { + setStage("confirm"); + } else { + void doExecute(); + } + }, [formHasErrors, hasSummary, doExecute]); + + const onConfirm = React.useCallback(() => { + if (formHasErrors) { + // opcional: toast + return; + } + void doExecute(); + }, [formHasErrors, doExecute]); + + const onBackToForm = React.useCallback(() => { + setStage("form"); + }, []); + + React.useEffect(() => { + if (unlockOpen && session.isUnlocked()) { + setUnlockOpen(false); + void doExecute(); + } + }, [unlockOpen, session]); + + const onFormChange = React.useCallback((patch: Record) => { + setForm((prev) => ({ ...prev, ...patch })); + }, []); + + const [errorsMap, setErrorsMap] = React.useState>({}); + const [stepIdx, setStepIdx] = React.useState(0); + + const wizard = React.useMemo(() => (action as any)?.form?.wizard, [action]); + const allFields = React.useMemo(() => getFieldsFromAction(action), [action]); + + const steps = React.useMemo(() => { + if (!wizard) return []; + const declared = Array.isArray(wizard.steps) ? wizard.steps : []; + if (declared.length) return declared; + const uniq = Array.from( + new Set(allFields.map((f: any) => f.step).filter(Boolean)), + ); + return uniq.map((id: any, i) => ({ id, title: `Step ${i + 1}` })); + }, [wizard, allFields]); + + const fieldsForStep = React.useMemo(() => { + if (!wizard || !steps.length) return allFields; + const cur = steps[stepIdx]?.id ?? stepIdx + 1; + return allFields.filter( + (f: any) => (f.step ?? 1) === cur || String(f.step) === String(cur), + ); + }, [wizard, steps, stepIdx, allFields]); + + const visibleFieldsForStep = React.useMemo(() => { + const list = fieldsForStep ?? []; + return list.filter((f: any) => { + if (!f?.showIf) return true; + try { + return templateBool(f.showIf, { ...templatingCtx, form }); + } catch (e) { + console.warn("Error evaluating showIf", f.name, e); + return true; + } + }); + }, [fieldsForStep, templatingCtx, form]); + + // Auto-populate form with default values from field.value when DS data or visible fields change + const prevStateRef = React.useRef<{ ds: string; fieldNames: string }>({ + ds: "", + fieldNames: "", + }); + React.useEffect(() => { + const dsSnapshot = JSON.stringify(mergedDs); + const fieldNamesSnapshot = visibleFieldsForStep + .map((f: any) => f.name) + .join(","); + const stateSnapshot = { ds: dsSnapshot, fieldNames: fieldNamesSnapshot }; + + // Only run when DS or visible fields change + if ( + prevStateRef.current.ds === dsSnapshot && + prevStateRef.current.fieldNames === fieldNamesSnapshot + ) { + return; + } + prevStateRef.current = stateSnapshot; + + setForm((prev) => { + const defaults: Record = {}; + let hasDefaults = false; + + // Build template context with current form state + const ctx = { + form: prev, + chain, + account: selectedAccount + ? { + address: selectedAccount.address, + nickname: selectedAccount.nickname, + pubKey: selectedAccount.publicKey, + } + : undefined, + fees: { ...feesResolved }, + params: { ...params }, + ds: mergedDs, + }; + + for (const field of visibleFieldsForStep) { + const fieldName = (field as any).name; + const fieldValue = (field as any).value; + const autoPopulate = (field as any).autoPopulate ?? "always"; // 'always' | 'once' | false + + // Skip auto-population if field has autoPopulate: false + if (autoPopulate === false) { + continue; + } + + // Skip if autoPopulate: 'once' and field was already populated + if (autoPopulate === "once" && autoPopulatedOnce.has(fieldName)) { + continue; + } + + // For 'always' mode: always update, for 'once': only if empty + const shouldPopulate = + fieldValue != null && + (autoPopulate === "always" || + prev[fieldName] === undefined || + prev[fieldName] === "" || + prev[fieldName] === null); + + if (shouldPopulate) { + try { + const resolved = template(fieldValue, ctx); + if ( + resolved !== undefined && + resolved !== "" && + resolved !== null + ) { + defaults[fieldName] = resolved; + hasDefaults = true; + + // Mark as populated if autoPopulate is 'once' + if (autoPopulate === "once") { + setAutoPopulatedOnce((prev) => new Set([...prev, fieldName])); + } + } + } catch (e) { + // Template resolution failed, skip + } + } + } + + return hasDefaults ? { ...prev, ...defaults } : prev; + }); + }, [ + mergedDs, + visibleFieldsForStep, + chain, + selectedAccount, + feesResolved, + params, + ]); + + const handleErrorsChange = React.useCallback( + (errs: Record, hasErrors: boolean) => { + setErrorsMap(errs); + setFormHasErrors(hasErrors); + }, + [], + ); + + const hasStepErrors = React.useMemo(() => { + const missingRequired = visibleFieldsForStep.some( + (f: any) => f.required && (form[f.name] == null || form[f.name] === ""), + ); + const fieldErrors = visibleFieldsForStep.some( + (f: any) => !!errorsMap[f.name], + ); + return missingRequired || fieldErrors; + }, [visibleFieldsForStep, form, errorsMap]); + + const isLastStep = !wizard || stepIdx >= steps.length - 1; + + const goNext = React.useCallback(() => { + if (hasStepErrors) return; + if (!wizard || isLastStep) { + if (hasSummary) setStage("confirm"); + else void doExecute(); + } else { + setStepIdx((i) => i + 1); + } + }, [wizard, isLastStep, hasStepErrors, hasSummary, doExecute]); + + const goPrev = React.useCallback(() => { + if (!wizard) return; + setStepIdx((i) => Math.max(0, i - 1)); + }, [wizard]); + + return ( +
+ {stage === "confirm" && ( + + )} +
+ {isLoading &&
Loading…
} + {!isLoading && !isReady && ( +
No action "{actionId}" found in manifest
+ )} + + {!isLoading && isReady && ( + <> + {stage === "form" && ( + + + + {wizard && steps.length > 0 && ( +
+
{steps[stepIdx]?.title ?? `Step ${stepIdx + 1}`}
+
+ {stepIdx + 1} / {steps.length} +
+
+ )} + + {infoItems.length > 0 && ( +
+ {action?.form?.info?.title && ( +

+ {template(action?.form?.info?.title, templatingCtx)} +

+ )} +
+ {infoItems.map( + ( + d: { + icon: string | undefined; + label: + | string + | number + | boolean + | React.ReactElement< + any, + string | React.JSXElementConstructor + > + | Iterable + | React.ReactPortal + | null + | undefined; + value: any; + }, + i: React.Key | null | undefined, + ) => ( +
+
+ {d.icon ? ( + + ) : null} + + {d.label} + {d.value && ":"} + +
+ {d.value && ( + + {String(d.value ?? "—")} + + )} +
+ ), + )} +
+
+ )} + + {!hideSubmit && ( +
+ {wizard && stepIdx > 0 && ( + + )} + +
+ )} +
+ )} + + {stage === "confirm" && ( + +
+ {summaryTitle && ( +

{summaryTitle}

+ )} + +
+ {resolvedSummary.map((d, i) => ( +
+
+ {d.icon ? ( + + ) : null} + {d.label}: +
+ + {String(d.value ?? "—")} + +
+ ))} +
+
+ +
+ +
+
+ )} + + {stage === "executing" && ( + +
+
+
+
+

+ Processing Transaction... +

+

+ Please wait while your transaction is being processed +

+
+
+ )} + + setUnlockOpen(false)} + /> + + )} +
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/actions/ActionsModal.tsx b/cmd/rpc/web/wallet-new/src/actions/ActionsModal.tsx new file mode 100644 index 000000000..af4e32cd3 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ActionsModal.tsx @@ -0,0 +1,117 @@ +// ActionsModal.tsx +import React, {useEffect, useMemo, useState} from 'react' +import {motion, AnimatePresence} from 'framer-motion' +import {ModalTabs, Tab} from './ModalTabs' +import {Action as ManifestAction} from '@/manifest/types' +import ActionRunner from '@/actions/ActionRunner' +import {XIcon} from 'lucide-react' +import {cx} from '@/ui/cx' + +interface ActionModalProps { + actions?: (ManifestAction & { prefilledData?: Record })[] + isOpen: boolean + onClose: () => void +} + +export const ActionsModal: React.FC = ({ + actions, + isOpen, + onClose + }) => { + const [selectedTab, setSelectedTab] = useState(undefined) + + const modalSlot = useMemo(() => { + return actions?.find(a => a.id === selectedTab?.value)?.ui?.slots?.modal + }, [selectedTab, actions]) + + const modalClassName = modalSlot?.className + const modalStyle: React.CSSProperties | undefined = modalSlot?.style + + const availableTabs = useMemo(() => { + return ( + actions?.map(a => ({ + value: a.id, + label: a.title || a.id, + icon: a.icon + })) || [] + ) + }, [actions]) + + useEffect(() => { + if (availableTabs.length > 0) setSelectedTab(availableTabs[0]) + }, [availableTabs]) + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = 'auto' + } + } + }, [isOpen]) + + return ( + + {isOpen && ( + + e.stopPropagation()} + > + + + + + {selectedTab && ( + + a.id === selectedTab.value)?.prefilledData} + /> + + )} + + + )} + + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/ComboSelect.tsx b/cmd/rpc/web/wallet-new/src/actions/ComboSelect.tsx new file mode 100644 index 000000000..89e3f4d6c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ComboSelect.tsx @@ -0,0 +1,246 @@ +// ComboSelect.tsx — asigna un valor libre y lo muestra como “opción extra” seleccionada +// (MISMO DISEÑO: mismas clases y tokens que tu versión) +"use client"; + +import * as React from "react"; +import * as Popover from "@radix-ui/react-popover"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import {ArrowRight, Check, ChevronsUpDown} from "lucide-react"; +import {cx} from "@/ui/cx"; + +export type ComboOption = { label: string; value: string; disabled?: boolean }; + +export type ComboSelectProps = { + id?: string; + value?: string | null; + options: ComboOption[]; + onChange: (val: string | null, meta?: { assigned?: boolean }) => void; + + placeholder?: string; + emptyText?: string; + disabled?: boolean; + + /** Permite asignar el texto escrito como valor del select (sin crearlo en la lista). */ + allowAssign?: boolean; + /** Enter confirma el texto aunque no esté en options (atajo de teclado). */ + allowFreeInput?: boolean; + + // Estilo + className?: string; // Popover.Content + buttonClassName?: string; // Trigger + listHeight?: number; // px +}; + +export default function ComboSelect({ + id, + value, + options, + onChange, + placeholder = "Select", + emptyText = "No results", + disabled, + allowAssign = true, + allowFreeInput = true, + className, + buttonClassName, + listHeight = 240, + }: ComboSelectProps) { + const [open, setOpen] = React.useState(false); + const [query, setQuery] = React.useState(""); + const inputRef = React.useRef(null); + const isClosingRef = React.useRef(false); + + // 🔹 Opción temporal “extra” cuando se asigna un valor libre + const [tempOption, setTempOption] = React.useState(null); + + // Si `value` viene de fuera y no existe en options, crea/actualiza tempOption para que se vea seleccionada + React.useEffect(() => { + if (!value) { + if (tempOption) setTempOption(null); + return; + } + const exists = options.some((o) => o.value === value); + if (!exists) { + setTempOption({value, label: value}); + } else if (tempOption && tempOption.value !== value) { + setTempOption(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, options]); + + // Lista a renderizar = options + tempOption (si aplica). No mutamos la original. + const mergedOptions = React.useMemo(() => { + if (tempOption && !options.some((o) => o.value === tempOption.value)) { + return [...options, tempOption]; + } + return options; + }, [options, tempOption]); + + const selected = mergedOptions.find((o) => o.value === value) || null; + + const filtered = React.useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return mergedOptions; + return mergedOptions.filter((o) => (o.label + " " + o.value).toLowerCase().includes(q)); + }, [mergedOptions, query]); + + const closePopover = React.useCallback(() => { + if (isClosingRef.current) return; + isClosingRef.current = true; + setOpen(false); + setQuery(""); + setTimeout(() => { + isClosingRef.current = false; + }, 100); + }, []); + + const assignValue = (text: string) => { + const v = text.trim(); + if (!v) return; + // Creamos/actualizamos la opción temporal y la seleccionamos + const opt = {value: v, label: v}; + setTempOption(opt); + onChange(v, {assigned: true}); // <- solo asigna; no persiste en options global + closePopover(); + }; + + const handlePick = (val: string) => { + onChange(val, {assigned: false}); + closePopover(); + }; + + const onKeyDown: React.KeyboardEventHandler = (e) => { + if (e.key === "Enter" && query.trim() && allowFreeInput && allowAssign) { + e.preventDefault(); + assignValue(query); + } + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + closePopover(); + } + }; + + return ( + { + if (!o) { + closePopover(); + } else { + if (!isClosingRef.current) { + setOpen(true); + setTimeout(() => inputRef.current?.focus(), 50); + } + } + }} + > + + + + + { + // Prevent closing when clicking on the trigger + const target = e.target as HTMLElement; + if (target.closest('[role="combobox"]')) { + e.preventDefault(); + return; + } + closePopover(); + }} + onEscapeKeyDown={(e) => { + e.preventDefault(); + closePopover(); + }} + className={ + className ?? + "z-50 w-[--radix-popover-trigger-width] min-w-56 rounded-xl p-2 shadow-xl bg-bg-tertiary border border-bg-accent" + } + > + {/* Input */} +
+ setQuery(e.target.value)} + onKeyDown={onKeyDown} + placeholder={placeholder} + className="w-full bg-transparent outline-none placeholder:text-neutral-400" + /> +
+ +
+ {filtered.length === 0 && ( +
{emptyText}
+ )} + + {filtered.length > 0 && ( + + +
    + {filtered.map((opt) => { + const isSel = value === opt.value; + return ( +
  • + +
  • + ); + })} +
+
+ + + +
+ )} + + {allowAssign && query.trim() && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/actions/Confirm.tsx b/cmd/rpc/web/wallet-new/src/actions/Confirm.tsx new file mode 100644 index 000000000..9c11f7a4b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/Confirm.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { cx } from '../ui/cx' + +function ConfirmInner({ + summary, payload, showPayload = false, ctaLabel = 'Confirm', danger = false, onBack, onConfirm +}: { + summary: { label: string; value: string }[] + payload?: any + showPayload?: boolean + ctaLabel?: string + danger?: boolean + onBack: () => void + onConfirm: () => void +}) { + const [open, setOpen] = React.useState(showPayload) + + return ( +
+
+
    + {summary.map((s, i) => ( +
  • + {s.label} + {s.value} +
  • + ))} +
+
+ + {payload != null && ( +
+
+
Raw Payload
+ +
+ {open && ( +
+{JSON.stringify(payload, null, 2)}
+            
+ )} +
+ )} + +
+ + +
+
+ ) +} + +export default React.memo(ConfirmInner); + + + diff --git a/cmd/rpc/web/wallet-new/src/actions/FieldControl.tsx b/cmd/rpc/web/wallet-new/src/actions/FieldControl.tsx new file mode 100644 index 000000000..155f4da5d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/FieldControl.tsx @@ -0,0 +1,126 @@ +import React from "react"; +import { Field } from "@/manifest/types"; +import { collectDepsFromObject, template } from "@/core/templater"; +import { templateBool } from "@/core/templater"; +import { useFieldDs } from "@/actions/useFieldsDs"; +import { getFieldRenderer } from "@/actions/fields/fieldRegistry"; + +type Props = { + f: Field; + value: Record; + errors: Record; + templateContext: Record; + setVal: (field: Field | string, v: any) => void; + setLocalDs?: React.Dispatch>>; +}; + +export const FieldControl: React.FC = ({ + f, + value, + errors, + templateContext, + setVal, + setLocalDs, +}) => { + const resolveTemplate = React.useCallback( + (s?: any) => (typeof s === "string" ? template(s, templateContext) : s), + [templateContext], + ); + + const manualWatch: string[] = React.useMemo(() => { + const dsObj: any = (f as any)?.ds; + const watch = dsObj?.__options?.watch; + return Array.isArray(watch) ? watch : []; + }, [f]); + + const autoWatchAllRoots: string[] = React.useMemo(() => { + const dsObj: any = (f as any)?.ds; + return collectDepsFromObject(dsObj); + }, [f]); + + const autoWatchFormOnly: string[] = React.useMemo(() => { + return autoWatchAllRoots + .filter((p) => p.startsWith("form.")) + .map((p) => p.replace(/^form\.\??/, "form.")); + }, [autoWatchAllRoots]); + + const watchPaths: string[] = React.useMemo(() => { + const merged = new Set([...manualWatch, ...autoWatchFormOnly]); + return Array.from(merged); + }, [manualWatch, autoWatchFormOnly]); + + const { data: dsValue } = useFieldDs(f, templateContext); + + React.useEffect(() => { + if (!setLocalDs || dsValue == null) return; + + const fieldDs = (f as any)?.ds; + if (!fieldDs || typeof fieldDs !== "object") return; + + const declaredKeys = Object.keys(fieldDs).filter((k) => k !== "__options"); + if (declaredKeys.length === 0) return; + + setLocalDs((prev) => { + const next = { ...(prev || {}) }; + let changed = false; + + for (const key of declaredKeys) { + const incoming = (dsValue as any)?.[key] ?? dsValue; + + if (incoming === undefined) continue; + + const prevForKey = (prev as any)?.[key]; + + try { + const prevStr = JSON.stringify(prevForKey); + const incomingStr = JSON.stringify(incoming); + if (prevStr !== incomingStr) { + next[key] = incoming; + changed = true; + } + } catch { + if (prevForKey !== incoming) { + next[key] = incoming; + changed = true; + } + } + } + + return changed ? next : prev; + }); + }, [dsValue, setLocalDs, f]); + + const isVisible = + (f as any).showIf == null + ? true + : templateBool((f as any).showIf, templateContext); + + if (!isVisible) return null; + + const FieldRenderer = getFieldRenderer(f.type); + + if (!FieldRenderer) { + return ( +
+ Unsupported field type: {f.type} +
+ ); + } + + const error = errors[f.name]; + const currentValue = value[f.name] ?? ""; + + return ( + setVal(f, val)} + resolveTemplate={resolveTemplate} + setVal={(fieldId: string, v: any) => setVal(fieldId, v)} + /> + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/actions/FormRenderer.tsx b/cmd/rpc/web/wallet-new/src/actions/FormRenderer.tsx new file mode 100644 index 000000000..69d8078ef --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/FormRenderer.tsx @@ -0,0 +1,170 @@ +import React from "react"; +import type { Field, FieldOp } from "@/manifest/types"; +import { cx } from "@/ui/cx"; +import { validateField } from "./validators"; +import { useSession } from "@/state/session"; +import { FieldControl } from "@/actions/FieldControl"; +import { motion } from "framer-motion"; + +const Grid: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +type Props = { + fields: Field[]; + value: Record; + onChange: (patch: Record) => void; + ctx?: Record; + onErrorsChange?: (errors: Record, hasErrors: boolean) => void; + onFormOperation?: (fieldOperation: FieldOp) => void; + onDsChange?: React.Dispatch>>; +}; + +export default function FormRenderer({ + fields, + value, + onChange, + ctx, + onErrorsChange, + onDsChange, +}: Props) { + const [errors, setErrors] = React.useState>({}); + const [localDs, setLocalDs] = React.useState>({}); + const session = useSession(); + + + // When localDs changes, notify parent (ActionRunner) + React.useEffect(() => { + if (onDsChange && Object.keys(localDs).length > 0) { + onDsChange((prev) => { + const merged = { ...prev, ...localDs }; + // Only update if actually changed + if (JSON.stringify(prev) === JSON.stringify(merged)) return prev; + return merged; + }); + } + }, [localDs, onDsChange]); + + // For DS-critical fields (option, optionCard, switch), use immediate form values + // For text input fields, use debounced values + const templateContext = React.useMemo( + () => ({ + form: value, // Use immediate form values for DS reactivity + chain: ctx?.chain, + account: ctx?.account, + ds: { ...(ctx?.ds || {}), ...localDs }, + fees: ctx?.fees, + params: ctx?.params, + layout: ctx?.layout, + session: { password: session?.password }, + }), + [ + value, + ctx?.chain, + ctx?.account, + ctx?.ds, + ctx?.fees, + ctx?.params, + ctx?.layout, + session?.password, + localDs, + ], + ); + + + const fieldsKeyed = React.useMemo( + () => + fields.map((f: any) => ({ + ...f, + __key: `${f.tab ?? "default"}:${f.group ?? ""}:${f.name}`, + })), + [fields], + ); + + /** setVal + async validation */ + const setVal = React.useCallback( + (fOrName: Field | string, v: any) => { + const name = + typeof fOrName === "string" ? fOrName : (fOrName as any).name; + onChange({ [name]: v }); + + void (async () => { + const f = + typeof fOrName === "string" + ? (fieldsKeyed.find((x) => x.name === fOrName) as Field | undefined) + : (fOrName as Field); + + const e = await validateField((f as any) ?? {}, v, templateContext); + const errorMessage = !e.ok ? e.message : ""; + setErrors((prev) => + prev[name] === errorMessage + ? prev + : { ...prev, [name]: errorMessage }, + ); + })(); + }, + [onChange, ctx?.chain, fieldsKeyed], + ); + + const hasActiveErrors = React.useMemo(() => { + const anyMsg = Object.values(errors).some((m) => !!m); + const requiredMissing = fields.some( + (f) => f.required && (value[f.name] == null || value[f.name] === ""), + ); + return anyMsg || requiredMissing; + }, [errors, fields, value]); + + React.useEffect(() => { + onErrorsChange?.(errors, hasActiveErrors); + }, [errors, hasActiveErrors, onErrorsChange]); + + const tabs = React.useMemo( + () => + Array.from( + new Set(fieldsKeyed.map((f: any) => f.tab).filter(Boolean)), + ) as string[], + [fieldsKeyed], + ); + const [activeTab, setActiveTab] = React.useState(tabs[0] ?? "default"); + const fieldsInTab = React.useCallback( + (t?: string) => + fieldsKeyed.filter((f: any) => (tabs.length ? f.tab === t : true)), + [fieldsKeyed, tabs], + ); + + return ( + <> + {tabs.length > 0 && ( +
+ {tabs.map((t) => ( + + ))} +
+ )} + + {(tabs.length ? fieldsInTab(activeTab) : fieldsKeyed).map((f: any) => ( + + ))} + + + ); +} diff --git a/cmd/rpc/web/wallet-new/src/actions/ModalTabs.tsx b/cmd/rpc/web/wallet-new/src/actions/ModalTabs.tsx new file mode 100644 index 000000000..dc437c98f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ModalTabs.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {LucideIcon} from "@/components/ui/LucideIcon"; + +export interface Tab { + value: string; + label: string; + icon?: string; +} + +interface ModalTabsProps { + tabs: Tab[]; + activeTab?: Tab; + onTabChange?: (tab: Tab) => void; +} + +export const ModalTabs: React.FC = ({ + tabs, + activeTab, + onTabChange, + }) => { + return ( +
+
+ {tabs.map((tab, index) => ( + + ))} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/actions/Option.tsx b/cmd/rpc/web/wallet-new/src/actions/Option.tsx new file mode 100644 index 000000000..b8f9a3d3a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/Option.tsx @@ -0,0 +1,39 @@ +import {cx} from "@/ui/cx"; + +export type OptionItem = { label: string; value: string; help?: string; icon?: string; toolTip?: string } + +export const Option: React.FC<{ + selected: boolean + disabled?: boolean + onSelect: () => void + label: React.ReactNode + help?: React.ReactNode, +}> = ({ selected, disabled, onSelect, label, help }) => ( + +); diff --git a/cmd/rpc/web/wallet-new/src/actions/OptionCard.tsx b/cmd/rpc/web/wallet-new/src/actions/OptionCard.tsx new file mode 100644 index 000000000..245be9ac9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/OptionCard.tsx @@ -0,0 +1,39 @@ +import {cx} from "@/ui/cx"; + +export type OptionCardOpt = { label: string; value: string; help?: string; icon?: string; toolTip?: string } + +export const OptionCard: React.FC<{ + selected: boolean + disabled?: boolean + onSelect: () => void + label: React.ReactNode + help?: React.ReactNode, +}> = ({ selected, disabled, onSelect, label, help }) => ( + +); diff --git a/cmd/rpc/web/wallet-new/src/actions/Result.tsx b/cmd/rpc/web/wallet-new/src/actions/Result.tsx new file mode 100644 index 000000000..9e677c307 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/Result.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +function ResultInner({ message, link, onDone }:{ message: string; link?: { label: string; href: string }; onDone: () => void }) { + return ( +
+
+

{message}

+ {link &&

{link.label}

} +
+ +
+ ); +} +export default React.memo(ResultInner); diff --git a/cmd/rpc/web/wallet-new/src/actions/TableSelect.tsx b/cmd/rpc/web/wallet-new/src/actions/TableSelect.tsx new file mode 100644 index 000000000..ac197fff5 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/TableSelect.tsx @@ -0,0 +1,344 @@ +import * as React from 'react' +import { templateBool } from '@/core/templater' // ajusta la ruta si aplica + +/** Tipos básicos del manifest */ +type ColAlign = 'left' | 'center' | 'right' +type ColumnType = 'text' | 'image' | 'html' | 'committee' + +export type TableSelectColumn = { + key?: string + title?: string + align?: ColAlign + type?: ColumnType + className?: string // custom CSS classes for the cell + + /** TEXT */ + expr?: string + + /** IMAGE */ + src?: string // expr o key -> URL de imagen (si no hay, cae a avatar) + alt?: string // expr opcional para alt + initialsFrom?: string // expr/llave para derivar iniciales y color si no hay 'src' + size?: number // tamaño del avatar/imagen en px (default 28) + + /** HTML */ + html?: string // HTML templated (se renderiza con dangerouslySetInnerHTML) +} + +export type TableRowAction = { + title?: string // título de cabecera para la columna de acción + label?: string // template del label del botón + icon?: string // (reservado) por si luego usas un icon set central + showIf?: string // template condicional + disabledIf?: string // template condicional para deshabilitar el botón + emit?: { + op: 'set' | 'copy' | 'select' // select: marcar selección; set: setear otro field; copy: al portapapeles + field?: string // requerido para 'set' + value?: string // template + } +} + +/** Config del field en manifest */ +export type TableSelectField = { + id: string + name: string + type: 'tableSelect' + label?: string + help?: string + required?: boolean + readOnly?: boolean + multiple?: boolean + rowKey?: string + columns: TableSelectColumn[] + rows?: any[] // data estática + source?: { uses: string; selector?: string } // data dinámica: p.ej. {uses:'ds', selector:'committees'} + rowAction?: TableRowAction + /** cómo se selecciona */ + selectMode?: 'row' | 'action' | 'none' // 'row' (default): click en fila; 'action': sólo botón; 'none': deshabilitado +} + +/** Props del componente */ +export type TableSelectProps = { + field: TableSelectField + currentValue: any + onChange: (next: any) => void + errors?: Record + resolveTemplate: (v: any) => any + template: (tpl: string, ctx?: any) => any + templateContext?: any +} + +/** Utils locales */ +const cx = (...a: Array) => a.filter(Boolean).join(' ') +const asArray = (x: any) => Array.isArray(x) ? x : (x == null ? [] : [x]) +const pick = (obj: any, path?: string) => !path ? obj : path.split('.').reduce((acc, k) => acc?.[k], obj) +const safe = (v: any) => v == null ? '' : String(v) + +/** Mobile-first: span según cantidad de columnas totales (12 = full) */ +function spanResponsiveByCount(colCount: number): string { + if (colCount <= 1) return 'col-span-12' + if (colCount === 2) return 'col-span-12 sm:col-span-6 md:col-span-6' + if (colCount === 3) return 'col-span-12 sm:col-span-6 md:col-span-4 lg:col-span-4' + if (colCount === 4) return 'col-span-12 sm:col-span-6 md:col-span-3 lg:col-span-3' + if (colCount === 5) return 'col-span-12 sm:col-span-6 md:col-span-2 lg:col-span-2' + if (colCount === 6) return 'col-span-12 sm:col-span-6 md:col-span-2 lg:col-span-2' + return 'col-span-12 sm:col-span-6 md:col-span-2 lg:col-span-1' // 7+ +} + +/** Avatar helpers (para fallback cuando no hay imagen) */ +function hashColor(input: string): string { + let h = 0 + for (let i = 0; i < input.length; i++) h = (h << 5) - h + input.charCodeAt(i) + const hue = Math.abs(h) % 360 + return `hsl(${hue} 65% 45%)` +} +function getInitials(text?: string) { + const p = (text ?? '').trim().split(/\s+/) + const first = p[0]?.[0] ?? '' + const last = p.length > 1 ? p[p.length - 1]?.[0] ?? '' : '' + return (first + last).toUpperCase() || (text?.[0]?.toUpperCase() ?? '•') +} + +const TableSelect: React.FC = ({ + field: tf, + currentValue, + onChange, + errors = {}, + resolveTemplate, + template, + templateContext + }) => { + const columns = React.useMemo( + () => (tf.columns ?? []).map(c => ({ ...c, title: c.title ? resolveTemplate(c.title) : undefined })), + [tf.columns, resolveTemplate] + ) + const keyField = tf.rowKey ?? 'id' + const label = resolveTemplate(tf.label) + const selectMode = tf.selectMode ?? 'row' + + const base = tf.source ? templateContext?.[tf.source.uses] : undefined + const dsRows = tf.source ? asArray(pick(base, tf.source.selector)) : [] + const staticRows = asArray(tf.rows) + const rows = React.useMemo( + () => (dsRows.length ? dsRows : staticRows).map((r: any, idx: number) => ({ __idx: idx, ...r })), + [dsRows, staticRows] + ) + + const selectedKeys: string[] = React.useMemo(() => { + return tf.multiple + ? asArray(currentValue).map(String) + : (currentValue != null && currentValue !== '' ? [String(currentValue)] : []) + }, [currentValue, tf.multiple]) + + const setSelectedKey = (k: string) => { + if (tf.readOnly) return + if (tf.multiple) { + const next = selectedKeys.includes(k) ? selectedKeys.filter(x => x !== k) : [...selectedKeys, k] + onChange(next) + } else { + onChange(selectedKeys[0] === k ? '' : k) + } + } + + const toggleRow = (row: any) => { + if (selectMode !== 'row' || tf.readOnly) return + const k = String(row[keyField] ?? row.__idx) + setSelectedKey(k) + } + + const renderAction = (row: any) => { + const ra = tf.rowAction + if (!ra) return null + const localCtx = { ...templateContext, row } + const visible = ra.showIf == null ? true : templateBool(ra.showIf, localCtx) + if (!visible) return null + + const k = String(row[keyField] ?? row.__idx) + const selected = selectedKeys.includes(k) + const disabled = ra.disabledIf != null ? templateBool(ra.disabledIf, localCtx) : false + const btnLabel = ra.label ? template(ra.label, localCtx) : 'Action' + const onClick = async (e: React.MouseEvent) => { + e.stopPropagation() + if (disabled) return + if (!ra.emit) return + if (ra.emit.op === 'set') { + const val = ra.emit.value ? template(ra.emit.value, localCtx) : undefined + onChange(val) + } else if (ra.emit.op === 'copy') { + const val = ra.emit.value ? template(ra.emit.value, localCtx) : JSON.stringify(row) + await navigator.clipboard.writeText(String(val ?? '')) + } else if (ra.emit.op === 'select') { + if (tf.readOnly) return + setSelectedKey(k) + } + } + return ( + + ) + } + + /** 4) Pintado */ + const colCount = columns.length + (tf.rowAction ? 1 : 0) + const colSpanCls = spanResponsiveByCount(colCount) + const cellAlign = (a?: ColAlign) => + a === 'right' ? 'text-right' : a === 'center' ? 'text-center' : 'text-left' + + const renderImageCell = (col: TableSelectColumn, row: any) => { + const local = { ...templateContext, row } + const size = (col.size ?? 28) + const src = col.src ? safe(template(col.src, local)) : '' + const alt = col.alt ? safe(template(col.alt, local)) : safe((col.key ? row[col.key] : row.name) ?? '') + const basis = col.initialsFrom ? safe(template(col.initialsFrom, local)) : safe((col.key ? row[col.key] : row.name) ?? '') + const initials = getInitials(basis) + const color = hashColor(basis) + + if (src) { + return ( + {alt} + ) + } + return ( + + {initials} + + ) + } + + const renderCommitteeCell = (row: any) => { + const name = row.name ?? '—' + const minStake = row.minStake ?? '' + const initials = getInitials(name) + const color = hashColor(name) + const size = 36 + + return ( +
+ + {initials} + +
+ {name} + Min: {minStake} +
+
+ ) + } + + const renderCell = (c: TableSelectColumn, row: any) => { + const local = { ...templateContext, row } + + if (c.type === 'committee') return renderCommitteeCell(row) + if (c.type === 'image') return renderImageCell(c, row) + + if (c.type === 'html' && c.html) { + const htmlString = template(c.html, local) + return
+ } + + const cellVal = c.expr + ? template(c.expr, local) + : (c.key ? row[c.key] : '') + + // Format numbers with locale and currency if it's a staked amount + const formattedVal = typeof cellVal === 'number' && c.key === 'stakedAmount' + ? `${cellVal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${templateContext?.chain?.denom?.symbol ?? 'CNPY'}` + : safe(cellVal ?? '—') + + return {formattedVal} + } + + return ( +
+ {!!label &&
{label}
} + +
+
+ {/* Header */} +
+ {columns.map((c, i) => ( +
+ {safe(c.title)} +
+ ))} + {tf.rowAction?.title && ( +
+ {resolveTemplate(tf.rowAction.title)} +
+ )} +
+ + {/* Rows */} +
+ {rows.map((row: any) => { + const k = String(row[keyField] ?? row.__idx) + const selected = selectedKeys.includes(k) + return ( + + ) + })} + {rows.length === 0 && ( +
No data
+ )} +
+
+
+ + {(errors[tf.name]) && ( +
+ {errors[tf.name]} +
+ )} +
+ ) +} + +export default TableSelect \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/actions/WizardRunner.tsx b/cmd/rpc/web/wallet-new/src/actions/WizardRunner.tsx new file mode 100644 index 000000000..1bcead1c7 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/WizardRunner.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import type { Action } from "@/manifest/types"; +import FormRenderer from "./FormRenderer"; +import Confirm from "./Confirm"; +import Result from "./Result"; +import { template } from "@/core/templater"; +import { useResolvedFees } from "@/core/fees"; +import { useSession, attachIdleRenew } from "@/state/session"; +import UnlockModal from "../components/UnlockModal"; +import { useConfig } from "@/app/providers/ConfigProvider"; + +type Stage = "form" | "confirm" | "executing" | "result"; + +export default function WizardRunner({ action }: { action: Action }) { + const { chain } = useConfig(); + const [stage, setStage] = React.useState("form"); + const [stepIndex, setStepIndex] = React.useState(0); + const step = action.steps?.[stepIndex]; + const [form, setForm] = React.useState>({}); + const [txRes, setTxRes] = React.useState(null); + + const session = useSession(); + const ttlSec = chain?.session?.unlockTimeoutSec ?? 900; + React.useEffect(() => { + attachIdleRenew(ttlSec); + }, [ttlSec]); + + const requiresAuth = + (action?.auth?.type ?? + (action?.rpc?.base === "admin" ? "sessionPassword" : "none")) === + "sessionPassword"; + const [unlockOpen, setUnlockOpen] = React.useState(false); + + const feesResolved = useResolvedFees(chain?.fees, { + actionId: action?.id, + bucket: "avg", + ctx: { form, chain, action }, + }); + const fee = feesResolved.amount; + + const host = React.useMemo( + () => + action.rpc?.base === "admin" + ? (chain?.rpc.admin ?? chain?.rpc.base ?? "") + : (chain?.rpc.base ?? ""), + [action.rpc?.base, chain?.rpc.admin, chain?.rpc.base], + ); + + const payload = React.useMemo( + () => + template(action.rpc?.payload ?? {}, { + form, + chain, + session: { password: session.password }, + }), + [action.rpc?.payload, form, chain, session.password], + ); + + const confirmSummary = React.useMemo( + () => + (action.confirm?.summary ?? []).map((s) => ({ + label: s.label, + value: template(s.value, { + form, + chain, + fees: { effective: fee }, + }), + })), + [action.confirm?.summary, form, chain, fee], + ); + + const onNext = React.useCallback(() => { + if ((action.steps?.length ?? 0) > stepIndex + 1) setStepIndex((i) => i + 1); + else setStage("confirm"); + }, [action.steps?.length, stepIndex]); + + const onPrev = React.useCallback(() => { + setStepIndex((i) => (i > 0 ? i - 1 : i)); + if (stepIndex === 0) setStage("form"); + }, [stepIndex]); + + const onFormChange = React.useCallback((patch: Record) => { + setForm((prev) => ({ ...prev, ...patch })); + }, []); + + const doExecute = React.useCallback(async () => { + if (requiresAuth && !session.isUnlocked()) { + setUnlockOpen(true); + return; + } + setStage("executing"); + const res = await fetch(host + action.rpc?.path, { + method: action.rpc?.method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) + .then((r) => r.json()) + .catch(() => ({ hash: "0xDEMO" })); + setTxRes(res); + setStage("result"); + }, [ + requiresAuth, + session, + host, + action.rpc?.method, + action.rpc?.path, + payload, + ]); + + React.useEffect(() => { + if (unlockOpen && session.isUnlocked()) { + setUnlockOpen(false); + void doExecute(); + } + }, [unlockOpen, session, doExecute]); + + if (!step) return
Invalid wizard
; + + const asideOn = step.form?.layout?.aside?.show; + const asideWidth = step.form?.layout?.aside?.width ?? 5; + const mainWidth = 12 - (asideOn ? asideWidth : 0); + + return ( +
+
+
+

{step.title ?? "Step"}

+
+ Step {stepIndex + 1} / {action.steps?.length ?? 1} +
+
+ +
+
+ +
+ {stepIndex > 0 && ( + + )} + +
+
+ + {asideOn && ( +
+
+
Sidebar
+
+ Add widget: {step.aside?.widget ?? "custom"} +
+
+
+ )} +
+ + {stage === "confirm" && ( + setStage("form")} + onConfirm={doExecute} + /> + )} + + setUnlockOpen(false)} + /> + + {stage === "result" && ( + { + setStepIndex(0); + setStage("form"); + }} + /> + )} +
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/actions/components/FieldFeatures.tsx b/cmd/rpc/web/wallet-new/src/actions/components/FieldFeatures.tsx new file mode 100644 index 000000000..a39916f73 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/components/FieldFeatures.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { FieldOp } from '@/manifest/types' +import { template } from '@/core/templater' +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' + +type FieldFeaturesProps = { + fieldId: string + features?: FieldOp[] + ctx: Record + setVal: (fieldId: string, v: any) => void + currentValue?: any +} + +export const FieldFeatures: React.FC = ({ features, ctx, setVal, fieldId, currentValue }) => { + const { copyToClipboard } = useCopyToClipboard() + + if (!features?.length) return null + + const resolve = (s?: any) => (typeof s === 'string' ? template(s, ctx) : s) + + // Check if this field was programmatically prefilled (from prefilledData or modules) + const isProgrammaticallyPrefilled = ctx?.__programmaticallyPrefilled?.has(fieldId) ?? false + + // Only hide paste button if field is programmatically prefilled AND has a value + const shouldHidePaste = isProgrammaticallyPrefilled && currentValue !== undefined && currentValue !== null && currentValue !== '' + + const labelFor = (op: FieldOp) => { + const opAny = op as any + if (opAny.op === 'copy') return 'Copy' + if (opAny.op === 'paste') return 'Paste' + if (opAny.op === 'set' || opAny.op === 'max') { + // Custom label or default to "Max" for set/max operations + return opAny.label ?? 'Max' + } + return opAny.op + } + + const handle = async (op: FieldOp) => { + const opAny = op as any + switch (opAny.op) { + case 'copy': { + const txt = String(resolve(opAny.from) ?? '') + await copyToClipboard(txt, opAny.label || 'Field value') + return + } + case 'paste': { + const txt = await navigator.clipboard.readText() + setVal(fieldId, txt) + return + } + case 'set': + case 'max': { + // Resolve the value from manifest (can be a template expression) + const v = resolve(opAny.value) + setVal(opAny.field ?? fieldId, v) + return + } + } + } + + // Filter features: hide paste button ONLY when field is programmatically prefilled + const visibleFeatures = features.filter((op) => { + const opAny = op as any + // Hide paste button only if field was programmatically prefilled (not from autopopulate/DS) + if (opAny.op === 'paste' && shouldHidePaste) { + return false + } + return true + }) + + // Don't render if no visible features + if (!visibleFeatures.length) return null + + return ( +
+ {visibleFeatures.map((op) => ( + + ))} +
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/AddressField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/AddressField.tsx new file mode 100644 index 000000000..57aec4d76 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/AddressField.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const AddressField: React.FC = ({ + field, + value, + error, + templateContext, + onChange, + resolveTemplate, + setVal, +}) => { + const resolved = resolveTemplate(field.value) + const currentValue = value === '' && resolved != null ? resolved : value + + const hasFeatures = !!(field.features?.length) + const common = 'w-full bg-transparent border placeholder-text-muted text-white rounded px-3 py-2 focus:outline-none' + const paddingRight = hasFeatures ? 'pr-20' : '' + const border = error ? 'border-red-600' : 'border-muted-foreground border-opacity-50' + + return ( + + onChange(e.target.value)} + /> + + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/AdvancedSelectField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/AdvancedSelectField.tsx new file mode 100644 index 000000000..a0f583f16 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/AdvancedSelectField.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { AdvancedSelectField as AdvancedSelectFieldType } from '@/manifest/types' +import { template, templateAny } from '@/core/templater' +import { toOptions } from '@/actions/utils/fieldHelpers' +import ComboSelectRadix from '@/actions/ComboSelect' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const AdvancedSelectField: React.FC = ({ + field, + value, + error, + templateContext, + dsValue, + onChange, + resolveTemplate, +}) => { + const select = field as AdvancedSelectFieldType + const staticOptions = Array.isArray(select.options) ? select.options : [] + const rawOptions = dsValue && Object.keys(dsValue).length ? dsValue : staticOptions + + let mappedFromExpr: any[] | null = null + if (typeof (select as any).map === 'string') { + try { + const out = templateAny((select as any).map, templateContext) + if (Array.isArray(out)) { + mappedFromExpr = out + } else if (typeof out === 'string') { + try { + const maybe = JSON.parse(out) + if (Array.isArray(maybe)) mappedFromExpr = maybe + } catch {} + } + } catch (err) { + console.warn('select.map expression error:', err) + } + } + + const builtOptions = mappedFromExpr + ? mappedFromExpr.map((o) => ({ + label: String(o?.label ?? ''), + value: String(o?.value ?? ''), + })) + : toOptions(rawOptions, field, templateContext, template) + + const resolvedDefault = resolveTemplate(field.value) + const currentValue = value === '' && resolvedDefault != null ? resolvedDefault : value + + return ( + + onChange(val)} + placeholder={field.placeholder} + allowAssign={(field as any).allowCreate} + allowFreeInput={(field as any).allowFreeInput} + disabled={field.disabled} + /> + + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/AmountField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/AmountField.tsx new file mode 100644 index 000000000..0a8aa001f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/AmountField.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const AmountField: React.FC = ({ + field, + value, + error, + templateContext, + dsValue, + onChange, + resolveTemplate, + setVal, +}) => { + const currentValue = value ?? (dsValue?.amount ?? dsValue?.value ?? '') + const hasFeatures = !!(field.features?.length) + + // Get denomination from chain context + const denom = templateContext?.chain?.denom?.symbol || (field as any).denom || '' + const showDenom = !!denom + + // Calculate padding based on features and denom + // Increased padding for better spacing with the MAX button + const paddingRight = hasFeatures && showDenom ? 'pr-36' : hasFeatures ? 'pr-24' : showDenom ? 'pr-16' : '' + + const common = 'w-full bg-transparent border placeholder-text-muted text-white rounded px-3 py-2 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none' + const border = error ? 'border-red-600' : 'border-muted-foreground border-opacity-50' + + return ( + + onChange(e.currentTarget.value)} + min={(field as any).min} + max={(field as any).max} + /> + {showDenom && ( +
+ {denom} +
+ )} +
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/DynamicHtmlField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/DynamicHtmlField.tsx new file mode 100644 index 000000000..13f3e697d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/DynamicHtmlField.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const DynamicHtmlField: React.FC = ({ + field, + error, + templateContext, + resolveTemplate, +}) => { + const resolvedHtml = resolveTemplate((field as any).html) + + return ( + +
+ + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/FieldWrapper.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/FieldWrapper.tsx new file mode 100644 index 000000000..cfd448622 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/FieldWrapper.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { spanClasses } from '@/actions/utils/fieldHelpers' +import { FieldFeatures } from '@/actions/components/FieldFeatures' +import { FieldWrapperProps } from './types' + +export const FieldWrapper: React.FC = ({ + field, + error, + templateContext, + resolveTemplate, + hasFeatures, + setVal, + children, + currentValue, +}) => { + const help = error || resolveTemplate(field.help) + + return ( +
+ +
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/OptionCardField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/OptionCardField.tsx new file mode 100644 index 000000000..f1bf0f90d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/OptionCardField.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { OptionCard, OptionCardOpt } from '@/actions/OptionCard' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const OptionCardField: React.FC = ({ + field, + value, + error, + templateContext, + onChange, + resolveTemplate, +}) => { + const opts: OptionCardOpt[] = Array.isArray((field as any).options) ? (field as any).options : [] + const resolvedDefault = resolveTemplate(field.value) + const currentValue = (value === '' || value == null) && resolvedDefault != null ? resolvedDefault : value + + return ( + +
+ {opts.map((o, i) => { + const label = resolveTemplate(o.label) + const help = resolveTemplate(o.help) + const rawValue = resolveTemplate(o.value) ?? i + + // Normalize values for comparison (handle booleans, strings, numbers) + const normalizeValue = (v: any) => { + if (v === true || v === 'true') return true + if (v === false || v === 'false') return false + return v + } + + const normalizedOptionValue = normalizeValue(rawValue) + const normalizedCurrentValue = normalizeValue(currentValue) + const selected = normalizedCurrentValue === normalizedOptionValue + + return ( +
+ onChange(normalizedOptionValue)} + label={label} + help={help} + /> +
+ ) + })} +
+
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/OptionField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/OptionField.tsx new file mode 100644 index 000000000..50c312e54 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/OptionField.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { OptionField as OptionFieldType } from '@/manifest/types' +import { Option, OptionItem } from '@/actions/Option' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const OptionField: React.FC = ({ + field, + value, + error, + templateContext, + onChange, + resolveTemplate, +}) => { + const optionField = field as OptionFieldType + const isInLine = optionField.inLine + const opts: OptionItem[] = Array.isArray((field as any).options) ? (field as any).options : [] + const resolvedDefault = resolveTemplate(field.value) + const currentValue = (value === '' || value == null) && resolvedDefault != null ? resolvedDefault : value + + return ( + +
+ {opts.map((o, i) => { + const label = resolveTemplate(o.label) + const help = resolveTemplate(o.help) + const rawValue = resolveTemplate(o.value) ?? i + + // Normalize values for comparison (handle booleans, strings, numbers) + const normalizeValue = (v: any) => { + if (v === true || v === 'true') return true + if (v === false || v === 'false') return false + return String(v) + } + + const normalizedOptionValue = normalizeValue(rawValue) + const normalizedCurrentValue = normalizeValue(currentValue) + const selected = normalizedCurrentValue === normalizedOptionValue + + return ( +
+
+ ) + })} +
+
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/SelectField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/SelectField.tsx new file mode 100644 index 000000000..6ab402057 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/SelectField.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select' +import { SelectField as SelectFieldType } from '@/manifest/types' +import { template, templateAny } from '@/core/templater' +import { toOptions } from '@/actions/utils/fieldHelpers' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const SelectField: React.FC = ({ + field, + value, + error, + templateContext, + dsValue, + onChange, + resolveTemplate, +}) => { + const select = field as SelectFieldType + const staticOptions = Array.isArray(select.options) ? select.options : [] + const rawOptions = dsValue && Object.keys(dsValue).length ? dsValue : staticOptions + + let mappedFromExpr: any[] | null = null + if (typeof (select as any).map === 'string') { + try { + const out = templateAny((select as any).map, templateContext) + if (Array.isArray(out)) { + mappedFromExpr = out + } else if (typeof out === 'string') { + try { + const maybe = JSON.parse(out) + if (Array.isArray(maybe)) mappedFromExpr = maybe + } catch {} + } + } catch (err) { + console.warn('select.map expression error:', err) + } + } + + const builtOptions = mappedFromExpr + ? mappedFromExpr.map((o) => ({ + label: String(o?.label ?? ''), + value: String(o?.value ?? ''), + })) + : toOptions(rawOptions, field, templateContext, template) + + const resolvedDefault = resolveTemplate(field.value) + const currentValue = value === '' && resolvedDefault != null ? resolvedDefault : value + + return ( + + + + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/SwitchField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/SwitchField.tsx new file mode 100644 index 000000000..e183bb404 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/SwitchField.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import * as Switch from '@radix-ui/react-switch' +import { BaseFieldProps } from './types' + +export const SwitchField: React.FC = ({ + field, + value, + onChange, + resolveTemplate, +}) => { + const checked = Boolean(value ?? resolveTemplate(field.value) ?? false) + + return ( +
+
+
{resolveTemplate(field.label)}
+ onChange(next)} + className="relative h-5 w-9 rounded-full bg-neutral-700 data-[state=checked]:bg-emerald-500 outline-none shadow-inner transition-colors" + aria-label={String(resolveTemplate(field.label) ?? field.name)} + > + + +
+ {field.help && {resolveTemplate(field.help)}} +
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/TableSelectField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/TableSelectField.tsx new file mode 100644 index 000000000..994d8953e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/TableSelectField.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { template } from '@/core/templater' +import TableSelect from '@/actions/TableSelect' +import { BaseFieldProps } from './types' + +type TableSelectFieldProps = BaseFieldProps & { + errors: Record +} + +export const TableSelectField: React.FC = ({ + field, + value, + errors, + templateContext, + dsValue, + onChange, + resolveTemplate, +}) => { + // Track if we've initialized from DS + const hasInitializedRef = React.useRef(false) + + // Auto-populate from DS when it loads (for pre-selecting committees) + React.useEffect(() => { + // Only auto-populate if: + // 1. Field has a value template (e.g., "{{ ds.validator?.committees ?? [] }}") + // 2. Current value is empty + // 3. We haven't initialized yet + if ((field as any).value && !hasInitializedRef.current) { + const resolved = resolveTemplate((field as any).value) + + // Check if resolved value is non-empty + const hasResolvedValue = resolved && (Array.isArray(resolved) ? resolved.length > 0 : resolved !== '') + const hasCurrentValue = value && (Array.isArray(value) ? value.length > 0 : value !== '') + + if (hasResolvedValue && !hasCurrentValue) { + onChange(resolved) + hasInitializedRef.current = true + } + } + }, [templateContext, field, value, onChange, resolveTemplate]) + + return ( + onChange(next)} + errors={errors} + resolveTemplate={resolveTemplate} + template={template} + templateContext={templateContext} + /> + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/TextField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/TextField.tsx new file mode 100644 index 000000000..40e7ab75a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/TextField.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const TextField: React.FC = ({ + field, + value, + error, + templateContext, + dsValue, + onChange, + resolveTemplate, + setVal, +}) => { + const isTextarea = field.type === 'textarea' + const Component: any = isTextarea ? 'textarea' : 'input' + + const resolvedValue = resolveTemplate(field.value) + const currentValue = + value === '' && resolvedValue != null + ? resolvedValue + : value || (dsValue?.amount ?? dsValue?.value ?? '') + + const hasFeatures = !!(field.features?.length) + const common = 'w-full bg-transparent border placeholder-text-muted text-white rounded px-3 py-2 focus:outline-none' + const paddingRight = hasFeatures ? 'pr-24' : '' // Increased padding for better button spacing + const border = error ? 'border-red-600' : 'border-muted-foreground border-opacity-50' + + return ( + + onChange(e.currentTarget.value)} + /> + + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/fieldRegistry.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/fieldRegistry.tsx new file mode 100644 index 000000000..e2bd680db --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/fieldRegistry.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Field } from "@/manifest/types"; +import { TextField } from "./TextField"; +import { AmountField } from "./AmountField"; +import { AddressField } from "./AddressField"; +import { SelectField } from "./SelectField"; +import { AdvancedSelectField } from "./AdvancedSelectField"; +import { SwitchField } from "./SwitchField"; +import { OptionField } from "./OptionField"; +import { OptionCardField } from "./OptionCardField"; +import { TableSelectField } from "./TableSelectField"; +import { DynamicHtmlField } from "./DynamicHtmlField"; + +type FieldRenderer = React.FC<{ + field: Field; + value: any; + error?: string; + errors?: Record; + templateContext: Record; + dsValue?: any; + onChange: (value: any) => void; + resolveTemplate: (s?: any) => any; + setVal?: (fieldId: string, v: any) => void; +}>; + +export const fieldRegistry: Record = { + text: TextField, + textarea: TextField, + amount: AmountField, + address: AddressField, + select: SelectField, + advancedSelect: AdvancedSelectField, + switch: SwitchField, + option: OptionField, + optionCard: OptionCardField, + tableSelect: TableSelectField as any, + dynamicHtml: DynamicHtmlField, +}; + +export const getFieldRenderer = (fieldType: string): FieldRenderer | null => { + return fieldRegistry[fieldType] || null; +}; diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/index.ts b/cmd/rpc/web/wallet-new/src/actions/fields/index.ts new file mode 100644 index 000000000..2e639df7d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/index.ts @@ -0,0 +1,13 @@ +export { TextField } from './TextField' +export { AmountField } from './AmountField' +export { AddressField } from './AddressField' +export { SelectField } from './SelectField' +export { AdvancedSelectField } from './AdvancedSelectField' +export { SwitchField } from './SwitchField' +export { OptionField } from './OptionField' +export { OptionCardField } from './OptionCardField' +export { TableSelectField } from './TableSelectField' +export { DynamicHtmlField } from './DynamicHtmlField' +export { FieldWrapper } from './FieldWrapper' +export { fieldRegistry, getFieldRenderer } from './fieldRegistry' +export type { BaseFieldProps, FieldWrapperProps } from './types' diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/types.ts b/cmd/rpc/web/wallet-new/src/actions/fields/types.ts new file mode 100644 index 000000000..38a626ab8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/types.ts @@ -0,0 +1,24 @@ +import { Field } from '@/manifest/types' +import React from 'react' + +export type BaseFieldProps = { + field: Field + value: any + error?: string + templateContext: Record + dsValue?: any + onChange: (value: any) => void + resolveTemplate: (s?: any) => any + setVal?: (fieldId: string, v: any) => void +} + +export type FieldWrapperProps = { + field: Field + error?: string + templateContext: Record + resolveTemplate: (s?: any) => any + hasFeatures?: boolean + setVal?: (fieldId: string, v: any) => void + children: React.ReactNode + currentValue?: any +} diff --git a/cmd/rpc/web/wallet-new/src/actions/useActionDs.ts b/cmd/rpc/web/wallet-new/src/actions/useActionDs.ts new file mode 100644 index 000000000..47378533d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/useActionDs.ts @@ -0,0 +1,240 @@ +import React from "react"; +import { useDS } from "@/core/useDs"; +import { template, collectDepsFromObject } from "@/core/templater"; + +/** + * Hook to load all DS for an action/form level + * This replaces the per-field DS system with a cleaner, more performant approach + */ +export function useActionDs(actionDs: any, ctx: any, actionId: string, accountAddress?: string) { + // Extract all DS keys from action.ds + const dsKeys = React.useMemo(() => { + if (!actionDs || typeof actionDs !== "object") return []; + return Object.keys(actionDs).filter(k => k !== "__options"); + }, [actionDs]); + + // Global options for all DS in this action + const globalOptions = React.useMemo(() => { + return actionDs?.__options || {}; + }, [actionDs]); + + // Auto-detect watch paths from all DS params + const autoWatchPaths = React.useMemo(() => { + const deps = new Set(); + + for (const key of dsKeys) { + const dsParams = actionDs[key]; + const extracted = collectDepsFromObject(dsParams); + extracted.forEach(d => { + // Only watch form.* paths for reactivity + if (d.startsWith('form.')) { + deps.add(d); + } + }); + } + + return Array.from(deps); + }, [actionDs, dsKeys]); + + // Manual watch paths from __options.watch + const manualWatchPaths = React.useMemo(() => { + const watch = globalOptions.watch; + return Array.isArray(watch) ? watch : []; + }, [globalOptions]); + + // Combined watch paths + const watchPaths = React.useMemo(() => { + return Array.from(new Set([...autoWatchPaths, ...manualWatchPaths])); + }, [autoWatchPaths, manualWatchPaths]); + + // Create watch snapshot for change detection + const watchSnapshot = React.useMemo(() => { + const snapshot: Record = {}; + for (const path of watchPaths) { + const keys = path.split('.'); + let value = ctx; + for (const key of keys) { + value = value?.[key]; + } + snapshot[path] = value; + } + return snapshot; + }, [watchPaths, ctx]); + + // Serialize watch snapshot for dependency tracking + const watchKey = React.useMemo(() => { + try { + return JSON.stringify(watchSnapshot); + } catch { + return ''; + } + }, [watchSnapshot]); + + // Helper to check if a value is empty/invalid for DS params + const isEmptyValue = (val: any): boolean => { + if (val === null || val === undefined) return true; + if (typeof val === 'string' && val.trim() === '') return true; + if (typeof val === 'object' && Object.keys(val).length === 0) return true; + return false; + }; + + // Helper to check if DS params have all required values + const hasRequiredValues = (params: Record): boolean => { + // Empty object {} means no params required, which is valid (e.g., keystore DS) + if (typeof params === 'object' && !Array.isArray(params)) { + const keys = Object.keys(params); + if (keys.length === 0) return true; // {} is valid + } + + // Check all nested values for empty strings, null, or undefined + const checkDeep = (obj: any): boolean => { + if (obj == null) return false; + if (typeof obj === 'string') return obj.trim() !== ''; + if (Array.isArray(obj)) return obj.length > 0; + if (typeof obj === 'object') { + // For objects, check if at least one value is non-empty + const values = Object.values(obj); + if (values.length === 0) return false; + return values.some(v => checkDeep(v)); + } + return true; + }; + + return checkDeep(params); + }; + + // Pre-calculate all DS configurations (no hooks here) + const dsConfigs = React.useMemo(() => { + const deepResolve = (obj: any): any => { + if (obj == null) return obj; + if (typeof obj === "string") { + return template(obj, ctx); + } + if (Array.isArray(obj)) { + return obj.map(deepResolve); + } + if (typeof obj === "object") { + const result: Record = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === "__options") continue; + result[k] = deepResolve(v); + } + return result; + } + return obj; + }; + + return dsKeys.map(dsKey => { + const dsParams = actionDs[dsKey]; + const dsLocalOptions = dsParams?.__options || {}; + + // Resolve templates in DS params + let renderedParams = {}; + try { + renderedParams = deepResolve(dsParams); + } catch (err) { + console.warn(`Error resolving DS params for ${dsKey}:`, err); + } + + // Check if DS is enabled (manual override from manifest) + const enabledValue = dsLocalOptions.enabled ?? globalOptions.enabled ?? true; + let isManuallyEnabled = true; + if (typeof enabledValue === 'string') { + try { + const resolved = template(enabledValue, ctx); + isManuallyEnabled = !!resolved && resolved !== 'false'; + } catch { + isManuallyEnabled = false; + } + } else { + isManuallyEnabled = !!enabledValue; + } + + // Auto-detect if DS params have all required values + // This prevents requests with empty/undefined params + const hasValues = hasRequiredValues(renderedParams); + + // DS is only enabled if both manual check passes AND params have values + const isEnabled = isManuallyEnabled && hasValues; + + // Build DS options + // Create a unique scope that includes action + DS key to avoid cache collisions + // Don't include accountAddress here because it's the selected account, not the DS param + // The ctxKey (JSON.stringify of params) in useDS already handles param-level uniqueness + const uniqueScope = `action:${actionId}:ds:${dsKey}`; + + const dsOptions = { + enabled: isEnabled, + scope: uniqueScope, + staleTimeMs: dsLocalOptions.staleTimeMs ?? globalOptions.staleTimeMs ?? 5000, + gcTimeMs: dsLocalOptions.gcTimeMs ?? globalOptions.gcTimeMs ?? 300000, + refetchIntervalMs: dsLocalOptions.refetchIntervalMs ?? globalOptions.refetchIntervalMs, + refetchOnWindowFocus: dsLocalOptions.refetchOnWindowFocus ?? globalOptions.refetchOnWindowFocus ?? false, + refetchOnMount: dsLocalOptions.refetchOnMount ?? globalOptions.refetchOnMount ?? true, + refetchOnReconnect: dsLocalOptions.refetchOnReconnect ?? globalOptions.refetchOnReconnect ?? false, + retry: dsLocalOptions.retry ?? globalOptions.retry ?? 1, + retryDelay: dsLocalOptions.retryDelay ?? globalOptions.retryDelay, + }; + + return { dsKey, renderedParams, dsOptions }; + }); + }, [dsKeys, actionDs, ctx, watchKey, globalOptions, actionId, accountAddress]); + + // Call useDS hooks with fixed number of slots (max 10 DS per action) + const ds0 = useDS(dsConfigs[0]?.dsKey ?? "__disabled__", dsConfigs[0]?.renderedParams ?? {}, dsConfigs[0]?.dsOptions ?? { enabled: false }); + const ds1 = useDS(dsConfigs[1]?.dsKey ?? "__disabled__", dsConfigs[1]?.renderedParams ?? {}, dsConfigs[1]?.dsOptions ?? { enabled: false }); + const ds2 = useDS(dsConfigs[2]?.dsKey ?? "__disabled__", dsConfigs[2]?.renderedParams ?? {}, dsConfigs[2]?.dsOptions ?? { enabled: false }); + const ds3 = useDS(dsConfigs[3]?.dsKey ?? "__disabled__", dsConfigs[3]?.renderedParams ?? {}, dsConfigs[3]?.dsOptions ?? { enabled: false }); + const ds4 = useDS(dsConfigs[4]?.dsKey ?? "__disabled__", dsConfigs[4]?.renderedParams ?? {}, dsConfigs[4]?.dsOptions ?? { enabled: false }); + const ds5 = useDS(dsConfigs[5]?.dsKey ?? "__disabled__", dsConfigs[5]?.renderedParams ?? {}, dsConfigs[5]?.dsOptions ?? { enabled: false }); + const ds6 = useDS(dsConfigs[6]?.dsKey ?? "__disabled__", dsConfigs[6]?.renderedParams ?? {}, dsConfigs[6]?.dsOptions ?? { enabled: false }); + const ds7 = useDS(dsConfigs[7]?.dsKey ?? "__disabled__", dsConfigs[7]?.renderedParams ?? {}, dsConfigs[7]?.dsOptions ?? { enabled: false }); + const ds8 = useDS(dsConfigs[8]?.dsKey ?? "__disabled__", dsConfigs[8]?.renderedParams ?? {}, dsConfigs[8]?.dsOptions ?? { enabled: false }); + const ds9 = useDS(dsConfigs[9]?.dsKey ?? "__disabled__", dsConfigs[9]?.renderedParams ?? {}, dsConfigs[9]?.dsOptions ?? { enabled: false }); + + // Collect all DS results + const allDsResults = [ds0, ds1, ds2, ds3, ds4, ds5, ds6, ds7, ds8, ds9]; + const dsResults = React.useMemo(() => { + return dsConfigs.map((config, idx) => ({ + dsKey: config.dsKey, + ...allDsResults[idx] + })); + }, [dsConfigs, ...allDsResults.map(d => d.data)]); + + // Merge all DS data into a single object + const allDsData = React.useMemo(() => { + const merged: Record = {}; + for (const { dsKey, data } of dsResults) { + if (data !== undefined && data !== null) { + merged[dsKey] = data; + } + } + return merged; + }, [dsResults]); + + // Refetch all when watch values change + const prevWatchKeyRef = React.useRef(watchKey); + React.useEffect(() => { + if (prevWatchKeyRef.current !== watchKey && prevWatchKeyRef.current !== '') { + // Watch values changed, refetch all enabled DS + for (const result of dsResults) { + if (result.refetch) { + result.refetch(); + } + } + } + prevWatchKeyRef.current = watchKey; + }, [watchKey, dsResults]); + + const isLoading = dsResults.some(r => r.isLoading); + const hasError = dsResults.some(r => r.error); + + return { + ds: allDsData, + isLoading, + hasError, + refetchAll: () => { + dsResults.forEach(r => r.refetch?.()); + } + }; +} diff --git a/cmd/rpc/web/wallet-new/src/actions/useFieldsDs.ts b/cmd/rpc/web/wallet-new/src/actions/useFieldsDs.ts new file mode 100644 index 000000000..308163798 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/useFieldsDs.ts @@ -0,0 +1,156 @@ +import React from "react"; +import { Field } from "@/manifest/types"; +import { useDS, type DSOptions } from "@/core/useDs"; +import { template } from "@/core/templater"; + +export function useFieldDs(field: Field, ctx: any) { + const fieldName = (field as any)?.name || (field as any)?.id || 'unknown'; + + const dsConfig = React.useMemo(() => { + const dsObj = (field as any)?.ds; + if (!dsObj || typeof dsObj !== "object") return null; + + // Filter out __options to get only DS keys + const keys = Object.keys(dsObj).filter(k => k !== "__options"); + if (keys.length === 0) return null; + + // Get the first DS key (e.g., "account", "keystore") + const dsKey = keys[0]; + const dsParams = dsObj[dsKey]; + const options = dsObj.__options || {}; + + return { dsKey, dsParams, options }; + }, [field]); + + const enabled = !!dsConfig; + + // Extract watch paths for reactivity + const watchPaths = React.useMemo(() => { + if (!dsConfig?.options?.watch) return []; + const watch = dsConfig.options.watch; + return Array.isArray(watch) ? watch : []; + }, [dsConfig]); + + // Build watched values snapshot for reactivity + const watchSnapshot = React.useMemo(() => { + const snapshot: Record = {}; + for (const path of watchPaths) { + const keys = path.split('.'); + let value = ctx; + for (const key of keys) { + value = value?.[key]; + } + snapshot[path] = value; + } + return snapshot; + }, [watchPaths, ctx]); + + // Serialize watch snapshot for triggering refetch + const watchKey = React.useMemo(() => { + try { + return JSON.stringify(watchSnapshot); + } catch { + return ''; + } + }, [watchSnapshot]); + + // Resolve templates in DS params using the proper templater + const renderedParams = React.useMemo(() => { + if (!enabled || !dsConfig) return {}; + + try { + // Deep resolve all templates in the params object + const deepResolve = (obj: any): any => { + if (obj == null) return obj; + if (typeof obj === "string") { + return template(obj, ctx); + } + if (Array.isArray(obj)) { + return obj.map(deepResolve); + } + if (typeof obj === "object") { + const result: Record = {}; + for (const [k, v] of Object.entries(obj)) { + // Skip __options key + if (k === "__options") continue; + result[k] = deepResolve(v); + } + return result; + } + return obj; + }; + + return deepResolve(dsConfig.dsParams); + } catch (err) { + console.warn("Error resolving DS params:", err); + return {}; + } + }, [dsConfig, ctx, enabled]); + + // Build DS options from __options in manifest + const dsOptions = React.useMemo((): DSOptions => { + if (!dsConfig?.options) return { enabled }; + + const opts = dsConfig.options; + + // Check if DS should be enabled based on template condition + let isEnabled = enabled; + if (opts.enabled !== undefined) { + if (typeof opts.enabled === 'string') { + // Template-based enabled (e.g., "{{ form.operator }}") + try { + const resolved = template(opts.enabled, ctx); + isEnabled = enabled && !!resolved && resolved !== 'false'; + } catch { + isEnabled = false; + } + } else { + // Boolean value + isEnabled = enabled && !!opts.enabled; + } + } + + // Scope by action/form only (not by field) for better cache sharing + // The ctxKey in useDs already handles param differentiation + const actionScope = ctx?.__scope ?? 'global'; + + return { + enabled: isEnabled, + // Use action-level scope so fields in the same form share cache + scope: actionScope, + // Caching options - use shorter staleTime when watching values for better reactivity + staleTimeMs: watchPaths.length > 0 ? 0 : (opts.staleTimeMs ?? 5000), + gcTimeMs: opts.gcTimeMs, + refetchIntervalMs: opts.refetchIntervalMs, + refetchOnWindowFocus: opts.refetchOnWindowFocus ?? false, + refetchOnMount: opts.refetchOnMount ?? true, + refetchOnReconnect: opts.refetchOnReconnect ?? false, + // Error handling + retry: opts.retry ?? 1, + retryDelay: opts.retryDelay, + }; + }, [dsConfig, enabled, ctx?.__scope, watchPaths.length]); + + const { data, isLoading, error, refetch } = useDS( + dsConfig?.dsKey ?? "__disabled__", + renderedParams, + dsOptions + ); + + // Force refetch when watch values change + const prevWatchKeyRef = React.useRef(watchKey); + React.useEffect(() => { + if (enabled && prevWatchKeyRef.current !== watchKey && prevWatchKeyRef.current !== '') { + // watchKey changed, force refetch + refetch(); + } + prevWatchKeyRef.current = watchKey; + }, [watchKey, enabled, refetch]); + + return { + data: enabled ? data : null, + isLoading: enabled ? isLoading : false, + error: enabled ? error : null, + refetch, + }; +} diff --git a/cmd/rpc/web/wallet-new/src/actions/utils/fieldHelpers.ts b/cmd/rpc/web/wallet-new/src/actions/utils/fieldHelpers.ts new file mode 100644 index 000000000..0801a8edf --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/utils/fieldHelpers.ts @@ -0,0 +1,103 @@ +import { template } from '@/core/templater' + +export const getByPath = (obj: any, selector?: string) => { + if (!selector || !obj) return obj + return selector.split('.').reduce((acc, k) => acc?.[k], obj) +} + +export const toOptions = ( + raw: any, + f?: any, + templateContext?: Record, + resolveTemplate?: (s: any, ctx?: any) => any +): Array<{ label: string; value: string }> => { + if (!raw) return [] + const map = f?.map ?? {} + + // Use the main templating system + const evalDynamic = (expr: string, item?: any) => { + if (!expr || typeof expr !== 'string') return expr + const localCtx = { ...templateContext, row: item, item } + // Use the template function which handles all cases + return template(expr, localCtx) + } + + const makeLabel = (item: any) => { + if (map.label) return evalDynamic(map.label, item) + return ( + item.label ?? + item.name ?? + item.id ?? + item.value ?? + item.address ?? + JSON.stringify(item) + ) + } + + const makeValue = (item: any) => { + if (map.value) return evalDynamic(map.value, item) + return String(item.value ?? item.id ?? item.address ?? item.key ?? item) + } + + if (Array.isArray(raw)) { + return raw.map((item) => ({ + label: String(makeLabel(item) ?? ''), + value: String(makeValue(item) ?? ''), + })) + } + + if (typeof raw === 'object') { + return Object.entries(raw).map(([k, v]) => ({ + label: String(makeLabel(v) ?? k), + value: String(makeValue(v) ?? k), + })) + } + + return [] +} + +const SPAN_MAP = { + 1: 'col-span-1', + 2: 'col-span-2', + 3: 'col-span-3', + 4: 'col-span-4', + 5: 'col-span-5', + 6: 'col-span-6', + 7: 'col-span-7', + 8: 'col-span-8', + 9: 'col-span-9', + 10: 'col-span-10', + 11: 'col-span-11', + 12: 'col-span-12', +} + +const RSP = (n?: number) => { + const c = Math.max(1, Math.min(12, Number(n || 12))) + return SPAN_MAP[c as keyof typeof SPAN_MAP] || 'col-span-12' +} + +export const spanClasses = (f: any, layout?: any) => { + const conf = f?.span ?? f?.ui?.grid?.colSpan ?? layout?.grid?.defaultSpan + const base = typeof conf === 'number' ? { base: conf } : (conf || {}) + + // Mobile-first approach: full width on small screens + const mobileBase = 'col-span-12' + + // Desktop span: use 'base' config or default to full width + const baseSpan = base.base != null ? RSP(base.base) : 'col-span-12' + + // Build responsive classes + // sm: small tablets (640px+) + const sm = base.sm != null ? `sm:${RSP(base.sm)}` : '' + + // md: tablets and up (768px+) - use baseSpan if not explicitly set + const md = base.md != null ? `md:${RSP(base.md)}` : (base.base != null ? `md:${baseSpan}` : '') + + // lg: large screens (1024px+) + const lg = base.lg != null ? `lg:${RSP(base.lg)}` : '' + + // xl: extra large screens (1280px+) + const xl = base.xl != null ? `xl:${RSP(base.xl)}` : '' + + return [mobileBase, sm, md, lg, xl].filter(Boolean).join(' ') +} diff --git a/cmd/rpc/web/wallet-new/src/actions/validators.ts b/cmd/rpc/web/wallet-new/src/actions/validators.ts new file mode 100644 index 000000000..6eeb0ab84 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/validators.ts @@ -0,0 +1,237 @@ +// validators.ts +import type { Field, AmountField } from "@/manifest/types"; +import {template} from "@/core/templater"; + +type RuleCode = + | "required" + | "min" + | "max" + | "length.min" + | "length.max" + | "minSelected" + | "maxSelected" + | "pattern"; + +export type ValidationResult = + | { ok: true; [key: string]: any } + | { ok: true; errors: { [key: string]: string[] } } + | { ok: false; code: RuleCode; message: string }; + +const DEFAULT_MESSAGES: Record = { + required: "This field is required.", + min: "Minimum allowed is {{min}}.", + max: "Maximum allowed is {{max}}.", + minSelected: "Minimum selected is {{min}}.", + maxSelected: "Maximum selected is {{max}}.", + "length.min": "Minimum length is {{length.min}} characters.", + "length.max": "Maximum length is {{length.max}} characters.", + pattern: "Invalid format.", +}; + +const isEmpty = (s: any) => + s == null || (typeof s === "string" && s.trim() === ""); + +const get = (o: any, path?: string) => + !path ? o : path.split(".").reduce((a, k) => a?.[k], o); + +const resolveMsg = ( + overrides: Record | undefined, + code: RuleCode, + params: Record +) => { + const raw = overrides?.[code] ?? DEFAULT_MESSAGES[code]; + return template(raw, params); +}; + +function evalNumeric(v: any, ctx: Record): number | undefined { + if (v == null) return undefined; + if (typeof v === "number") return Number.isFinite(v) ? v : undefined; + if (typeof v === "string") { + const raw = v.includes("{{") ? template(v, ctx) : v; + + const match = String(raw) + .replace(/\u00A0/g, " ") // NBSP + .match(/[-+]?(?:\d{1,3}(?:[ ,]\d{3})+|\d+)(?:[.,]\d+)?/); + + if (!match) return undefined; + + let num = match[0].trim(); + + if (num.includes(",") && num.includes(".")) { + const lastComma = num.lastIndexOf(","); + const lastDot = num.lastIndexOf("."); + if (lastComma > lastDot) { + num = num.replace(/\./g, "").replace(",", "."); + } else { + num = num.replace(/,/g, ""); + } + } else if (num.includes(",")) { + num = num.replace(",", "."); + } else { + num = num.replace(/\s+/g, ""); + } + + const n = Number(num); + return Number.isFinite(n) ? n : undefined; + } + return undefined; +} + +export async function validateField( + field: Field, + value: any, + ctx: Record = {} +): Promise { + if (field.type === "switch") return { ok: true }; + + // OPTIONCARD + if (field.type === "optionCard") { + if (field.required && (value === undefined || value === null || value === "")) { + return { + ok: false, + code: "required", + message: resolveMsg( + (field as any).validation?.messages, + "required", + { field, value, ...ctx } + ), + }; + } + return { ok: true }; + } + + // TABLESELECT + if (field.type === "tableSelect") { + const arr = Array.isArray(value) ? value : value ? [value] : []; + if (field.required && arr.length === 0) { + return { + ok: false, + code: "required", + message: resolveMsg( + (field as any).validation?.messages, + "required", + { field, value, ...ctx } + ), + }; + } + + const vconf = (field as any).validation ?? {}; + const min = evalNumeric(vconf.min, ctx); + const max = evalNumeric(vconf.max, ctx); + + + if (typeof min === "number" && arr.length < min) { + return { + ok: false, + code: "minSelected", + message: resolveMsg(vconf.messages, "minSelected", { min, field, value, ...ctx }), + }; + } + if (typeof max === "number" && arr.length > max) { + return { + ok: false, + code: "maxSelected", + message: resolveMsg(vconf.messages, "maxSelected", { max, field, value, ...ctx }), + }; + } + return { ok: true }; + } + + // ——— base shared validation ——— + const templatedValue = typeof value === "string" ? template(value, ctx) : value; + const formattedValue = isEmpty(templatedValue) ? value : templatedValue; + const vconf = (field as any).validation ?? {}; + const messages: Record | undefined = vconf.messages; + const asString = value == null ? "" : String(value); + + // REQUIRED + if (field.required && (formattedValue == null || formattedValue === "")) { + return { + ok: false, + code: "required", + message: resolveMsg(messages, "required", { field, value, ...ctx }), + }; + } + + // AMOUNT + if (field.type === "amount") { + const f = field as AmountField; + + const n = typeof formattedValue === "string" + ? Number(formattedValue.trim().replace(/,/g, "")) + : Number(formattedValue); + + const safeValue = Number.isNaN(n) ? 0 : n; + + const min = evalNumeric(f.min ?? vconf.min, ctx); + const max = evalNumeric(f.max ?? vconf.max, ctx); + + if (typeof min === "number" && safeValue < min) { + return { + ok: false, + code: "min", + message: resolveMsg(messages, "min", { min, field, value: safeValue, ...ctx }), + }; + } + + if (typeof max === "number" && safeValue > max) { + return { + ok: false, + code: "max", + message: resolveMsg(messages, "max", { max, field, value: safeValue, ...ctx }), + }; + } + } + + // LENGTH (ahora soporta min/max templated) + if (vconf.length && typeof asString === "string") { + const lmin = evalNumeric(get(vconf, "length.min"), ctx); + const lmax = evalNumeric(get(vconf, "length.max"), ctx); + if (typeof lmin === "number" && asString.length < lmin) { + return { + ok: false, + code: "length.min", + message: resolveMsg(messages, "length.min", { + length: { min: lmin, max: lmax }, + field, + value: formattedValue, + ...ctx, + }), + }; + } + if (typeof lmax === "number" && asString.length > lmax) { + return { + ok: false, + code: "length.max", + message: resolveMsg(messages, "length.max", { + length: { min: lmin, max: lmax }, + field, + value: formattedValue, + ...ctx, + }), + }; + } + } + + // PATTERN + if (vconf.pattern) { + const pattern = template(vconf.pattern, ctx); + + const rx = + new RegExp(pattern) + + if (!rx.test(asString)) { + return { + ok: false, + code: "pattern", + message: resolveMsg(messages, "pattern", { + field, + value: formattedValue, + ...ctx, + }), + }; + } + } + + return { ok: true }; +} diff --git a/cmd/rpc/web/wallet-new/src/app/App.tsx b/cmd/rpc/web/wallet-new/src/app/App.tsx new file mode 100644 index 000000000..e09c10b77 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/App.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import {RouterProvider} from 'react-router-dom' +import {ConfigProvider} from './providers/ConfigProvider' +import router from "./routes"; +import {AccountsProvider} from "@/app/providers/AccountsProvider"; +import {ToastProvider} from "@/toast/ToastContext"; +import {ActionModalProvider} from "@/app/providers/ActionModalProvider"; +import {Theme} from "@radix-ui/themes"; + +export default function App() { + return ( + + + + + + + + + + + + ) +} diff --git a/cmd/rpc/web/wallet-new/src/app/pages/Accounts.tsx b/cmd/rpc/web/wallet-new/src/app/pages/Accounts.tsx new file mode 100644 index 000000000..a9a0ff4a8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Accounts.tsx @@ -0,0 +1,636 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { + ArrowLeftRight, + Box, + CheckCircle, + ChevronDown, + Circle, + Layers, + Lock, + Search, + Send, + Shield, + Wallet, +} from "lucide-react"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Filler, +} from "chart.js"; +import { Line } from "react-chartjs-2"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useBalanceHistory } from "@/hooks/useBalanceHistory"; +import { useStakedBalanceHistory } from "@/hooks/useStakedBalanceHistory"; +import { useBalanceChart } from "@/hooks/useBalanceChart"; +import { useActionModal } from "@/app/providers/ActionModalProvider"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Filler, +); + +export const Accounts = () => { + const { + accounts, + loading: accountsLoading, + selectedAccount, + switchAccount, + } = useAccounts(); + const { + totalBalance, + totalStaked, + balances, + stakingData, + loading: dataLoading, + } = useAccountData(); + const { data: balanceHistory, isLoading: balanceHistoryLoading } = + useBalanceHistory(); + const { data: stakedHistory, isLoading: stakedHistoryLoading } = + useStakedBalanceHistory(); + const { data: balanceChartData = [], isLoading: balanceChartLoading } = + useBalanceChart({ points: 6, type: "balance" }); + const { data: stakedChartData = [], isLoading: stakedChartLoading } = + useBalanceChart({ points: 6, type: "staked" }); + const { openAction } = useActionModal(); + + const [searchTerm, setSearchTerm] = useState(""); + const [selectedNetwork, setSelectedNetwork] = useState("All Networks"); + + const formatAddress = (address: string) => { + return ( + address.substring(0, 5) + "..." + address.substring(address.length - 6) + ); + }; + + const formatBalance = (amount: number) => { + return (amount / 1000000).toFixed(2); + }; + + const getAccountIcon = (index: number) => { + const icons = [ + { icon: Wallet, bg: "bg-gradient-to-r from-primary/80 to-primary/40" }, + { icon: Layers, bg: "bg-gradient-to-r from-blue-500/80 to-blue-500/40" }, + { + icon: ArrowLeftRight, + bg: "bg-gradient-to-r from-purple-500/80 to-purple-500/40", + }, + { + icon: Shield, + bg: "bg-gradient-to-r from-green-500/80 to-green-500/40", + }, + { icon: Box, bg: "bg-gradient-to-r from-red-500/80 to-red-500/40" }, + ]; + return icons[index % icons.length]; + }; + + const getAccountStatus = (address: string) => { + const stakingInfo = stakingData.find((data) => data.address === address); + if (stakingInfo && stakingInfo.staked > 0) { + return { + status: "Staked", + color: "bg-primary/20 text-primary", + }; + } + return { + status: "Liquid", + color: "bg-gray-500/20 text-gray-400", + }; + }; + + const getStatusColor = (status: string) => { + const stakedText = "Staked"; + const unstakingText = "Unstaking"; + const liquidText = "Liquid"; + const delegatedText = "Delegated"; + + switch (status) { + case stakedText: + return "bg-primary/20 text-primary"; + case unstakingText: + return "bg-orange-500/20 text-orange-400"; + case liquidText: + return "bg-gray-500/20 text-gray-400"; + case delegatedText: + return "bg-primary/20 text-primary"; + default: + return "bg-gray-500/20 text-gray-400"; + } + }; + + const getRealTotal = (address: string) => { + const balanceInfo = balances.find((b) => b.address === address); + const stakingInfo = stakingData.find((s) => s.address === address); + + const liquid = balanceInfo?.amount || 0; + const staked = stakingInfo?.staked || 0; + + return { liquid, staked, total: liquid + staked }; + }; + + const getStakedPercentage = (address: string) => { + const { staked, total } = getRealTotal(address); + + if (total === 0) return 0; + return (staked / total) * 100; + }; + + const getLiquidPercentage = (address: string) => { + const { liquid, total } = getRealTotal(address); + + if (total === 0) return 0; + return (liquid / total) * 100; + }; + + const getLiquidAmount = (address: string) => { + const { liquid } = getRealTotal(address); + return liquid; + }; + + // Get real 24h changes from unified history hooks + const balanceChangePercentage = balanceHistory?.changePercentage || 0; + const stakedChangePercentage = stakedHistory?.changePercentage || 0; + + // Prepare chart data from useBalanceChart hook + const balanceChart = { + labels: balanceChartData.map((d) => d.label), + datasets: [ + { + data: balanceChartData.map((d) => d.value / 1000000), + borderColor: "#6fe3b4", + backgroundColor: "rgba(111, 227, 180, 0.1)", + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4, + }, + ], + }; + + const stakedChart = { + labels: stakedChartData.map((d) => d.label), + datasets: [ + { + data: stakedChartData.map((d) => d.value / 1000000), + borderColor: "#6fe3b4", + backgroundColor: "rgba(111, 227, 180, 0.1)", + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4, + }, + ], + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + }, + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + }, + }, + elements: { + point: { + radius: 0, + }, + }, + }; + + const handleSendAction = (address: string) => { + // Set the account as selected before opening the action + const account = accounts.find((a) => a.address === address); + if (account && selectedAccount !== account) { + switchAccount(account.id); + } + // Open send action modal with prefilled output address + openAction("send", { + prefilledData: { + output: address, + }, + onFinish: () => { + console.log("Send action completed"); + }, + }); + }; + + const processedAddresses = accounts.map((account, index) => { + const balanceInfo = balances.find((b) => b.address === account.address); + const balance = balanceInfo?.amount || 0; + const formattedBalance = formatBalance(balance); + const stakingInfo = stakingData.find( + (data) => data.address === account.address, + ); + const staked = stakingInfo?.staked || 0; + const stakedFormatted = formatBalance(staked); + const liquidAmount = getLiquidAmount(account.address); + const liquidFormatted = formatBalance(liquidAmount); + const stakedPercentage = getStakedPercentage(account.address); + const liquidPercentage = getLiquidPercentage(account.address); + const statusInfo = getAccountStatus(account.address); + const accountIcon = getAccountIcon(index); + + return { + id: account.address, + address: formatAddress(account.address), + fullAddress: account.address, + nickname: account.nickname || formatAddress(account.address), + balance: formattedBalance, + staked: stakedFormatted, + liquid: liquidFormatted, + stakedPercentage: stakedPercentage, + liquidPercentage: liquidPercentage, + status: statusInfo.status, + statusColor: getStatusColor(statusInfo.status), + icon: accountIcon.icon, + iconBg: accountIcon.bg, + }; + }); + + const filteredAddresses = processedAddresses.filter( + (addr) => + addr.address.toLowerCase().includes(searchTerm.toLowerCase()) || + addr.nickname.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + const activeAddressesCount = processedAddresses.filter( + (addr) => addr.status === "Staked" || addr.status === "Delegated", + ).length; + + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.6, + staggerChildren: 0.1, + }, + }, + }; + + const cardVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, + }; + + if (accountsLoading || dataLoading) { + return ( +
+
{"Loading accounts..."}
+
+ ); + } + + return ( + +
+ {/* Header Section */} + +
+
+

+ All Addresses +

+

+ Manage and monitor all your blockchain addresses across + different networks +

+
+ + {/* Search and Filter Bar */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full bg-bg-secondary lg:w-96 border border-bg-accent rounded-lg pl-10 pr-4 py-2 text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50" + /> +
+
+ + +
+
+
+
+ + {/* Summary Cards */} + + {/* Total Balance Card */} +
+
+

+ Total Balance +

+ +
+
+ +  CNPY +
+
+ {balanceHistoryLoading ? ( + Loading... + ) : balanceHistory ? ( + = 0 ? "text-primary" : "text-status-error"}`} + > + + + + {balanceChangePercentage >= 0 ? "+" : ""} + {balanceChangePercentage.toFixed(2)}% + 24h change + + ) : ( + No data + )} + {!balanceChartLoading && balanceChartData.length > 0 && ( +
+ +
+ )} +
+
+ + {/* Total Staked Card */} +
+
+

+ Total Staked +

+ +
+
+ +  CNPY +
+
+ {stakedHistoryLoading ? ( + Loading... + ) : stakedHistory ? ( + = 0 ? "text-primary" : "text-status-error"}`} + > + + + + {stakedChangePercentage >= 0 ? "+" : ""} + {stakedChangePercentage.toFixed(2)}% + 24h change + + ) : ( + No data + )} + {!stakedChartLoading && stakedChartData.length > 0 && ( +
+ +
+ )} +
+
+ + {/* Active Addresses Card */} +
+
+

+ Active Addresses +

+ +
+ +
+ {activeAddressesCount} of {accounts.length} +
+
+ + + All Validators Synced + +
+
+
+ + {/* Address Portfolio Section */} + +
+
+

+ Address Portfolio +

+
+
+ Live +
+
+
+
+ + {/* Table */} +
+ + + + + + + + + + + + + {filteredAddresses.map((address, index) => { + return ( + + + + + + + + + ); + })} + +
+ Address + + Total Balance + + Staked + + Liquid + + Status + + Actions +
+
+
+ +
+
+
+ {address.nickname} +
+
+ {address.address} +
+
+
+
+
+
+ {Number(address.balance).toLocaleString()} CNPY +
+
+
+
+
+ {Number(address.staked).toLocaleString()} CNPY +
+
+ {address.stakedPercentage.toFixed(2)}% +
+
+
+
+
+ {Number(address.liquid).toLocaleString()} CNPY +
+
+ {address.liquidPercentage.toFixed(2)}% +
+
+
+ + {address.status} + + +
+ {/* handleViewDetails(address.fullAddress)}*/} + {/* title="View Details"*/} + {/*>*/} + {/* */} + {/**/} + + {/* handleMoreActions(address.fullAddress)}*/} + {/* title="More Actions"*/} + {/*>*/} + {/* */} + {/**/} +
+
+
+
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/app/pages/AllAddresses.tsx b/cmd/rpc/web/wallet-new/src/app/pages/AllAddresses.tsx new file mode 100644 index 000000000..e4d1bfb78 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/AllAddresses.tsx @@ -0,0 +1,288 @@ +import React, { useState, useMemo } from "react"; +import { motion } from "framer-motion"; +import { Search, Wallet, Copy } from "lucide-react"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { useAccounts } from "@/app/providers/AccountsProvider"; + +export const AllAddresses = () => { + const { accounts, loading: accountsLoading } = useAccounts(); + const { balances, stakingData } = useAccountData(); + const { copyToClipboard } = useCopyToClipboard(); + + const [searchTerm, setSearchTerm] = useState(""); + const [filterStatus, setFilterStatus] = useState("all"); + + const formatAddress = (address: string) => { + return ( + address.substring(0, 12) + "..." + address.substring(address.length - 12) + ); + }; + + const formatBalance = (amount: number) => { + return (amount / 1000000).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + }; + + const getAccountStatus = (address: string) => { + const stakingInfo = stakingData.find((data) => data.address === address); + if (stakingInfo && stakingInfo.staked > 0) { + return "Staked"; + } + return "Liquid"; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "Staked": + return "bg-primary/20 text-primary border border-primary/40"; + case "Unstaking": + return "bg-orange-500/20 text-orange-400 border border-orange-500/40"; + case "Liquid": + return "bg-gray-500/20 text-gray-400 border border-gray-500/40"; + default: + return "bg-gray-500/20 text-gray-400 border border-gray-500/40"; + } + }; + + const processedAddresses = useMemo(() => { + return accounts.map((account) => { + const balanceInfo = balances.find((b) => b.address === account.address); + const balance = balanceInfo?.amount || 0; + const stakingInfo = stakingData.find( + (data) => data.address === account.address, + ); + const staked = stakingInfo?.staked || 0; + const total = balance + staked; + + return { + id: account.address, + address: account.address, + nickname: account.nickname || "Unnamed", + balance: balance, + staked: staked, + total: total, + status: getAccountStatus(account.address), + }; + }); + }, [accounts, balances, stakingData]); + + // Filter addresses + const filteredAddresses = useMemo(() => { + return processedAddresses.filter((addr) => { + const matchesSearch = + searchTerm === "" || + addr.address.toLowerCase().includes(searchTerm.toLowerCase()) || + addr.nickname.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = + filterStatus === "all" || addr.status === filterStatus; + + return matchesSearch && matchesStatus; + }); + }, [processedAddresses, searchTerm, filterStatus]); + + // Calculate totals + const totalBalance = useMemo(() => { + return filteredAddresses.reduce((sum, addr) => sum + addr.balance, 0); + }, [filteredAddresses]); + + const totalStaked = useMemo(() => { + return filteredAddresses.reduce((sum, addr) => sum + addr.staked, 0); + }, [filteredAddresses]); + + if (accountsLoading) { + return ( +
+
Loading addresses...
+
+ ); + } + + return ( + +
+ {/* Header */} +
+

+ All Addresses +

+

+ Manage all your wallet addresses and their balances +

+
+ + {/* Filters */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-bg-primary border border-bg-accent rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:border-primary/40 transition-colors" + /> +
+ + {/* Status Filter */} +
+ +
+
+
+ + {/* Stats */} +
+
+
Total Addresses
+
+ {accounts.length} +
+
+
+
Total Balance
+
+ {formatBalance(totalBalance)} CNPY +
+
+
+
Total Staked
+
+ {formatBalance(totalStaked)} CNPY +
+
+
+
Filtered Results
+
+ {filteredAddresses.length} +
+
+
+ + {/* Addresses Table */} +
+
+ + + + + + + + + + + + + {filteredAddresses.length > 0 ? ( + filteredAddresses.map((addr, i) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+ Address + + Nickname + + Liquid Balance + + Staked + + Total + + Status +
+
+
+ +
+
+
+ {formatAddress(addr.address)} +
+ +
+
+
+
+ {addr.nickname} +
+
+
+ {formatBalance(addr.balance)} CNPY +
+
+
+ {formatBalance(addr.staked)} CNPY +
+
+
+ {formatBalance(addr.total)} CNPY +
+
+ + {addr.status} + +
+ No addresses found +
+
+
+
+
+ ); +}; + +export default AllAddresses; diff --git a/cmd/rpc/web/wallet-new/src/app/pages/AllTransactions.tsx b/cmd/rpc/web/wallet-new/src/app/pages/AllTransactions.tsx new file mode 100644 index 000000000..1eb093e59 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/AllTransactions.tsx @@ -0,0 +1,320 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { motion } from "framer-motion"; +import { Search, ExternalLink } from "lucide-react"; +import { useDashboard } from "@/hooks/useDashboard"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { LucideIcon } from "@/components/ui/LucideIcon"; + +const getStatusColor = (s: string) => + s === "Confirmed" + ? "bg-green-500/20 text-green-400" + : s === "Open" + ? "bg-red-500/20 text-red-400" + : s === "Pending" + ? "bg-yellow-500/20 text-yellow-400" + : "bg-gray-500/20 text-gray-400"; + +const toEpochMs = (t: any) => { + const n = Number(t ?? 0); + if (!Number.isFinite(n) || n <= 0) return 0; + if (n > 1e16) return Math.floor(n / 1e6); + if (n > 1e13) return Math.floor(n / 1e3); + return n; +}; + +const formatTimeAgo = (tsMs: number) => { + const now = Date.now(); + const diff = Math.max(0, now - (tsMs || 0)); + const m = Math.floor(diff / 60000); + const h = Math.floor(diff / 3600000); + const d = Math.floor(diff / 86400000); + if (m < 60) return `${m} min ago`; + if (h < 24) return `${h} hour${h > 1 ? "s" : ""} ago`; + return `${d} day${d > 1 ? "s" : ""} ago`; +}; + +const formatDate = (tsMs: number) => { + return new Date(tsMs).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}; + +export const AllTransactions = () => { + const { allTxs, isTxLoading } = useDashboard(); + const { manifest, chain } = useConfig(); + + const [searchTerm, setSearchTerm] = useState(""); + const [filterType, setFilterType] = useState("all"); + const [filterStatus, setFilterStatus] = useState("all"); + + const getIcon = useCallback( + (txType: string) => manifest?.ui?.tx?.typeIconMap?.[txType] ?? "Circle", + [manifest], + ); + + const getTxMap = useCallback( + (txType: string) => manifest?.ui?.tx?.typeMap?.[txType] ?? txType, + [manifest], + ); + + const getFundWay = useCallback( + (txType: string) => manifest?.ui?.tx?.fundsWay?.[txType] ?? "neutral", + [manifest], + ); + + const symbol = String(chain?.denom?.symbol) ?? "CNPY"; + + const toDisplay = useCallback( + (amount: number) => { + const decimals = Number(chain?.denom?.decimals) ?? 6; + return amount / Math.pow(10, decimals); + }, + [chain], + ); + + // Get unique transaction types + const txTypes = useMemo(() => { + const types = new Set(allTxs.map((tx) => tx.type)); + return ["all", ...Array.from(types)]; + }, [allTxs]); + + // Filter transactions + const filteredTransactions = useMemo(() => { + return allTxs.filter((tx) => { + const matchesSearch = + searchTerm === "" || + tx.hash.toLowerCase().includes(searchTerm.toLowerCase()) || + getTxMap(tx.type).toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesType = filterType === "all" || tx.type === filterType; + const matchesStatus = + filterStatus === "all" || tx.status === filterStatus; + + return matchesSearch && matchesType && matchesStatus; + }); + }, [allTxs, searchTerm, filterType, filterStatus, getTxMap]); + + if (isTxLoading) { + return ( +
+
Loading transactions...
+
+ ); + } + + return ( + +
+ {/* Header */} +
+

+ All Transactions +

+

+ View and manage all your transaction history +

+
+ + {/* Filters */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-bg-primary border border-bg-accent rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:border-primary/40 transition-colors" + /> +
+ + {/* Type Filter */} +
+ +
+ + {/* Status Filter */} +
+ +
+
+
+ + {/* Stats */} +
+
+
+ Total Transactions +
+
+ {allTxs.length} +
+
+
+
Confirmed
+
+ {allTxs.filter((tx) => tx.status === "Confirmed").length} +
+
+
+
Pending
+
+ {allTxs.filter((tx) => tx.status === "Pending").length} +
+
+
+
Filtered Results
+
+ {filteredTransactions.length} +
+
+
+ + {/* Transactions Table */} +
+
+ + + + + + + + + + + + + {filteredTransactions.length > 0 ? ( + filteredTransactions.map((tx, i) => { + const fundsWay = getFundWay(tx.type); + const prefix = + fundsWay === "out" ? "-" : fundsWay === "in" ? "+" : ""; + const amountTxt = `${prefix}${toDisplay(Number(tx.amount || 0)).toFixed(2)} ${symbol}`; + const epochMs = toEpochMs(tx.time); + + return ( + + + + + + + + + ); + }) + ) : ( + + + + )} + +
+ Time + + Type + + Hash + + Amount + + Status + + Actions +
+
+ {formatTimeAgo(epochMs)} +
+
+ {formatDate(epochMs)} +
+
+
+ + + {getTxMap(tx.type)} + +
+
+
+ {tx.hash.slice(0, 8)}...{tx.hash.slice(-6)} +
+
+
+ {amountTxt} +
+
+ + {tx.status} + + + + Explorer + + +
+ No transactions found +
+
+
+
+
+ ); +}; + +export default AllTransactions; diff --git a/cmd/rpc/web/wallet-new/src/app/pages/Dashboard.tsx b/cmd/rpc/web/wallet-new/src/app/pages/Dashboard.tsx new file mode 100644 index 000000000..132e5996f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Dashboard.tsx @@ -0,0 +1,102 @@ +import {motion} from 'framer-motion'; +import {TotalBalanceCard} from '@/components/dashboard/TotalBalanceCard'; +import {StakedBalanceCard} from '@/components/dashboard/StakedBalanceCard'; +import {QuickActionsCard} from '@/components/dashboard/QuickActionsCard'; +import {AllAddressesCard} from '@/components/dashboard/AllAddressesCard'; +import {NodeManagementCard} from '@/components/dashboard/NodeManagementCard'; +import {ErrorBoundary} from '@/components/ErrorBoundary'; +import {RecentTransactionsCard} from "@/components/dashboard/RecentTransactionsCard"; +import {ActionsModal} from "@/actions/ActionsModal"; +import {useDashboard} from "@/hooks/useDashboard"; + + +export const Dashboard = () => { + + const { + manifestLoading, + manifest, + isTxLoading, + allTxs, + onRunAction, + isActionModalOpen, + setIsActionModalOpen, + selectedActions + } = useDashboard(); + + const containerVariants = { + hidden: {opacity: 0}, + visible: { + opacity: 1, + transition: { + duration: 0.6, + staggerChildren: 0.1 + } + } + }; + + if (manifestLoading) { + return ( +
+
Loading dashboard...
+
+ ); + } + + return ( + + +
+ {/* Top Section - Balance Cards */} +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + {/* Middle Section - Transactions and Addresses */} +
+
+ + + +
+
+ + + +
+
+ + {/* Bottom Section - Node Management */} +
+ + + +
+
+ + setIsActionModalOpen(false)}/> +
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/app/pages/Governance.tsx b/cmd/rpc/web/wallet-new/src/app/pages/Governance.tsx new file mode 100644 index 000000000..84e3cf198 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Governance.tsx @@ -0,0 +1,264 @@ +import React, { useState, useCallback, useMemo } from "react"; +import { motion } from "framer-motion"; +import { Plus, BarChart3 } from "lucide-react"; +import { useGovernance, Poll, Proposal } from "@/hooks/useGovernance"; +import { ProposalTable } from "@/components/governance/ProposalTable"; +import { PollCard } from "@/components/governance/PollCard"; +import { ProposalDetailsModal } from "@/components/governance/ProposalDetailsModal"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { ActionsModal } from "@/actions/ActionsModal"; +import { useManifest } from "@/hooks/useManifest"; +import { useAccounts } from "@/app/providers/AccountsProvider"; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.6, + staggerChildren: 0.1, + }, + }, +}; + +export const Governance = () => { + const { selectedAccount } = useAccounts(); + const { data: proposals = [] } = useGovernance(); + const { manifest } = useManifest(); + + const [isActionModalOpen, setIsActionModalOpen] = useState(false); + const [selectedActions, setSelectedActions] = useState([]); + const [selectedProposal, setSelectedProposal] = useState( + null, + ); + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + // Separate active and past proposals + const { activeProposals, pastProposals } = useMemo(() => { + const active = proposals.filter( + (p: { status: string }) => + p.status === "active" || p.status === "pending", + ); + const past = proposals.filter( + (p: { status: string }) => + p.status === "passed" || p.status === "rejected", + ); + return { activeProposals: active, pastProposals: past }; + }, [proposals]); + + // Mock polls data (since we don't have polls endpoint yet) + const mockPolls: Poll[] = useMemo(() => { + // Transform some active proposals into polls for demonstration + return activeProposals.slice(0, 2).map((p: Proposal) => ({ + id: p.hash, + hash: p.hash, + title: p.title, + description: p.description, + status: p.status === "active" ? ("active" as const) : ("passed" as const), + endTime: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days from now + yesPercent: p.yesPercent, + noPercent: p.noPercent, + accountVotes: { + yes: Math.floor(p.yesPercent * 0.7), + no: Math.floor(p.noPercent * 0.7), + }, + validatorVotes: { + yes: Math.floor(p.yesPercent * 0.3), + no: Math.floor(p.noPercent * 0.3), + }, + approve: p.approve, + createdHeight: p.createdHeight, + endHeight: p.endHeight, + time: p.time, + })); + }, [activeProposals]); + + const handleVoteProposal = useCallback( + (proposalHash: string, vote: "approve" | "reject") => { + console.log(`Voting ${vote} on proposal ${proposalHash}`); + + // Find the vote action in the manifest + const voteAction = manifest?.actions?.find( + (action: any) => action.id === "vote", + ); + + if (voteAction) { + setSelectedActions([ + { + ...voteAction, + prefilledData: { + proposalId: proposalHash, + vote: vote === "approve" ? "yes" : "no", + }, + }, + ]); + setIsActionModalOpen(true); + } else { + alert( + `Vote ${vote} on proposal ${proposalHash.slice(0, 8)}...\n\nNote: Add 'vote' action to manifest.json to enable actual voting.`, + ); + } + }, + [manifest], + ); + + const handleVotePoll = useCallback( + (pollHash: string, vote: "approve" | "reject") => { + console.log(`Voting ${vote} on poll ${pollHash}`); + alert( + `Poll voting: ${vote} on ${pollHash.slice(0, 8)}...\n\nThis will be integrated with the poll voting endpoint.`, + ); + }, + [], + ); + + const handleCreateProposal = useCallback(() => { + const createProposalAction = manifest?.actions?.find( + (action: any) => action.id === "createProposal", + ); + + if (createProposalAction) { + setSelectedActions([createProposalAction]); + setIsActionModalOpen(true); + } else { + alert( + 'Create proposal functionality\n\nAdd "createProposal" action to manifest.json to enable.', + ); + } + }, [manifest]); + + const handleCreatePoll = useCallback(() => { + alert( + "Create Poll functionality\n\nThis will open a modal to create a new poll.", + ); + }, []); + + const handleViewDetails = useCallback( + (hash: string) => { + const proposal = proposals.find((p: { hash: string }) => p.hash === hash); + if (proposal) { + setSelectedProposal(proposal); + setIsDetailsModalOpen(true); + } + }, + [proposals], + ); + + return ( + + +
+ {/* Active Proposals and Polls Grid */} +
+ {/* Active Proposals Section */} +
+
+
+

+ Active Proposals +

+

+ Vote on proposals that shape the future of the Canopy + ecosystem +

+
+
+ + + + +
+ + {/* Active Polls Section */} +
+
+
+

+ Active Polls +

+
+
+ + +
+
+ + {/* Polls Grid */} +
+ {mockPolls.length === 0 ? ( +
+ +

No active polls

+
+ ) : ( + mockPolls.map((poll) => ( + + + + )) + )} +
+
+
+ + {/* Past Proposals Section */} +
+ + + +
+ + {/* Past Polls Section would go here */} +
+ + {/* Actions Modal */} + setIsActionModalOpen(false)} + /> + + {/* Proposal Details Modal */} + setIsDetailsModalOpen(false)} + onVote={handleVoteProposal} + /> +
+
+ ); +}; + +export default Governance; diff --git a/cmd/rpc/web/wallet-new/src/app/pages/KeyManagement.tsx b/cmd/rpc/web/wallet-new/src/app/pages/KeyManagement.tsx new file mode 100644 index 000000000..015705f36 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/KeyManagement.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Download } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { CurrentWallet } from '@/components/key-management/CurrentWallet'; +import { ImportWallet } from '@/components/key-management/ImportWallet'; +import { NewKey } from '@/components/key-management/NewKey'; +import { useDS } from '@/core/useDs'; +import { downloadJson } from '@/helpers/download'; +import { useToast } from '@/toast/ToastContext'; + + + +export const KeyManagement = (): JSX.Element => { + const toast = useToast(); + const { data: keystore } = useDS('keystore', {}); + + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.6, + staggerChildren: 0.1 + } + } + }; + + const handleDownloadKeys = () => { + if (!keystore) { + toast.error({ + title: 'No keys available', + description: 'Keystore data has not loaded yet.', + }); + return; + } + + downloadJson(keystore, 'keystore'); + toast.success({ + title: 'Download started', + description: 'Your keystore JSON is on its way.', + }); + }; + + return ( +
+ {/* Main Content */} +
+
+ +

Key Management

+

Manage your wallet keys and security settings

+
+ +
+ + {/* Three Panel Layout */} + + + + + + +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/app/pages/Monitoring.tsx b/cmd/rpc/web/wallet-new/src/app/pages/Monitoring.tsx new file mode 100644 index 000000000..5d57bbe70 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Monitoring.tsx @@ -0,0 +1,173 @@ +import React, { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { useAvailableNodes, useNodeData } from "@/hooks/useNodes"; +import NodeStatus from "@/components/monitoring/NodeStatus"; +import NetworkPeers from "@/components/monitoring/NetworkPeers"; +import NodeLogs from "@/components/monitoring/NodeLogs"; +import PerformanceMetrics from "@/components/monitoring/PerformanceMetrics"; +import SystemResources from "@/components/monitoring/SystemResources"; +import RawJSON from "@/components/monitoring/RawJSON"; +import MonitoringSkeleton from "@/components/monitoring/MonitoringSkeleton"; + +export default function Monitoring(): JSX.Element { + const [activeTab, setActiveTab] = useState< + "quorum" | "logger" | "config" | "peerInfo" | "peerBook" + >("quorum"); + const [isPaused, setIsPaused] = useState(false); + + // Get current node (single node only) + const { data: availableNodes = [], isLoading: nodesLoading } = + useAvailableNodes(); + const currentNode = availableNodes[0]; // Always use the first (and only) node + + // Get data for current node + const { data: nodeData, isLoading: nodeDataLoading } = useNodeData( + currentNode?.id || "", + ); + + // Process node data from React Query + const nodeStatus = { + synced: nodeData?.consensus?.isSyncing === false, + blockHeight: nodeData?.consensus?.view?.height || 0, + syncProgress: + nodeData?.consensus?.isSyncing === false + ? 100 + : nodeData?.consensus?.syncProgress || 0, + nodeAddress: nodeData?.consensus?.address || "", + phase: nodeData?.consensus?.view?.phase || "", + round: nodeData?.consensus?.view?.round || 0, + networkID: nodeData?.consensus?.view?.networkID || 0, + chainId: nodeData?.consensus?.view?.chainId || 0, + status: nodeData?.consensus?.status || "", + blockHash: nodeData?.consensus?.blockHash || "", + resultsHash: nodeData?.consensus?.resultsHash || "", + proposerAddress: nodeData?.consensus?.proposerAddress || "", + }; + + const networkPeers = { + totalPeers: nodeData?.peers?.numPeers || 0, + connections: { + in: nodeData?.peers?.numInbound || 0, + out: nodeData?.peers?.numOutbound || 0, + }, + peerId: nodeData?.peers?.id?.publicKey || "", + networkAddress: + nodeData?.validatorSet?.validatorSet?.find( + (v: any) => v.publicKey === nodeData?.consensus?.publicKey, + )?.netAddress || "", + publicKey: nodeData?.consensus?.publicKey || "", + peers: nodeData?.peers?.peers || [], + }; + + const logs = + typeof nodeData?.logs === "string" + ? nodeData.logs.split("\n").filter(Boolean) + : []; + + const metrics = { + processCPU: nodeData?.resources?.process?.usedCPUPercent || 0, + systemCPU: nodeData?.resources?.system?.usedCPUPercent || 0, + processRAM: nodeData?.resources?.process?.usedMemoryPercent || 0, + systemRAM: nodeData?.resources?.system?.usedRAMPercent || 0, + diskUsage: nodeData?.resources?.system?.usedDiskPercent || 0, + networkIO: (nodeData?.resources?.system?.ReceivedBytesIO || 0) / 1000000, + totalRAM: nodeData?.resources?.system?.totalRAM || 0, + availableRAM: nodeData?.resources?.system?.availableRAM || 0, + usedRAM: nodeData?.resources?.system?.usedRAM || 0, + freeRAM: nodeData?.resources?.system?.freeRAM || 0, + totalDisk: nodeData?.resources?.system?.totalDisk || 0, + usedDisk: nodeData?.resources?.system?.usedDisk || 0, + freeDisk: nodeData?.resources?.system?.freeDisk || 0, + receivedBytes: nodeData?.resources?.system?.ReceivedBytesIO || 0, + writtenBytes: nodeData?.resources?.system?.WrittenBytesIO || 0, + }; + + const systemResources = { + threadCount: nodeData?.resources?.process?.threadCount || 0, + fileDescriptors: nodeData?.resources?.process?.fdCount || 0, + maxFileDescriptors: nodeData?.resources?.process?.maxFileDescriptors || 0, + }; + + const handleCopyAddress = () => { + if (nodeStatus.nodeAddress) { + navigator.clipboard.writeText(nodeStatus.nodeAddress); + } + }; + + const handlePauseToggle = () => { + setIsPaused(!isPaused); + }; + + const handleClearLogs = () => { + // Logs are managed by React Query, this is just for UI state + console.log("Clear logs requested"); + }; + + const handleExportLogs = () => { + const blob = new Blob([logs.join("\n")], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "node-logs.txt"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + // No-op function for node change since we only have one node + const handleNodeChange = () => { + // This function is kept for component compatibility but does nothing + // since we only monitor the current node + }; + + // Loading state + if (nodesLoading || nodeDataLoading) { + return ; + } + + return ( + +
+ + + {/* Two column layout for main content */} +
+ {/* Left column */} +
+ + +
+ + {/* Right column */} +
+ + + +
+
+
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/app/pages/Staking.tsx b/cmd/rpc/web/wallet-new/src/app/pages/Staking.tsx new file mode 100644 index 000000000..f7a4f3d88 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Staking.tsx @@ -0,0 +1,214 @@ +import React, { + useEffect, + useRef, + useMemo, + useState, + useCallback, +} from "react"; +import { motion } from "framer-motion"; +import { useStakingData } from "@/hooks/useStakingData"; +import { useValidators } from "@/hooks/useValidators"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useMultipleBlockProducerData } from "@/hooks/useBlockProducerData"; +import { useManifest } from "@/hooks/useManifest"; +import { useDSFetcher } from "@/core/dsFetch"; +import { StatsCards } from "@/components/staking/StatsCards"; +import { Toolbar } from "@/components/staking/Toolbar"; +import { ValidatorList } from "@/components/staking/ValidatorList"; +import { useActionModal } from "@/app/providers/ActionModalProvider"; + +type ValidatorRow = { + address: string; + nickname?: string; + stakedAmount: number; + status: "Staked" | "Paused" | "Unstaking"; + rewards24h: number; + chains?: string[]; + isSynced: boolean; + // Additional validator information + committees?: number[]; + compound?: boolean; + delegate?: boolean; + maxPausedHeight?: number; + netAddress?: string; + output?: string; + publicKey?: string; + unstakingHeight?: number; +}; + +const chainLabels = ["DEX", "CAN"] as const; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { opacity: 1, transition: { duration: 0.6, staggerChildren: 0.1 } }, +}; + +export default function Staking(): JSX.Element { + const { + data: staking = { totalStaked: 0, totalRewards: 0, chartData: [] } as any, + } = useStakingData(); + const { totalStaked } = useAccountData(); + const { data: validators = [] } = useValidators(); + const { openAction } = useActionModal(); + const dsFetch = useDSFetcher(); + + const csvRef = useRef(null); + + const [searchTerm, setSearchTerm] = useState(""); + const [chainCount, setChainCount] = useState(0); + + const validatorAddresses = useMemo( + () => validators.map((v: any) => v.address), + [validators], + ); + + const { data: blockProducerData = {} } = + useMultipleBlockProducerData(validatorAddresses); + + useEffect(() => { + let isCancelled = false; + + const run = async () => { + try { + const all = await dsFetch("validators"); + const ourAddresses = new Set(validators.map((v: any) => v.address)); + const committees = new Set(); + (all || []).forEach((v: any) => { + if (ourAddresses.has(v.address) && Array.isArray(v.committees)) { + v.committees.forEach((c: number) => committees.add(c)); + } + }); + if (!isCancelled) { + setChainCount((prev) => + prev !== committees.size ? committees.size : prev, + ); + } + } catch { + if (!isCancelled) setChainCount(0); + } + }; + + if (validators.length > 0) run(); + return () => { + isCancelled = true; + }; + }, [validators]); + + // 🧮 Construir filas memoizadas + const rows: ValidatorRow[] = useMemo(() => { + return validators.map((v: any) => ({ + address: v.address, + nickname: v.nickname, + stakedAmount: v.stakedAmount || 0, + status: v.unstaking ? "Unstaking" : v.paused ? "Paused" : "Staked", + rewards24h: blockProducerData[v.address]?.rewards24h || 0, + chains: + v.committees?.map( + (id: number) => chainLabels[id % chainLabels.length], + ) || [], + isSynced: !v.paused, + // Additional info + committees: v.committees, + compound: v.compound, + delegate: v.delegate, + maxPausedHeight: v.maxPausedHeight, + netAddress: v.netAddress, + output: v.output, + publicKey: v.publicKey, + unstakingHeight: v.unstakingHeight, + })); + }, [validators, blockProducerData]); + + const filtered: ValidatorRow[] = useMemo(() => { + const q = searchTerm.toLowerCase(); + if (!q) return rows; + return rows.filter( + (r) => + (r.nickname || "").toLowerCase().includes(q) || + r.address.toLowerCase().includes(q), + ); + }, [rows, searchTerm]); + + const prepareCSVData = useCallback(() => { + const header = [ + "address", + "nickname", + "stakedAmount", + "rewards24h", + "status", + ]; + const lines = [header.join(",")].concat( + filtered.map((r) => + [ + r.address, + r.nickname || "", + r.stakedAmount, + r.rewards24h, + r.status, + ].join(","), + ), + ); + return lines.join("\n"); + }, [filtered]); + + const exportCSV = useCallback(() => { + const csvContent = prepareCSVData(); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + + if (csvRef.current) { + csvRef.current.href = url; + csvRef.current.download = "validators.csv"; + csvRef.current.click(); + } + + setTimeout(() => URL.revokeObjectURL(url), 100); + }, [prepareCSVData]); + + const activeValidatorsCount = useMemo( + () => validators.filter((v: any) => !v.paused).length, + [validators], + ); + + // Handler para agregar stake - abre el action "stake" del manifest + const handleAddStake = useCallback(() => { + openAction("stake"); + }, [openAction]); + + return ( + + {/* Hidden link for CSV export */} + + +
+ {/* Top stats */} + + +
+ {/* Toolbar */} + + + {/* Validator List */} + +
+
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/app/providers/AccountsProvider.tsx b/cmd/rpc/web/wallet-new/src/app/providers/AccountsProvider.tsx new file mode 100644 index 000000000..3ac49b980 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/AccountsProvider.tsx @@ -0,0 +1,184 @@ +'use client' + +import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react' +import { useConfig } from '@/app/providers/ConfigProvider' +import {useDS} from "@/core/useDs"; +import {useDSFetcher} from "@/core/dsFetch"; + + + +type KeystoreResponse = { + addressMap: Record + nicknameMap: Record +} + +export type Account = { + id: string + address: string + nickname: string + publicKey: string, + isActive?: boolean, +} + +type AccountsContextValue = { + accounts: Account[] + selectedId: string | null + selectedAccount: Account | null + selectedAddress?: string + loading: boolean + error: string | null + isReady: boolean + + switchAccount: (id: string | null) => void + createNewAccount: (nickname: string, password: string) => Promise + deleteAccount: (accountId: string) => Promise + refetch: () => Promise +} + +const AccountsContext = createContext(undefined) + +const STORAGE_KEY = 'activeAccountId' + +export function AccountsProvider({ children }: { children: React.ReactNode }) { + const { data: ks, isLoading, isFetching, error, refetch } = + useDS('keystore', {}, { refetchIntervalMs: 30 * 1000 }) + + const dsFetch = useDSFetcher() + + const accounts: Account[] = useMemo(() => { + const map = ks?.addressMap ?? {} + return Object.entries(map).map(([address, entry]) => ({ + id: address, + address, + nickname: (entry as any).keyNickname || `Account ${address.slice(0, 8)}...`, + publicKey: (entry as any).publicKey ?? (entry as any).public_key ?? '', + })) + }, [ks]) + + const [selectedId, setSelectedId] = useState(null) + const [isReady, setIsReady] = useState(false) + + useEffect(() => { + try { + const saved = typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null + if (saved) setSelectedId(saved) + } finally { + setIsReady(true) + } + const onStorage = (e: StorageEvent) => { + if (e.key === STORAGE_KEY) setSelectedId(e.newValue ?? null) + } + window.addEventListener('storage', onStorage) + return () => window.removeEventListener('storage', onStorage) + }, []) + + useEffect(() => { + if (!isReady) return + if (!selectedId && accounts.length > 0) { + const first = accounts[0].id + setSelectedId(first) + if (typeof window !== 'undefined') localStorage.setItem(STORAGE_KEY, first) + } + }, [isReady, selectedId, accounts]) + + const selectedAccount = useMemo( + () => accounts.find(a => a.id === selectedId) ?? null, + [accounts, selectedId] + ) + + const selectedAddress = useMemo(() => selectedAccount?.address, [selectedAccount]) + + const stableError = useMemo( + () => (error ? ((error as any).message ?? 'Error') : null), + [error] + ) + + const switchAccount = useCallback((id: string | null) => { + setSelectedId(id) + if (typeof window !== 'undefined') { + if (id) localStorage.setItem(STORAGE_KEY, id) + else localStorage.removeItem(STORAGE_KEY) + } + }, []) + + const createNewAccount = useCallback(async (nickname: string, password: string): Promise => { + try { + // Use the keystoreNewKey datasource + const response = await dsFetch('keystoreNewKey', { + nickname, + password + }) + + // Refetch accounts after creating a new one + await refetch() + + // Return the new address (remove quotes if present) + return typeof response === 'string' ? response.replace(/"/g, '') : response + } catch (err) { + console.error('Error creating account:', err) + throw err + } + }, [dsFetch, refetch]) + + const deleteAccount = useCallback(async (accountId: string): Promise => { + try { + const account = accounts.find(acc => acc.id === accountId) + if (!account) { + throw new Error('Account not found') + } + + // Use the keystoreDelete datasource + await dsFetch('keystoreDelete', { + nickname: account.nickname + }) + + // If we deleted the active account, switch to another one + if (selectedId === accountId && accounts.length > 1) { + const nextAccount = accounts.find(acc => acc.id !== accountId) + if (nextAccount) { + setSelectedId(nextAccount.id) + } + } + + // Refetch accounts after deleting + await refetch() + } catch (err) { + console.error('Error deleting account:', err) + throw err + } + }, [accounts, selectedId, dsFetch, refetch]) + + const loading = isLoading || isFetching + + const value: AccountsContextValue = useMemo(() => ({ + accounts, + selectedId, + selectedAccount, + selectedAddress, + loading, + error: stableError, + isReady, + switchAccount, + createNewAccount, + deleteAccount, + refetch, + }), [accounts, selectedId, selectedAccount, selectedAddress, loading, stableError, isReady, switchAccount, createNewAccount, deleteAccount, refetch]) + + return ( + + {children} + + ) +} + +export function useAccounts() { + const ctx = useContext(AccountsContext) + if (!ctx) throw new Error('useAccounts must be used within ') + return ctx +} diff --git a/cmd/rpc/web/wallet-new/src/app/providers/ActionModalProvider.tsx b/cmd/rpc/web/wallet-new/src/app/providers/ActionModalProvider.tsx new file mode 100644 index 000000000..9ff5ed098 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/ActionModalProvider.tsx @@ -0,0 +1,206 @@ +import React, { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import ActionRunner from '@/actions/ActionRunner'; +import { useManifest } from '@/hooks/useManifest'; +import { XIcon } from 'lucide-react'; +import { cx } from '@/ui/cx'; +import { ModalTabs, Tab } from '@/actions/ModalTabs'; +import {LucideIcon} from "@/components/ui/LucideIcon"; + +interface ActionModalContextType { + openAction: (actionId: string, options?: ActionModalOptions) => void; + closeAction: () => void; + isOpen: boolean; + currentActionId: string | null; +} + +interface ActionModalOptions { + onFinish?: () => void; + onClose?: () => void; + prefilledData?: Record; + relatedActions?: string[]; // IDs of related actions to show as tabs +} + +const ActionModalContext = createContext(undefined); + +export const useActionModal = () => { + const context = useContext(ActionModalContext); + if (!context) { + throw new Error('useActionModal must be used within ActionModalProvider'); + } + return context; +}; + +export const ActionModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [currentActionId, setCurrentActionId] = useState(null); + const [selectedTab, setSelectedTab] = useState(undefined); + const [options, setOptions] = useState({}); + const { manifest } = useManifest(); + + const openAction = useCallback((actionId: string, opts: ActionModalOptions = {}) => { + setCurrentActionId(actionId); + setOptions(opts); + setIsOpen(true); + }, []); + + const closeAction = useCallback(() => { + setIsOpen(false); + if (options.onClose) { + options.onClose(); + } + // Clear state after animation + setTimeout(() => { + setCurrentActionId(null); + setSelectedTab(undefined); + setOptions({}); + }, 300); + }, [options]); + + const handleFinish = useCallback(() => { + if (options.onFinish) { + options.onFinish(); + } + closeAction(); + }, [options, closeAction]); + + // Build tabs from current action and related actions + const availableTabs = useMemo(() => { + if (!currentActionId || !manifest) return []; + + const currentAction = manifest.actions.find(a => a.id === currentActionId); + if (!currentAction) return []; + + const tabs: Tab[] = [{ + value: currentAction.id, + label: currentAction.title || currentAction.id, + icon: currentAction.icon + }]; + + // Add related actions from options or manifest + const relatedActionIds = options.relatedActions || currentAction.relatedActions || []; + relatedActionIds.forEach(relatedId => { + const relatedAction = manifest.actions.find(a => a.id === relatedId); + if (relatedAction) { + tabs.push({ + value: relatedAction.id, + label: relatedAction.title || relatedAction.id, + icon: relatedAction.icon + }); + } + }); + + return tabs; + }, [currentActionId, manifest, options.relatedActions]); + + // Set initial selected tab when tabs change + useEffect(() => { + if (availableTabs.length > 0 && !selectedTab) { + setSelectedTab(availableTabs[0]); + } + }, [availableTabs, selectedTab]); + + // Get active action ID from selected tab or current action + const activeActionId = selectedTab?.value || currentActionId; + + // Get modal slot configuration from manifest for active action + const modalSlot = useMemo(() => { + return manifest?.actions?.find(a => a.id === activeActionId)?.ui?.slots?.modal; + }, [activeActionId, manifest]); + + const modalClassName = modalSlot?.className; + const modalStyle: React.CSSProperties | undefined = modalSlot?.style; + + // Prevent body scroll when modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = 'auto'; + }; + } + }, [isOpen]); + + return ( + + {children} + + {/* Modal Overlay */} + + {isOpen && currentActionId && ( + + e.stopPropagation()} + > + {/* Close Button */} + + + {/* Tabs - only show if there are multiple actions */} + {availableTabs.length > 1 ? ( + + ) : ( + /* Single action title */ + availableTabs.length === 1 && ( +
+ {availableTabs[0].icon && ( +
+ +
+ )} +

+ {availableTabs[0].label} +

+
+ ) + )} + + {/* Action Runner with scroll */} + {selectedTab && ( + + + + )} +
+
+ )} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/app/providers/ConfigProvider.tsx b/cmd/rpc/web/wallet-new/src/app/providers/ConfigProvider.tsx new file mode 100644 index 000000000..d0b426895 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/ConfigProvider.tsx @@ -0,0 +1,36 @@ +import React, { createContext, useContext, useMemo } from 'react' +import { useEmbeddedConfig } from '@/manifest/loader' +import { useNodeParams } from '@/manifest/params' +import type { Manifest } from '@/manifest/types' + +type Ctx = { + chain?: Record + manifest?: Manifest + params: Record + isLoading: boolean + error: unknown + base: string +} + +const ConfigCtx = createContext({ params: {}, isLoading: true, error: null, base: '' }) + +export const ConfigProvider: React.FC> = ({ children, chainId }) => { + const { chain, manifest, isLoading, error, base } = useEmbeddedConfig(chainId) + const { data: params, loading: pLoading, error: pError } = useNodeParams(chain) + + const value = useMemo(() => ({ + chain, manifest, params, + isLoading: isLoading || pLoading, + error: error ?? pError, + base + }), [chain, manifest, params, isLoading, pLoading, error, pError, base]) + + // bridge for FormRenderer validators (optional) + if (typeof window !== 'undefined') { + ;(window as any).__configCtx = { chain, manifest } + } + + return {children} +} + +export function useConfig() { return useContext(ConfigCtx) } diff --git a/cmd/rpc/web/wallet-new/src/app/providers/README_ACTION_MODAL.md b/cmd/rpc/web/wallet-new/src/app/providers/README_ACTION_MODAL.md new file mode 100644 index 000000000..d2fb322f4 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/README_ACTION_MODAL.md @@ -0,0 +1,140 @@ +# Action Modal Integration + +## Overview + +The `ActionModalProvider` provides a global modal system for running actions (like send, stake, etc.) from anywhere in the application. + +## Setup + +The provider is already integrated in `src/app/App.tsx`: + +```tsx + + + + + +``` + +## Usage + +### 1. Import the hook + +```tsx +import { useActionModal } from '@/app/providers/ActionModalProvider'; +``` + +### 2. Use in your component + +```tsx +export const YourComponent = () => { + const { openAction } = useActionModal(); + + const handleSend = () => { + openAction('send', { + onFinish: () => { + console.log('Send completed!'); + // Refresh data, show toast, etc. + }, + onClose: () => { + console.log('Modal closed'); + } + }); + }; + + return ( + + ); +}; +``` + +### 3. Available Actions + +Actions are defined in `public/plugin/canopy/manifest.json`. Common actions include: + +- `send` - Send tokens to another address +- `stake` - Stake tokens +- `unstake` - Unstake tokens +- `editStake` - Edit stake amount +- `receive` - Show receive address + +### 4. Setting the Selected Account + +Before opening an action, make sure to set the correct account: + +```tsx +import { useAccounts } from '@/hooks/useAccounts'; +import { useActionModal } from '@/app/providers/ActionModalProvider'; + +export const AccountList = () => { + const { accounts, setSelectedAccount } = useAccounts(); + const { openAction } = useActionModal(); + + const handleSendFromAccount = (accountAddress: string) => { + // Find and set the account + const account = accounts.find(a => a.address === accountAddress); + if (account && setSelectedAccount) { + setSelectedAccount(account); + } + + // Open the send action + openAction('send', { + onFinish: () => { + // Refresh balances, show success message, etc. + } + }); + }; + + return ( +
+ {accounts.map(account => ( + + ))} +
+ ); +}; +``` + +## Example: Complete Integration + +See `src/app/pages/Accounts.tsx` for a complete example of how to: + +1. Import and use the hook +2. Set the selected account before opening an action +3. Handle callbacks (onFinish, onClose) +4. Integrate with existing UI components + +## API Reference + +### `useActionModal()` + +Returns an object with: + +- `openAction(actionId: string, options?: ActionModalOptions)` - Opens an action modal +- `closeAction()` - Closes the current action modal +- `isOpen: boolean` - Whether a modal is currently open +- `currentActionId: string | null` - The ID of the currently open action + +### `ActionModalOptions` + +```typescript +interface ActionModalOptions { + onFinish?: () => void; // Called when action completes successfully + onClose?: () => void; // Called when modal is closed (any reason) +} +``` + +## Styling + +The modal uses the following classes from your theme: + +- `bg-bg-secondary` - Modal background +- `bg-bg-tertiary` - Button backgrounds +- `border-bg-accent` - Borders +- `text-text-muted` - Icon colors + +You can customize the modal appearance by editing `src/app/providers/ActionModalProvider.tsx`. diff --git a/cmd/rpc/web/wallet-new/src/app/routes.tsx b/cmd/rpc/web/wallet-new/src/app/routes.tsx new file mode 100644 index 000000000..281fb5495 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/routes.tsx @@ -0,0 +1,37 @@ + +import { createBrowserRouter } from 'react-router-dom' +import MainLayout from '@/components/layouts/MainLayout' + +import Dashboard from '@/app/pages/Dashboard' +import { KeyManagement } from '@/app/pages/KeyManagement' +import { Accounts } from '@/app/pages/Accounts' +import Staking from '@/app/pages/Staking' +import Monitoring from '@/app/pages/Monitoring' +import Governance from '@/app/pages/Governance' +import AllTransactions from '@/app/pages/AllTransactions' +import AllAddresses from '@/app/pages/AllAddresses' + +// Placeholder components for the new routes +const Portfolio = () =>
Portfolio - Próximamente
+ +const router = createBrowserRouter([ + { + element: , + children: [ + { path: '/', element: }, + { path: '/accounts', element: }, + { path: '/portfolio', element: }, + { path: '/staking', element: }, + { path: '/governance', element: }, + { path: '/monitoring', element: }, + { path: '/key-management', element: }, + { path: '/all-transactions', element: }, + { path: '/all-addresses', element: }, + ], + }, +], { + basename: import.meta.env.BASE_URL, +}) + +export default router + diff --git a/cmd/rpc/web/wallet-new/src/components/ErrorBoundary.tsx b/cmd/rpc/web/wallet-new/src/components/ErrorBoundary.tsx new file mode 100644 index 000000000..fc4cdc29b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ErrorBoundary.tsx @@ -0,0 +1,61 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+
+
⚠️
+

+ Something went wrong +

+

+ An unexpected error occurred. Please reload the page. +

+ + {this.state.error && ( +
+ + Error details + +
+                  {this.state.error.message}
+                
+
+ )} +
+
+ ); + } + + return this.props.children; + } +} diff --git a/cmd/rpc/web/wallet-new/src/components/UnlockModal.tsx b/cmd/rpc/web/wallet-new/src/components/UnlockModal.tsx new file mode 100644 index 000000000..9463d9b6a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/UnlockModal.tsx @@ -0,0 +1,52 @@ +import React, {useState} from 'react' +import {useSession} from '../state/session' +import {LockOpenIcon, XIcon} from "lucide-react"; + +export default function UnlockModal({address, ttlSec, open, onClose}: + { address: string; ttlSec: number; open: boolean; onClose: () => void }) { + const [pwd, setPwd] = useState('') + const [err, setErr] = useState('') + const unlock = useSession(s => s.unlock) + if (!open) return null + + const submit = async () => { + if (!pwd) { + setErr('Password required'); + return + } + unlock(address, pwd, ttlSec) + onClose() + } + + return ( +
+
+

Unlock wallet

+

Authorize transactions for the + next {Math.round(ttlSec / 60)} minutes.

+ setPwd(e.target.value)} + placeholder="Password" + className="w-full bg-transparent text-canopy-50 border border-muted rounded-md px-3 py-2" + /> + {err &&
{err}
} +
+ + + + +
+
+
+ ) +} diff --git a/cmd/rpc/web/wallet-new/src/components/accounts/AddressRow.tsx b/cmd/rpc/web/wallet-new/src/components/accounts/AddressRow.tsx new file mode 100644 index 000000000..b15440ad9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/accounts/AddressRow.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { useManifest } from '@/hooks/useManifest'; + +interface Address { + id: string; + address: string; + totalBalance: number; + staked: number; + liquid: number; + status: 'Active' | 'Inactive' | 'Pending'; + icon: string; + iconBg: string; +} + +interface AddressRowProps { + address: Address; + index: number; + onViewDetails: (address: string) => void; + onSend: (address: string) => void; + onReceive: (address: string) => void; +} + +const formatAddress = (address: string) => { + return address.substring(0, 5) + '...' + address.substring(address.length - 6); +}; + +const formatBalance = (amount: number) => { + return (amount / 1000000).toFixed(2); +}; + +export const AddressRow: React.FC = ({ + address, + index, + onViewDetails, + onSend, + onReceive + }) => { + + const getStatusColor = (status: string) => { + switch (status) { + case 'Active': + return 'bg-green-500/20 text-green-400'; + case 'Inactive': + return 'bg-red-500/20 text-red-400'; + case 'Pending': + return 'bg-yellow-500/20 text-yellow-400'; + default: + return 'bg-gray-500/20 text-gray-400'; + } + }; + + return ( + + +
+
+ +
+
+
{formatAddress(address.address)}
+
{address.address}
+
+
+ + +
{formatBalance(address.totalBalance)} CNPY
+ + +
{formatBalance(address.staked)} CNPY
+ + +
{formatBalance(address.liquid)} CNPY
+ + + + {address.status} + + + +
+ + + +
+ +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/accounts/StatsCard.tsx b/cmd/rpc/web/wallet-new/src/components/accounts/StatsCard.tsx new file mode 100644 index 000000000..64c11ad02 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/accounts/StatsCard.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Wallet, Lock, Gift } from "lucide-react"; +import { Line } from "react-chartjs-2"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; + +interface StatsCardsProps { + totalBalance: number; + totalStaked: number; + totalRewards: number; + balanceChange: number; + stakingChange: number; + rewardsChange: number; + balanceChartData: any; + stakingChartData: any; + rewardsChartData: any; + chartOptions: any; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }, +}; + +export const StatsCards: React.FC = ({ + totalBalance, + totalStaked, + totalRewards, + balanceChange, + stakingChange, + rewardsChange, + balanceChartData, + stakingChartData, + rewardsChartData, + chartOptions, +}) => { + const statsData = [ + { + id: "totalBalance", + title: "Total Balance", + value: totalBalance, + change: balanceChange, + chartData: balanceChartData, + icon: Wallet, + iconColor: "text-primary", + }, + { + id: "totalStaked", + title: "Total Staked", + value: totalStaked, + change: stakingChange, + chartData: stakingChartData, + icon: Lock, + iconColor: "text-primary", + }, + { + id: "totalRewards", + title: "Total Rewards", + value: totalRewards, + change: rewardsChange, + chartData: rewardsChartData, + icon: Gift, + iconColor: "text-primary", + }, + ]; + + return ( +
+ {statsData.map((stat) => ( + +
+

+ {stat.title} +

+ +
+
+ +  CNPY +
+
+ = 0 ? "text-primary" : "text-red-400"}`} + > + {stat.change >= 0 ? "+" : ""} + {stat.change.toFixed(1)}% + + {" "} + 24h change + + +
+ +
+
+
+ ))} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/accounts/index.ts b/cmd/rpc/web/wallet-new/src/components/accounts/index.ts new file mode 100644 index 000000000..1c1b1cbfd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/accounts/index.ts @@ -0,0 +1,2 @@ +export { StatsCards } from './StatsCard'; +export { AddressRow } from './AddressRow'; diff --git a/cmd/rpc/web/wallet-new/src/components/dashboard/AllAddressesCard.tsx b/cmd/rpc/web/wallet-new/src/components/dashboard/AllAddressesCard.tsx new file mode 100644 index 000000000..812a55b58 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/AllAddressesCard.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Wallet } from "lucide-react"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { NavLink } from "react-router-dom"; + +export const AllAddressesCard = () => { + const { accounts, loading: accountsLoading } = useAccounts(); + const { balances, stakingData, loading: dataLoading } = useAccountData(); + + const formatAddress = (address: string) => { + return ( + address.substring(0, 6) + "..." + address.substring(address.length - 4) + ); + }; + + const formatBalance = (amount: number) => { + return (amount / 1000000).toFixed(2); // Convert from micro denomination + }; + + const getAccountStatus = (address: string) => { + // Check if this address has staking data + const stakingInfo = stakingData.find((data) => data.address === address); + if (stakingInfo && stakingInfo.staked > 0) { + return "Staked"; + } + return "Liquid"; + }; + + // Removed mocked images - using consistent wallet icon + + const getStatusColor = (status: string) => { + switch (status) { + case "Staked": + return "bg-primary/20 text-primary"; + case "Unstaking": + return "bg-orange-500/20 text-orange-400"; + case "Liquid": + return "bg-gray-500/20 text-gray-400"; + case "Delegated": + return "bg-primary/20 text-primary"; + default: + return "bg-gray-500/20 text-gray-400"; + } + }; + + const getChangeColor = (change: string) => { + return change.startsWith("+") ? "text-green-400" : "text-red-400"; + }; + + const processedAddresses = accounts.map((account) => { + // Find the balance for this account + const balanceInfo = balances.find((b) => b.address === account.address); + const balance = balanceInfo?.amount || 0; + const formattedBalance = formatBalance(balance); + const status = getAccountStatus(account.address); + + return { + id: account.address, + address: formatAddress(account.address), + fullAddress: account.address, + nickname: account.nickname || "Unnamed", + balance: `${formattedBalance} CNPY`, + totalValue: formattedBalance, + status: status, + }; + }); + + if (accountsLoading || dataLoading) { + return ( + +
+
Loading addresses...
+
+
+ ); + } + + return ( + + {/* Title with See All link */} +
+

+ All Addresses +

+ + See All ({processedAddresses.length}) + +
+ + {/* Addresses List */} +
+ {processedAddresses.length > 0 ? ( + processedAddresses.slice(0, 4).map((address, index) => ( + +
+ {/* Icon */} +
+ +
+ + {/* Content Container */} +
+ {/* Top Row: Nickname and Address */} +
+
+ {address.nickname} +
+
+ {address.address} +
+
+ + {/* Bottom Row: Balance and Status */} +
+
+ {address.totalValue} CNPY +
+ + {address.status} + +
+
+
+
+ )) + ) : ( +
+ No addresses found +
+ )} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/dashboard/NodeManagementCard.tsx b/cmd/rpc/web/wallet-new/src/components/dashboard/NodeManagementCard.tsx new file mode 100644 index 000000000..410951279 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/NodeManagementCard.tsx @@ -0,0 +1,482 @@ +import React, { useState, useCallback, useMemo } from "react"; +import { motion } from "framer-motion"; +import { Play, Pause } from "lucide-react"; +import { useValidators } from "@/hooks/useValidators"; +import { useMultipleValidatorRewardsHistory } from "@/hooks/useMultipleValidatorRewardsHistory"; +import { useMultipleValidatorSets } from "@/hooks/useValidatorSet"; +import { useManifest } from "@/hooks/useManifest"; +import { ActionsModal } from "@/actions/ActionsModal"; + +export const NodeManagementCard = (): JSX.Element => { + const { data: validators = [], isLoading, error } = useValidators(); + const { manifest } = useManifest(); + + const validatorAddresses = validators.map((v) => v.address); + const { data: rewardsData = {} } = + useMultipleValidatorRewardsHistory(validatorAddresses); + + // Get unique committee IDs from validators + const committeeIds = useMemo(() => { + const ids = new Set(); + validators.forEach((v: any) => { + if (Array.isArray(v.committees)) { + v.committees.forEach((id: number) => ids.add(id)); + } + }); + return Array.from(ids); + }, [validators]); + + const { data: validatorSetsData = {} } = + useMultipleValidatorSets(committeeIds); + + const [isActionModalOpen, setIsActionModalOpen] = useState(false); + const [selectedActions, setSelectedActions] = useState([]); + + const formatAddress = (address: string) => { + return ( + address.substring(0, 8) + "..." + address.substring(address.length - 4) + ); + }; + + const formatStakeAmount = (amount: number) => { + return (amount / 1000000).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + }; + + const formatRewards = (rewards: number) => { + return `+${(rewards / 1000000).toFixed(2)} CNPY`; + }; + + const getWeight = (validator: any): number => { + if (!validator.committees || validator.committees.length === 0) return 0; + if (!validator.publicKey) return 0; + + // Check all committees this validator is part of + for (const committeeId of validator.committees) { + const validatorSet = validatorSetsData[committeeId]; + if (!validatorSet || !validatorSet.validatorSet) continue; + + // Find this validator by matching public key + const member = validatorSet.validatorSet.find( + (m: any) => m.publicKey === validator.publicKey, + ); + + if (member) { + // Return the voting power directly (it's already the weight) + return member.votingPower; + } + } + + return 0; + }; + + const formatWeight = (weight: number) => { + return weight.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); + }; + + const getStatus = (validator: any) => { + if (validator.unstaking) return "Unstaking"; + if (validator.paused) return "Paused"; + return "Staked"; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "Staked": + return "bg-green-500/20 text-green-400"; + case "Unstaking": + return "bg-orange-500/20 text-orange-400"; + case "Paused": + return "bg-red-500/20 text-red-400"; + default: + return "bg-gray-500/20 text-gray-400"; + } + }; + + const getNodeColor = (index: number) => { + const colors = [ + "bg-gradient-to-r from-primary/80 to-primary/40", + "bg-gradient-to-r from-orange-500/80 to-orange-500/40", + "bg-gradient-to-r from-blue-500/80 to-blue-500/40", + "bg-gradient-to-r from-red-500/80 to-red-500/40", + ]; + return colors[index % colors.length]; + }; + + const handlePauseUnpause = useCallback( + (validator: any, action: "pause" | "unpause") => { + const actionId = + action === "pause" ? "pauseValidator" : "unpauseValidator"; + const actionDef = manifest?.actions?.find((a: any) => a.id === actionId); + + if (actionDef) { + setSelectedActions([ + { + ...actionDef, + prefilledData: { + validatorAddress: validator.address, + }, + }, + ]); + setIsActionModalOpen(true); + } else { + alert(`${action} action not found in manifest`); + } + }, + [manifest], + ); + + const handlePauseAll = useCallback(() => { + const activeValidators = validators.filter((v) => !v.paused); + if (activeValidators.length === 0) { + alert("No active validators to pause"); + return; + } + + // For simplicity, pause the first validator + // In a full implementation, you could loop through all + const firstValidator = activeValidators[0]; + handlePauseUnpause(firstValidator, "pause"); + }, [validators, handlePauseUnpause]); + + const handleResumeAll = useCallback(() => { + const pausedValidators = validators.filter((v) => v.paused); + if (pausedValidators.length === 0) { + alert("No paused validators to resume"); + return; + } + + const firstValidator = pausedValidators[0]; + handlePauseUnpause(firstValidator, "unpause"); + }, [validators, handlePauseUnpause]); + + const generateMiniChart = (index: number) => { + const dataPoints = 8; + const patterns = [ + [30, 35, 40, 45, 50, 55, 60, 65], + [50, 48, 52, 50, 49, 51, 50, 52], + [70, 65, 60, 55, 50, 45, 40, 35], + [50, 60, 40, 55, 35, 50, 45, 50], + ]; + + const pattern = patterns[index % patterns.length]; + + const points = pattern.map((y, i) => ({ + x: (i / (dataPoints - 1)) * 100, + y: y, + })); + + const pathData = points + .map((point, i) => `${i === 0 ? "M" : "L"}${point.x},${point.y}`) + .join(" "); + + const isUpward = pattern[pattern.length - 1] > pattern[0]; + const isDownward = pattern[pattern.length - 1] < pattern[0]; + const color = isUpward ? "#10b981" : isDownward ? "#ef4444" : "#6b7280"; + + return ( + + + + + + + + + + {points.map((point, i) => ( + + ))} + + ); + }; + + const sortedValidators = validators.slice(0, 4).sort((a, b) => { + const getNodeNumber = (validator: any) => { + const nickname = validator.nickname || ""; + const match = nickname.match(/node_(\d+)/); + return match ? parseInt(match[1]) : 999; + }; + + return getNodeNumber(a) - getNodeNumber(b); + }); + + const processedValidators = sortedValidators.map((validator) => { + return { + address: formatAddress(validator.address), + stakeAmount: formatStakeAmount(validator.stakedAmount), + status: getStatus(validator), + rewards24h: formatRewards(rewardsData[validator.address]?.change24h || 0), + originalValidator: validator, + }; + }); + + if (isLoading) { + return ( + +
+
Loading validators...
+
+
+ ); + } + + if (error) { + return ( + +
+
Error loading validators
+
+
+ ); + } + + return ( + <> + + {/* Header with action buttons */} +
+

+ Node Management +

+
+ + +
+
+ + {/* Table - Desktop */} +
+ + + + + + + + + + + + {processedValidators.length > 0 ? ( + processedValidators.map((node, index) => { + return ( + + + + + + + + ); + }) + ) : ( + + + + )} + +
+ Address + + Stake Amount + + Status + + Rewards (24h) + + Actions +
+
+
+
+ + {node.originalValidator.nickname || + `Node ${index + 1}`} + + + {formatAddress(node.originalValidator.address)} + +
+
+
+
+ + {node.stakeAmount} + + {generateMiniChart(index)} +
+
+ + {node.status} + + + + {node.rewards24h} + + + +
+ No validators found +
+
+ + {/* Cards - Mobile */} +
+ {processedValidators.map((node, index) => ( + +
+
+
+
+
+ {node.originalValidator.nickname || `Node ${index + 1}`} +
+
+ {formatAddress(node.originalValidator.address)} +
+
+
+ +
+
+
+
Stake
+
+ {node.stakeAmount} +
+
+
+
Status
+ + {node.status} + +
+
+
+ Rewards (24h) +
+
+ {node.rewards24h} +
+
+
+
+ ))} +
+
+ + {/* Actions Modal */} + setIsActionModalOpen(false)} + /> + + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/dashboard/QuickActionsCard.tsx b/cmd/rpc/web/wallet-new/src/components/dashboard/QuickActionsCard.tsx new file mode 100644 index 000000000..367afd548 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/QuickActionsCard.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { LucideIcon } from '@/components/ui/LucideIcon'; +import {selectQuickActions} from "@/core/actionForm"; +import {Action} from "@/manifest/types"; + +export function QuickActionsCard({actions, onRunAction, maxNumberOfItems }:{ + actions?: Action[]; + onRunAction?: (a: Action) => void; + maxNumberOfItems?: number; +}) { + + const sortedActions = React.useMemo(() => + selectQuickActions(actions, maxNumberOfItems), [actions, maxNumberOfItems]) + + const cols = React.useMemo( + () => Math.min(Math.max(sortedActions.length || 1, 1), 2), + [sortedActions.length] + ); + const gridTemplateColumns = React.useMemo( + () => `repeat(${cols}, minmax(0, 1fr))`, + [cols] + ); + + return ( + +

Quick Actions

+ +
+ {sortedActions.map((a, i) => ( + onRunAction?.(a)} + className="group bg-bg-tertiary hover:bg-canopy-500 rounded-lg p-4 flex flex-col items-center gap-2 transition-all" + initial={{ opacity: 0, scale: 0.95 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ duration: 0.25 }} + whileHover={{ scale: 1.04 }} + whileTap={{ scale: 0.98 }} + aria-label={a.title ?? a.id} + > + + {a.title ?? a.id} + + ))} + {sortedActions.length === 0 && ( +
No quick actions
+ )} +
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/dashboard/RecentTransactionsCard.tsx b/cmd/rpc/web/wallet-new/src/components/dashboard/RecentTransactionsCard.tsx new file mode 100644 index 000000000..753fabb1b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/RecentTransactionsCard.tsx @@ -0,0 +1,287 @@ +import React, { useCallback } from "react"; +import { motion } from "framer-motion"; +import { ExternalLink } from "lucide-react"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { NavLink } from "react-router-dom"; + +const getStatusColor = (s: string) => + s === "Confirmed" + ? "bg-green-500/20 text-green-400" + : s === "Open" + ? "bg-red-500/20 text-red-400" + : s === "Pending" + ? "bg-yellow-500/20 text-yellow-400" + : "bg-gray-500/20 text-gray-400"; + +export interface Transaction { + hash: string; + time: number; + type: string; + amount: number; + status: string; +} + +export interface RecentTransactionsCardProps { + transactions?: Transaction[]; + isLoading?: boolean; + hasError?: boolean; +} + +const toEpochMs = (t: any) => { + const n = Number(t ?? 0); + if (!Number.isFinite(n) || n <= 0) return 0; + if (n > 1e16) return Math.floor(n / 1e6); // ns -> ms + if (n > 1e13) return Math.floor(n / 1e3); // us -> ms + return n; // ya ms +}; + +const formatTimeAgo = (tsMs: number) => { + const now = Date.now(); + const diff = Math.max(0, now - (tsMs || 0)); + const m = Math.floor(diff / 60000), + h = Math.floor(diff / 3600000), + d = Math.floor(diff / 86400000); + if (m < 60) return `${m} min ago`; + if (h < 24) return `${h} hour${h > 1 ? "s" : ""} ago`; + return `${d} day${d > 1 ? "s" : ""} ago`; +}; + +export const RecentTransactionsCard: React.FC = ({ + transactions, + isLoading = false, + hasError = false, +}) => { + const { manifest, chain } = useConfig(); + + const getIcon = useCallback( + (txType: string) => manifest?.ui?.tx?.typeIconMap?.[txType] ?? "Circle", + [manifest], + ); + const getTxMap = useCallback( + (txType: string) => manifest?.ui?.tx?.typeMap?.[txType] ?? txType, + [manifest], + ); + + const getFundWay = useCallback( + (txType: string) => manifest?.ui?.tx?.fundsWay?.[txType] ?? txType, + [manifest], + ); + + const getTxTimeAgo = useCallback((): ((tx: Transaction) => String) => { + return (tx: Transaction) => { + const epochMs = toEpochMs(tx.time); + return formatTimeAgo(epochMs); + }; + }, []); + + const symbol = String(chain?.denom?.symbol) ?? "CNPY"; + + const toDisplay = useCallback( + (amount: number) => { + const decimals = Number(chain?.denom?.decimals) ?? 6; + return amount / Math.pow(10, decimals); + }, + [chain], + ); + + if (!transactions) { + return ( + +
+
+ Select an account to view transactions +
+
+
+ ); + } + + if (!transactions?.length) { + return ( + +
+
No transactions found
+
+
+ ); + } + + if (isLoading) { + return ( + +
+
Loading transactions...
+
+
+ ); + } + + if (hasError) { + return ( + +
+
Error loading transactions
+
+
+ ); + } + + return ( + + {/* Title */} +
+
+

+ Recent Transactions +

+ + Live + +
+
+ + {/* Header - Hidden on mobile */} +
+
Time
+
Action
+
Amount
+
Status
+
+ + {/* Rows */} +
+ + {/* See All */} +
+ + See All ({transactions.length}) + +
+ + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/dashboard/StakedBalanceCard.tsx b/cmd/rpc/web/wallet-new/src/components/dashboard/StakedBalanceCard.tsx new file mode 100644 index 000000000..3184c5157 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/StakedBalanceCard.tsx @@ -0,0 +1,257 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Coins } from "lucide-react"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useStakedBalanceHistory } from "@/hooks/useStakedBalanceHistory"; +import { useBalanceChart } from "@/hooks/useBalanceChart"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; + +export const StakedBalanceCard = () => { + const { totalStaked, stakingData, loading } = useAccountData(); + + const { data: chartData = [], isLoading: chartLoading } = useBalanceChart({ + points: 4, + type: "staked", + }); + const { chain } = useConfig(); + const [hasAnimated, setHasAnimated] = useState(false); + const [hoveredPoint, setHoveredPoint] = useState(null); + const [mousePosition, setMousePosition] = useState<{ + x: number; + y: number; + } | null>(null); + + // Calculate total rewards from all staking data + const totalRewards = stakingData.reduce((sum, data) => sum + data.rewards, 0); + return ( + setHasAnimated(true)} + > + {/* Lock Icon */} +
+ +
+ + {/* Title */} +

+ Staked Balance (All addresses) +

+ + {/* Balance */} +
+ {loading ? ( +
...
+ ) : ( +
+
+ +
+
+ )} +
+ + {/* Currency */} +
CNPY
+ + {/* Full Chart */} +
+ {(() => { + try { + if (chartLoading || loading) { + return ( +
+
+ Loading chart... +
+
+ ); + } + + if (chartData.length === 0) { + return ( +
+
No chart data
+
+ ); + } + + // Normalizar datos del chart para SVG + const maxValue = Math.max(...chartData.map((d) => d.value), 1); + const minValue = Math.min(...chartData.map((d) => d.value), 0); + const range = maxValue - minValue || 1; + + const points = chartData.map((point, index) => ({ + x: (index / Math.max(chartData.length - 1, 1)) * 100, + y: 50 - ((point.value - minValue) / range) * 40, // Normalizado a rango 10-50 + })); + + const pathData = points + .map( + (point, index) => + `${index === 0 ? "M" : "L"}${point.x},${point.y}`, + ) + .join(" "); + + const fillPathData = `${pathData} L100,60 L0,60 Z`; + + const symbol = chain?.denom?.symbol || "CNPY"; + const decimals = chain?.denom?.decimals || 6; + + return ( +
{ + const rect = e.currentTarget.getBoundingClientRect(); + setMousePosition({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + }} + onMouseLeave={() => { + setHoveredPoint(null); + setMousePosition(null); + }} + > + + {/* Grid lines */} + + + + + + + + + + + + {/* Chart line */} + + + {/* Gradient fill under the line */} + + + {/* Data points with hover areas */} + {points.map((point, index) => ( + + {/* Invisible larger circle for easier hover */} + setHoveredPoint(index)} + onMouseLeave={() => setHoveredPoint(null)} + /> + {/* Visible point */} + + + ))} + + + {/* Tooltip */} + + {hoveredPoint !== null && + mousePosition && + chartData[hoveredPoint] && ( + +
+ {chartData[hoveredPoint].label} +
+
+ {( + chartData[hoveredPoint].value / + Math.pow(10, decimals) + ).toLocaleString("en-US", { + maximumFractionDigits: 2, + minimumFractionDigits: 2, + })}{" "} + {symbol} +
+
+ Block:{" "} + {chartData[hoveredPoint].timestamp.toLocaleString()} +
+
+ )} +
+
+ ); + } catch (error) { + console.error("Error rendering chart:", error); + return ( +
+
Chart error
+
+ ); + } + })()} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/dashboard/TotalBalanceCard.tsx b/cmd/rpc/web/wallet-new/src/components/dashboard/TotalBalanceCard.tsx new file mode 100644 index 000000000..44ecb4355 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/TotalBalanceCard.tsx @@ -0,0 +1,88 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { Wallet } from "lucide-react"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useBalanceHistory } from "@/hooks/useBalanceHistory"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; + +export const TotalBalanceCard = () => { + const { totalBalance, loading } = useAccountData(); + const { data: historyData, isLoading: historyLoading } = useBalanceHistory(); + const [hasAnimated, setHasAnimated] = useState(false); + + return ( + setHasAnimated(true)} + > + {/* Wallet Icon */} +
+ +
+ + {/* Title */} +

+ Total Balance (All Addresses) +

+ + {/* Balance */} +
+ {loading ? ( +
...
+ ) : ( +
+
+ +
+
+ )} +
+ + {/* 24h Change */} +
+ {historyLoading ? ( + Loading 24h change... + ) : historyData ? ( + = 0 + ? "text-primary" + : "text-status-error" + }`} + > + + + + + %24h change + + ) : ( + No historical data + )} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/feedback/Spinner.tsx b/cmd/rpc/web/wallet-new/src/components/feedback/Spinner.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/rpc/web/wallet-new/src/components/governance/GovernanceStatsCards.tsx b/cmd/rpc/web/wallet-new/src/components/governance/GovernanceStatsCards.tsx new file mode 100644 index 000000000..009d3b36c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/GovernanceStatsCards.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Proposal } from '@/hooks/useGovernance'; + +interface GovernanceStatsCardsProps { + proposals: Proposal[]; + votingPower: number; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } +}; + +export const GovernanceStatsCards: React.FC = ({ + proposals, + votingPower +}) => { + const activeProposals = proposals.filter(p => p.status === 'active').length; + const passedProposals = proposals.filter(p => p.status === 'passed').length; + const totalProposals = proposals.length; + + const formatVotingPower = (amount: number) => { + if (!amount && amount !== 0) return '0.00'; + return (amount / 1000000).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + }; + + const statsData = [ + { + id: 'votingPower', + title: 'Your Voting Power', + value: `${formatVotingPower(votingPower)} CNPY`, + subtitle: 'Based on staked amount', + icon: 'fa-solid fa-balance-scale', + iconColor: 'text-primary', + valueColor: 'text-white' + }, + { + id: 'activeProposals', + title: 'Active Proposals', + value: activeProposals.toString(), + subtitle: ( + + + Open for voting + + ), + icon: 'fa-solid fa-vote-yea', + iconColor: 'text-primary', + valueColor: 'text-white' + }, + { + id: 'passedProposals', + title: 'Passed Proposals', + value: passedProposals.toString(), + subtitle: `${totalProposals} total proposals`, + icon: 'fa-solid fa-check-circle', + iconColor: 'text-primary', + valueColor: 'text-white' + }, + { + id: 'participation', + title: 'Your Participation', + value: '0', + subtitle: 'Votes cast', + icon: 'fa-solid fa-chart-line', + iconColor: 'text-text-secondary', + valueColor: 'text-white' + } + ]; + + return ( +
+ {statsData.map((stat) => ( + +
+

+ {stat.title} +

+ +
+

+ {stat.value} +

+
+ {stat.subtitle} +
+
+ ))} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/governance/PollCard.tsx b/cmd/rpc/web/wallet-new/src/components/governance/PollCard.tsx new file mode 100644 index 000000000..4f8834214 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/PollCard.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Poll } from '@/hooks/useGovernance'; + +interface PollCardProps { + poll: Poll; + onVote?: (pollHash: string, vote: 'approve' | 'reject') => void; + onViewDetails?: (pollHash: string) => void; +} + +export const PollCard: React.FC = ({ poll, onVote, onViewDetails }) => { + const getStatusColor = (status: Poll['status']) => { + switch (status) { + case 'active': + return 'bg-primary/20 text-primary border-primary/40'; + case 'passed': + return 'bg-green-500/20 text-green-400 border-green-500/40'; + case 'rejected': + return 'bg-red-500/20 text-red-400 border-red-500/40'; + default: + return 'bg-text-muted/20 text-text-muted border-text-muted/40'; + } + }; + + const getStatusLabel = (status: Poll['status']) => { + return status.charAt(0).toUpperCase() + status.slice(1); + }; + + const formatEndTime = (endTime: string) => { + try { + const date = new Date(endTime); + const now = new Date(); + const diffMs = date.getTime() - now.getTime(); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); + + if (diffMs < 0) return 'Ended'; + if (diffHours < 1) return `${diffMins}m`; + if (diffHours < 24) return `${diffHours}h ${diffMins}m`; + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ${diffHours % 24}h`; + } catch { + return endTime; + } + }; + + return ( + + {/* Header with status and time */} +
+
+ + {getStatusLabel(poll.status)} + + {poll.status === 'active' && ( + + {formatEndTime(poll.endTime)} + + )} +
+ + #{poll.hash.slice(0, 8)}... + +
+ + {/* Title and Description */} +
+

+ {poll.title} +

+

+ {poll.description} +

+
+ + {/* Voting Progress Bars */} +
+
+ FOR: {poll.yesPercent.toFixed(1)}% + AGAINST: {poll.noPercent.toFixed(1)}% +
+ + {/* Combined Progress Bar */} +
+
+
+
+ + {/* Account vs Validator Stats */} +
+ {/* Account Votes */} +
+
+ + Accounts +
+
+
+ For + + {poll.accountVotes.yes} + +
+
+ Against + + {poll.accountVotes.no} + +
+
+
+ + {/* Validator Votes */} +
+
+ + Validators +
+
+
+ For + + {poll.validatorVotes.yes} + +
+
+ Against + + {poll.validatorVotes.no} + +
+
+
+
+
+ + {/* Action Buttons */} +
+ {poll.status === 'active' && onVote && ( + <> + + + + )} + {onViewDetails && ( + + )} +
+ + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/governance/ProposalCard.tsx b/cmd/rpc/web/wallet-new/src/components/governance/ProposalCard.tsx new file mode 100644 index 000000000..cce369180 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/ProposalCard.tsx @@ -0,0 +1,201 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Proposal } from "@/hooks/useGovernance"; + +interface ProposalCardProps { + proposal: Proposal; + onVote?: (proposalId: string, vote: "yes" | "no" | "abstain") => void; +} + +const getStatusColor = (status: Proposal["status"]) => { + switch (status) { + case "active": + return "bg-primary/20 text-primary border-primary/40"; + case "passed": + return "bg-green-500/20 text-green-400 border-green-500/40"; + case "rejected": + return "bg-red-500/20 text-red-400 border-red-500/40"; + case "pending": + return "bg-yellow-500/20 text-yellow-400 border-yellow-500/40"; + default: + return "bg-text-muted/20 text-text-muted border-text-muted/40"; + } +}; + +const getStatusLabel = (status: Proposal["status"]) => { + switch (status) { + case "active": + return "Active"; + case "passed": + return "Passed"; + case "rejected": + return "Rejected"; + case "pending": + return "Pending"; + default: + return status; + } +}; + +export const ProposalCard: React.FC = ({ + proposal, + onVote, +}) => { + const totalVotes = + proposal.yesVotes + proposal.noVotes + proposal.abstainVotes; + const yesPercentage = + totalVotes > 0 ? (proposal.yesVotes / totalVotes) * 100 : 0; + const noPercentage = + totalVotes > 0 ? (proposal.noVotes / totalVotes) * 100 : 0; + const abstainPercentage = + totalVotes > 0 ? (proposal.abstainVotes / totalVotes) * 100 : 0; + + const formatDate = (dateString: string) => { + if (!dateString) return "N/A"; + try { + return new Date(dateString).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + } catch { + return dateString; + } + }; + + return ( + + {/* Header */} +
+
+
+ + #{proposal.id.slice(0, 8)}... + + + {getStatusLabel(proposal.status)} + +
+

+ {proposal.title} +

+

+ {proposal.description} +

+
+
+ + {/* Voting Progress */} +
+
+ Voting Progress + {totalVotes.toLocaleString()} votes +
+ + {/* Progress bars */} +
+ {/* Yes votes */} +
+
+ Yes + + {yesPercentage.toFixed(1)}% + +
+
+
+
+
+ + {/* No votes */} +
+
+ No + + {noPercentage.toFixed(1)}% + +
+
+
+
+
+ + {/* Abstain votes */} +
+
+ Abstain + + {abstainPercentage.toFixed(1)}% + +
+
+
+
+
+
+
+ + {/* Timeline */} +
+
+ Voting Start + {formatDate(proposal.votingStartTime || "")} +
+
+ Voting End + {formatDate(proposal.votingEndTime || "")} +
+
+ + {/* Vote Buttons */} + {proposal.status === "active" && onVote && ( +
+ + + +
+ )} + + {/* Proposer info */} +
+
+ Proposed by: + + {proposal.proposer.slice(0, 6)}...{proposal.proposer.slice(-4)} + +
+
+ + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/governance/ProposalDetailsModal.tsx b/cmd/rpc/web/wallet-new/src/components/governance/ProposalDetailsModal.tsx new file mode 100644 index 000000000..f3d1f7a50 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/ProposalDetailsModal.tsx @@ -0,0 +1,286 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Proposal } from '@/hooks/useGovernance'; + +interface ProposalDetailsModalProps { + proposal: Proposal | null; + isOpen: boolean; + onClose: () => void; + onVote?: (proposalHash: string, vote: 'approve' | 'reject') => void; +} + +export const ProposalDetailsModal: React.FC = ({ + proposal, + isOpen, + onClose, + onVote +}) => { + if (!proposal) return null; + + const getCategoryColor = (category: string) => { + const colors: Record = { + 'Gov': 'bg-blue-500/20 text-blue-400 border-blue-500/40', + 'Subsidy': 'bg-orange-500/20 text-orange-400 border-orange-500/40', + 'Other': 'bg-purple-500/20 text-purple-400 border-purple-500/40' + }; + return colors[category] || colors.Other; + }; + + const getResultBadge = (result: string) => { + const colors: Record = { + 'Pass': 'bg-green-500/20 text-green-400 border border-green-500/40', + 'Fail': 'bg-red-500/20 text-red-400 border border-red-500/40', + 'Pending': 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/40' + }; + return colors[result] || colors.Pending; + }; + + const formatDate = (timestamp: string) => { + try { + return new Date(timestamp).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch { + return timestamp; + } + }; + + const formatAddress = (address: string) => { + if (address.length <= 16) return address; + return `${address.slice(0, 8)}...${address.slice(-8)}`; + }; + + return ( + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Modal */} +
+ + {/* Header */} +
+
+
+ + {proposal.category} + + + {proposal.result} + +
+

+ {proposal.title} +

+

+ Proposal ID: {proposal.hash.slice(0, 16)}... +

+
+ +
+ + {/* Content */} +
+
+ {/* Description */} +
+

+ Description +

+

+ {proposal.description} +

+
+ + {/* Voting Results */} +
+

+ Voting Results +

+ +
+
+ For: {proposal.yesPercent.toFixed(1)}% + Against: {proposal.noPercent.toFixed(1)}% +
+
+
+
+
+
+ +
+
+
+ + Votes For +
+
+ {proposal.yesPercent.toFixed(1)}% +
+
+
+
+ + Votes Against +
+
+ {proposal.noPercent.toFixed(1)}% +
+
+
+
+ + {/* Proposal Information */} +
+

+ Proposal Information +

+
+
+ Proposer + + {formatAddress(proposal.proposer)} + +
+
+ Submit Time + + {formatDate(proposal.submitTime)} + +
+
+ Start Block + + #{proposal.startHeight.toLocaleString()} + +
+
+ End Block + + #{proposal.endHeight.toLocaleString()} + +
+
+ Type + + {proposal.type || 'Unknown'} + +
+
+
+ + {/* Technical Details */} + {proposal.msg && ( +
+

+ Technical Details +

+
+
+                                                    {JSON.stringify(proposal.msg, null, 2)}
+                                                
+
+
+ )} + + {/* Transaction Details */} + {(proposal.fee || proposal.memo) && ( +
+

+ Transaction Details +

+
+ {proposal.fee && ( +
+ Fee + + {(proposal.fee / 1000000).toFixed(6)} CNPY + +
+ )} + {proposal.memo && ( +
+ Memo + + {proposal.memo} + +
+ )} +
+
+ )} +
+
+ + {/* Footer with Actions */} +
+
+ + {proposal.status === 'active' && onVote && ( + <> + + + + )} +
+
+ +
+ + )} + + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/governance/ProposalTable.tsx b/cmd/rpc/web/wallet-new/src/components/governance/ProposalTable.tsx new file mode 100644 index 000000000..96c06d280 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/ProposalTable.tsx @@ -0,0 +1,215 @@ +import React, { useState, useMemo } from 'react'; +import { Proposal } from '@/hooks/useGovernance'; + +interface ProposalTableProps { + proposals: Proposal[]; + title: string; + isPast?: boolean; + onVote?: (proposalHash: string, vote: 'approve' | 'reject') => void; + onViewDetails?: (proposalHash: string) => void; +} + +export const ProposalTable: React.FC = ({ + proposals, + title, + isPast = false, + onVote, + onViewDetails +}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [categoryFilter, setCategoryFilter] = useState('All Categories'); + + const categories = useMemo(() => { + const cats = ['All Categories', ...new Set(proposals.map(p => p.category))]; + return cats; + }, [proposals]); + + const filteredProposals = useMemo(() => { + let filtered = proposals; + + if (categoryFilter !== 'All Categories') { + filtered = filtered.filter(p => p.category === categoryFilter); + } + + if (searchTerm) { + const search = searchTerm.toLowerCase(); + filtered = filtered.filter(p => + p.title.toLowerCase().includes(search) || + p.description.toLowerCase().includes(search) || + p.hash.toLowerCase().includes(search) + ); + } + + return filtered; + }, [proposals, categoryFilter, searchTerm]); + + const getCategoryColor = (category: string) => { + const colors: Record = { + 'Gov': 'bg-blue-500/20 text-blue-400 border-blue-500/40', + 'Subsidy': 'bg-orange-500/20 text-orange-400 border-orange-500/40', + 'Other': 'bg-purple-500/20 text-purple-400 border-purple-500/40' + }; + return colors[category] || colors.Other; + }; + + const getResultBadge = (result: string) => { + const colors: Record = { + 'Pass': 'bg-green-500/20 text-green-400', + 'Fail': 'bg-red-500/20 text-red-400', + 'Pending': 'bg-yellow-500/20 text-yellow-400' + }; + return colors[result] || colors.Pending; + }; + + const formatTimeAgo = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return 'Today'; + if (diffDays === 1) return '1 day ago'; + if (diffDays < 7) return `${diffDays} days ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`; + return `${Math.floor(diffDays / 30)} months ago`; + }; + + return ( +
+ {/* Header */} +
+
+

{title}

+ {!isPast && ( +

+ Vote on proposals that shape the future of the Canopy ecosystem +

+ )} +
+
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-bg-primary border border-bg-accent rounded-lg text-sm text-text-primary placeholder-text-muted focus:outline-none focus:border-primary/40 transition-colors" + /> +
+ +
+ + {/* Table */} +
+ + + + + + + + + + + + + {filteredProposals.length === 0 ? ( + + + + ) : ( + filteredProposals.map((proposal) => ( + + {/* Proposal */} + + + {/* Category */} + + + {/* Result */} + + + {/* Turnout */} + + + {/* Ended */} + + + {/* Actions */} + + + )) + )} + +
ProposalCategoryResultTurnoutEndedActions
+ No proposals found +
+
+
+ {proposal.title} +
+
+ {proposal.description} +
+
+
+ + {proposal.category} + + + + {proposal.result} + + +
+ {proposal.yesPercent.toFixed(1)}% +
+
+
+ {isPast ? formatTimeAgo(proposal.submitTime) : `Block ${proposal.endHeight}`} +
+
+
+ {!isPast && proposal.status === 'active' && onVote && ( + <> + + + + )} + +
+
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/governance/ProposalsList.tsx b/cmd/rpc/web/wallet-new/src/components/governance/ProposalsList.tsx new file mode 100644 index 000000000..3bea2c7e0 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/ProposalsList.tsx @@ -0,0 +1,143 @@ +import React, { useState, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { Proposal } from '@/hooks/useGovernance'; +import { ProposalCard } from './ProposalCard'; + +interface ProposalsListProps { + proposals: Proposal[]; + isLoading: boolean; + onVote?: (proposalId: string, vote: 'yes' | 'no' | 'abstain') => void; +} + +type FilterStatus = 'all' | 'active' | 'passed' | 'rejected' | 'pending'; + +export const ProposalsList: React.FC = ({ + proposals, + isLoading, + onVote +}) => { + const [filter, setFilter] = useState('all'); + const [searchTerm, setSearchTerm] = useState(''); + + const filteredProposals = useMemo(() => { + let filtered = proposals; + + // Filter by status + if (filter !== 'all') { + filtered = filtered.filter(p => p.status === filter); + } + + // Filter by search term + if (searchTerm) { + const search = searchTerm.toLowerCase(); + filtered = filtered.filter(p => + p.title.toLowerCase().includes(search) || + p.description.toLowerCase().includes(search) || + p.id.toLowerCase().includes(search) || + p.hash?.toLowerCase().includes(search) + ); + } + + return filtered; + }, [proposals, filter, searchTerm]); + + const filterOptions: { value: FilterStatus; label: string; count: number }[] = [ + { value: 'all', label: 'All', count: proposals.length }, + { value: 'active', label: 'Active', count: proposals.filter(p => p.status === 'active').length }, + { value: 'passed', label: 'Passed', count: proposals.filter(p => p.status === 'passed').length }, + { value: 'rejected', label: 'Rejected', count: proposals.filter(p => p.status === 'rejected').length }, + { value: 'pending', label: 'Pending', count: proposals.filter(p => p.status === 'pending').length }, + ]; + + if (isLoading) { + return ( +
+
+
Loading proposals...
+
+
+ ); + } + + return ( +
+ {/* Header with filters */} +
+
+

+ Proposals +

+ + {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-bg-primary border border-bg-accent rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:border-primary/40 transition-colors" + /> +
+
+ + {/* Filter tabs */} +
+ {filterOptions.map((option) => ( + + ))} +
+
+ + {/* Proposals grid */} + {filteredProposals.length === 0 ? ( +
+ +

+ {searchTerm + ? 'No proposals found matching your search.' + : filter === 'all' + ? 'No proposals available.' + : `No ${filter} proposals.`} +

+
+ ) : ( + + {filteredProposals.map((proposal) => ( + + ))} + + )} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/governance/VotingPowerCard.tsx b/cmd/rpc/web/wallet-new/src/components/governance/VotingPowerCard.tsx new file mode 100644 index 000000000..a41b0dbcd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/VotingPowerCard.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useVotingPower } from '@/hooks/useGovernance'; +import AnimatedNumber from '@/components/ui/AnimatedNumber'; + +export const VotingPowerCard = () => { + const { selectedAccount } = useAccounts(); + const { data: votingPowerData, isLoading } = useVotingPower(selectedAccount?.address || ''); + + const formatVotingPower = (amount: number) => { + if (!amount && amount !== 0) return '0.00'; + return (amount / 1000000).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + }; + + return ( + + {/* Icon */} +
+ +
+ + {/* Title */} +

+ Your Voting Power +

+ + {/* Voting Power */} +
+ {isLoading ? ( +
+ ... +
+ ) : ( +
+
+ +
+ CNPY +
+ )} +
+ + {/* Additional Info */} +
+ + Based on staked amount + +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/key-management/CurrentWallet.tsx b/cmd/rpc/web/wallet-new/src/components/key-management/CurrentWallet.tsx new file mode 100644 index 000000000..38d1ce6a6 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/key-management/CurrentWallet.tsx @@ -0,0 +1,361 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { motion } from "framer-motion"; +import { + Copy, + Download, + Key, + AlertTriangle, + Shield, + Eye, + EyeOff, +} from "lucide-react"; +import { Button } from "@/components/ui/Button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/Select"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { useToast } from "@/toast/ToastContext"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useDS } from "@/core/useDs"; +import { downloadJson } from "@/helpers/download"; + +export const CurrentWallet = (): JSX.Element => { + const { accounts, selectedAccount, switchAccount } = useAccounts(); + + const [privateKey, setPrivateKey] = useState(""); + const [privateKeyVisible, setPrivateKeyVisible] = useState(false); + const [showPasswordModal, setShowPasswordModal] = useState(false); + const [password, setPassword] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [isFetchingKey, setIsFetchingKey] = useState(false); + const { copyToClipboard } = useCopyToClipboard(); + const toast = useToast(); + const dsFetch = useDSFetcher(); + const { data: keystore } = useDS("keystore", {}); + + const panelVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4 }, + }, + }; + + const selectedKeyEntry = useMemo(() => { + if (!keystore || !selectedAccount) return null; + return keystore.addressMap?.[selectedAccount.address] ?? null; + }, [keystore, selectedAccount]); + + useEffect(() => { + setPrivateKey(""); + setPrivateKeyVisible(false); + setShowPasswordModal(false); + setPassword(""); + setPasswordError(""); + }, [selectedAccount?.id]); + + const handleDownloadKeyfile = () => { + if (!selectedAccount) { + toast.error({ + title: "No Account Selected", + description: "Please select an active account first", + }); + return; + } + + if (!keystore) { + toast.error({ + title: "Keyfile Unavailable", + description: "Keystore data is not ready yet.", + }); + return; + } + + if (!selectedKeyEntry) { + toast.error({ + title: "Keyfile Unavailable", + description: "Selected wallet data is missing in the keystore.", + }); + return; + } + + const nickname = selectedKeyEntry.keyNickname || selectedAccount.nickname; + const nicknameValue = + (keystore.nicknameMap ?? {})[nickname] ?? selectedKeyEntry.keyAddress; + const keyfilePayload = { + addressMap: { + [selectedKeyEntry.keyAddress]: selectedKeyEntry, + }, + nicknameMap: { + [nickname]: nicknameValue, + }, + }; + + downloadJson(keyfilePayload, `keyfile-${nickname}`); + toast.success({ + title: "Download Started", + description: "Your keyfile JSON is downloading.", + }); + }; + + const handleRevealPrivateKeys = () => { + if (!selectedAccount) { + toast.error({ + title: "No Account Selected", + description: "Please select an active account first", + }); + return; + } + + if (privateKeyVisible) { + setPrivateKey(""); + setPrivateKeyVisible(false); + toast.success({ + title: "Private Key Hidden", + description: "Your private key is hidden again.", + icon: , + }); + return; + } + + setPassword(""); + setPasswordError(""); + setShowPasswordModal(true); + }; + + const handleFetchPrivateKey = async () => { + if (!selectedAccount) return; + if (!password) { + setPasswordError("Password is required."); + return; + } + + setIsFetchingKey(true); + setPasswordError(""); + + try { + const response = await dsFetch("keystoreGet", { + address: selectedKeyEntry?.keyAddress ?? selectedAccount.address, + password, + nickname: selectedKeyEntry?.keyNickname, + }); + const extracted = + (response as any)?.privateKey ?? + (response as any)?.private_key ?? + (response as any)?.PrivateKey ?? + (response as any)?.Private_key ?? + (typeof response === "string" ? response.replace(/"/g, "") : ""); + + if (!extracted) { + throw new Error("Private key not found."); + } + + setPrivateKey(extracted); + setPrivateKeyVisible(true); + setShowPasswordModal(false); + setPassword(""); + toast.success({ + title: "Private Key Revealed", + description: "Be careful! Your private key is now visible.", + icon: , + }); + } catch (error) { + setPasswordError("Unable to unlock with that password."); + toast.error({ + title: "Unlock Failed", + description: String(error), + }); + } finally { + setIsFetchingKey(false); + } + }; + + return ( + +
+

Current Wallet

+ +
+ +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ +
+

+ Security Warning +

+

+ Never share your private keys. Anyone with access to them can + control your funds. +

+
+
+
+
+ + {showPasswordModal && ( +
+
+

+ Unlock Private Key +

+

+ Enter your wallet password to reveal the private key. +

+ setPassword(e.target.value)} + placeholder="Password" + className="w-full bg-bg-tertiary text-white border border-bg-accent rounded-lg px-3 py-2.5" + /> + {passwordError && ( +
{passwordError}
+ )} +
+ + +
+
+
+ )} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/key-management/ImportWallet.tsx b/cmd/rpc/web/wallet-new/src/components/key-management/ImportWallet.tsx new file mode 100644 index 000000000..97a0ea063 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/key-management/ImportWallet.tsx @@ -0,0 +1,224 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { AlertTriangle } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useToast } from '@/toast/ToastContext'; + +export const ImportWallet = (): JSX.Element => { + const { createNewAccount } = useAccounts(); + const toast = useToast(); + + const [showPrivateKey, setShowPrivateKey] = useState(false); + const [activeTab, setActiveTab] = useState<'key' | 'keystore'>('key'); + const [importForm, setImportForm] = useState({ + privateKey: '', + password: '', + confirmPassword: '' + }); + + const panelVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4 } + } + }; + + const handleImportWallet = async () => { + if (!importForm.privateKey) { + toast.error({ title: 'Missing private key', description: 'Please enter a private key.' }); + return; + } + + if (!importForm.password) { + toast.error({ title: 'Missing password', description: 'Please enter a password.' }); + return; + } + + if (importForm.password !== importForm.confirmPassword) { + toast.error({ title: 'Password mismatch', description: 'Passwords do not match.' }); + return; + } + + const loadingToast = toast.info({ + title: 'Importing wallet...', + description: 'Please wait while your wallet is imported.', + sticky: true, + }); + + try { + // Here you would implement the import functionality + // For now, we'll create a new account with the provided name + await createNewAccount(importForm.password, 'Imported Wallet'); + toast.dismiss(loadingToast); + toast.success({ + title: 'Wallet imported', + description: 'Your wallet has been imported successfully.', + }); + setImportForm({ privateKey: '', password: '', confirmPassword: '' }); + } catch (error) { + toast.dismiss(loadingToast); + toast.error({ title: 'Error importing wallet', description: String(error) }); + } + }; + + return ( + +
+

Import Wallet

+
+ +
+ + +
+ + {activeTab === 'key' && ( +
+
+ +
+ setImportForm({ ...importForm, privateKey: e.target.value })} + className="w-full bg-bg-tertiary border border-bg-accent rounded-lg px-3 py-2.5 text-white pr-10 placeholder:font-mono" + /> + +
+
+ +
+ + setImportForm({ ...importForm, password: e.target.value })} + className="w-full bg-bg-tertiary border border-bg-accent rounded-lg px-3 py-2.5 text-white" + /> +
+ +
+ + setImportForm({ ...importForm, confirmPassword: e.target.value })} + className="w-full bg-bg-tertiary border border-bg-accent rounded-lg px-3 py-2.5 text-white" + /> +
+ +
+
+ +
+

Import Security Warning

+

+ Only import wallets from trusted sources. Verify all information before proceeding. +

+
+
+
+ + +
+ )} + + {activeTab === 'keystore' && ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+

Import Security Warning

+

+ Only import wallets from trusted sources. Verify all information before proceeding. +

+
+
+
+ + +
+ )} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/key-management/NewKey.tsx b/cmd/rpc/web/wallet-new/src/components/key-management/NewKey.tsx new file mode 100644 index 000000000..3b4d908ce --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/key-management/NewKey.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Button } from '@/components/ui/Button'; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useToast } from '@/toast/ToastContext'; + +export const NewKey = (): JSX.Element => { + const { createNewAccount } = useAccounts(); + const toast = useToast(); + + const [newKeyForm, setNewKeyForm] = useState({ + password: '', + walletName: '' + }); + + const panelVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4 } + } + }; + + const handleCreateWallet = async () => { + if (!newKeyForm.password) { + toast.error({ title: 'Missing password', description: 'Please enter a password.' }); + return; + } + + if (!newKeyForm.walletName) { + toast.error({ title: 'Missing wallet name', description: 'Please enter a wallet name.' }); + return; + } + + const loadingToast = toast.info({ + title: 'Creating wallet...', + description: 'Please wait while your wallet is created.', + sticky: true, + }); + + try { + await createNewAccount(newKeyForm.walletName, newKeyForm.password); + toast.dismiss(loadingToast); + toast.success({ + title: 'Wallet created', + description: `Wallet "${newKeyForm.walletName}" is ready.`, + }); + setNewKeyForm({ password: '', walletName: '' }); + } catch (error) { + toast.dismiss(loadingToast); + toast.error({ + title: 'Error creating wallet', + description: String(error), + }); + } + }; + + return ( + +
+

New Key

+
+ +
+
+
+ + setNewKeyForm({ ...newKeyForm, walletName: e.target.value })} + className="w-full bg-bg-tertiary border border-bg-accent rounded-lg px-3 py-2 text-white" + /> +
+
+ + setNewKeyForm({ ...newKeyForm, password: e.target.value })} + className="w-full bg-bg-tertiary border border-bg-accent rounded-lg px-3 py-2 text-white" + /> +
+
+ + +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/Footer.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/Footer.tsx new file mode 100644 index 000000000..3248a61d1 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Footer.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +export const Footer = (): JSX.Element => { + const containerVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + staggerChildren: 0.1 + } + } + }; + + const itemVariants = { + hidden: { opacity: 0, y: 10 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4 } + } + }; + + const linkVariants = { + hover: { + scale: 1.05, + color: "#6fe3b4", + transition: { duration: 0.2 } + } + }; + + return ( + +
+ + + Terms of Service + + + + Privacy Policy + + + + Security Guide + + + + Support + + +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/Logo.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/Logo.tsx new file mode 100644 index 000000000..777345d82 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Logo.tsx @@ -0,0 +1,36 @@ +import React from 'react' + +type LogoProps = { + size?: number + className?: string + showText?: boolean +} + +// Canopy Logo with SVG from logo.svg +const Logo: React.FC = ({ size = 100, className = '', showText = true }) => { + return ( +
+ + + + + + + + + + + + + + + {showText && ( + + Wallet + + )} +
+ ) +} + +export default Logo \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/MainLayout.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/MainLayout.tsx new file mode 100644 index 000000000..f0c9f45bd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/MainLayout.tsx @@ -0,0 +1,24 @@ +import { Outlet } from 'react-router-dom' +import { Sidebar } from "./Sidebar"; +import { TopNavbar } from "./TopNavbar"; +import { Footer } from "./Footer"; + +export default function MainLayout() { + return ( +
+ {/* Top Navbar - Desktop only (lg+) */} + + + {/* Mobile/Tablet Header + Sidebar (< lg) */} + + + {/* Main Content Area */} +
+
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/Navbar.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/Navbar.tsx new file mode 100644 index 000000000..98911d8e9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Navbar.tsx @@ -0,0 +1,383 @@ +import React, { useState } from 'react'; +import { Plus, Menu, X } from 'lucide-react'; +import {motion, AnimatePresence, Variants} from 'framer-motion'; +import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/Select"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useTotalStage } from "@/hooks/useTotalStage"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; +import Logo from './Logo'; +import { Link, NavLink } from 'react-router-dom'; + + +export const Navbar = (): JSX.Element => { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + + const { + accounts, + loading, + error: hasErrorInAccounts, + switchAccount, + selectedAccount + } = useAccounts(); + + const { data: totalStage, isLoading: stageLoading } = useTotalStage(); + + const containerVariants = { + hidden: { opacity: 0, y: -20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + staggerChildren: 0.1 + } + } + }; + + const itemVariants = { + hidden: { opacity: 0, y: -10 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4 } + } + }; + + const logoVariants = { + hidden: { scale: 0.8, opacity: 0 }, + visible: { + scale: 1, + opacity: 1, + transition: { + duration: 0.5, + type: "spring" as const, + stiffness: 200 + } + } + }; + + const mobileMenuVariants = { + closed: { + opacity: 0, + height: 0, + transition: { + duration: 0.3, + ease: "easeInOut" + } + }, + open: { + opacity: 1, + height: "auto", + transition: { + duration: 0.3, + ease: "easeInOut" + } + } + } as Variants; + + const navItems = [ + { name: 'Dashboard', path: '/' }, + { name: 'Accounts', path: '/accounts' }, + { name: 'Staking', path: '/staking' }, + { name: 'Governance', path: '/governance' }, + { name: 'Monitoring', path: '/monitoring' } + ]; + + return ( + +
+
+ {/* Logo */} + + +
+ +
+ +
+ + {/* Total Stage Portfolio - Hidden on small screens */} + + Total Tokens + + {stageLoading ? ( + '...' + ) : ( + + )} + + CNPY + + + {/* Navigation - Desktop only */} + + {navItems.map((item, index) => ( + + + `text-xs xl:text-sm font-medium transition-colors whitespace-nowrap ${isActive + ? 'text-primary border-b-2 border-primary pb-1' + : 'text-text-muted hover:text-text-primary' + }` + } + > + {item.name} + + + ))} + +
+ + + {/* Account Selector - Hidden on mobile */} + + + + + {/* Key Management Button - Responsive */} + + + + Key Mgmt + + + + {/* Hamburger Menu Button - Mobile only */} + setIsMobileMenuOpen(!isMobileMenuOpen)} + whileTap={{ scale: 0.95 }} + > + {isMobileMenuOpen ? ( + + ) : ( + + )} + + +
+ + {/* Mobile Menu */} + + {isMobileMenuOpen && ( + +
+ {/* Mobile Navigation Links */} + + + {/* Mobile Account Selector */} +
+ +
+ + {/* Mobile Key Management Button */} +
+ setIsMobileMenuOpen(false)} + className="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg px-3 py-2.5 flex items-center justify-center gap-2 transition-colors duration-200 min-h-[44px]" + > + + Key Management + +
+ + {/* Mobile Total Stage */} +
+
+
+ Total Tokens +
+ + {stageLoading ? '...' : ( + + )} + + CNPY +
+
+
+
+
+
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/Sidebar.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/Sidebar.tsx new file mode 100644 index 000000000..a7aaa6ba8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Sidebar.tsx @@ -0,0 +1,306 @@ +import React, { useState, useEffect } from 'react'; +import { NavLink, Link } from 'react-router-dom'; +import {motion, AnimatePresence, Variants} from 'framer-motion'; +import { + LayoutDashboard, + Wallet, + TrendingUp, + Vote, + Activity, + ChevronLeft, + ChevronRight, + Plus, + Menu, + X +} from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/Select"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useTotalStage } from "@/hooks/useTotalStage"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; +import Logo from './Logo'; + +interface NavItem { + name: string; + path: string; + icon: React.ElementType; +} + +const navItems: NavItem[] = [ + { name: 'Dashboard', path: '/', icon: LayoutDashboard }, + { name: 'Accounts', path: '/accounts', icon: Wallet }, + { name: 'Staking', path: '/staking', icon: TrendingUp }, + { name: 'Governance', path: '/governance', icon: Vote }, + { name: 'Monitoring', path: '/monitoring', icon: Activity } +]; + +export const Sidebar = (): JSX.Element => { + const [isCollapsed, setIsCollapsed] = useState(() => { + const saved = localStorage.getItem('sidebarCollapsed'); + return saved ? JSON.parse(saved) : false; + }); + const [isMobileOpen, setIsMobileOpen] = useState(false); + + const { + accounts, + loading, + error: hasErrorInAccounts, + switchAccount, + selectedAccount + } = useAccounts(); + + const { data: totalStage, isLoading: stageLoading } = useTotalStage(); + + useEffect(() => { + localStorage.setItem('sidebarCollapsed', JSON.stringify(isCollapsed)); + }, [isCollapsed]); + + const toggleSidebar = () => { + setIsCollapsed(!isCollapsed); + }; + + const toggleMobileSidebar = () => { + setIsMobileOpen(!isMobileOpen); + }; + + + const mobileSidebarVariants = { + open: { + x: 0, + transition: { + duration: 0.3, + ease: 'easeOut' + } + }, + closed: { + x: '-100%', + transition: { + duration: 0.3, + ease: 'easeIn' + } + } + } as Variants; + + const SidebarContent = ({ isMobile = false }: { isMobile?: boolean }) => ( + <> + {/* Logo Section */} +
+ {(!isCollapsed || isMobile) && ( + isMobile && setIsMobileOpen(false)}> +
+ +
+ + )} + {isCollapsed && !isMobile && ( + +
+ +
+ + )} + {!isMobile && ( + + {isCollapsed ? ( + + ) : ( + + )} + + )} + {isMobile && ( + + )} +
+ + {/* Collapse/Expand Button for Collapsed State */} + {isCollapsed && !isMobile && ( +
+ + + +
+ )} + + {/* Navigation */} + + + {/* Bottom Section */} +
+ {/* Total Stage */} + {(!isCollapsed || isMobile) && ( + +
+ Total Tokens +
+ + {stageLoading ? '...' : ( + + )} + + CNPY +
+
+
+ )} + + {/* Account Selector */} + {(!isCollapsed || isMobile) && ( + + )} + + {/* Key Management Button */} + isMobile && setIsMobileOpen(false)} + className={`bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg px-3 py-2.5 flex items-center gap-2 transition-colors duration-200 ${ + isCollapsed && !isMobile ? 'justify-center' : '' + }`} + > + + {(!isCollapsed || isMobile) && ( + Key Management + )} + +
+ + ); + + return ( + <> + {/* Mobile/Tablet Header - Only visible below lg */} +
+ + +
+ +
+ +
+ + {/* Mobile/Tablet Sidebar - Only visible below lg */} + + {isMobileOpen && ( + <> + {/* Backdrop */} + setIsMobileOpen(false)} + /> + {/* Sidebar */} + + + + + )} + + + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/TopNavbar.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/TopNavbar.tsx new file mode 100644 index 000000000..af5d298e8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/TopNavbar.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Link, NavLink } from 'react-router-dom'; +import { Plus } from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/Select"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useTotalStage } from "@/hooks/useTotalStage"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; +import Logo from './Logo'; + +const navItems = [ + { name: 'Dashboard', path: '/' }, + { name: 'Accounts', path: '/accounts' }, + { name: 'Staking', path: '/staking' }, + { name: 'Governance', path: '/governance' }, + { name: 'Monitoring', path: '/monitoring' } +]; + +export const TopNavbar = (): JSX.Element => { + const { + accounts, + loading, + error: hasErrorInAccounts, + switchAccount, + selectedAccount + } = useAccounts(); + + const { data: totalStage, isLoading: stageLoading } = useTotalStage(); + + return ( + +
+ {/* Left section - Logo + Navigation */} +
+ {/* Logo */} + +
+ +
+ + + {/* Navigation */} + +
+ + {/* Right section - Total Tokens + Account + Key Management */} +
+ {/* Total Tokens */} + + Total Tokens +
+ {stageLoading ? ( + '...' + ) : ( + + )} +
+ CNPY +
+ + {/* Account Selector */} + + + {/* Key Management Button */} + + + Key Management + +
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/MetricsCard.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/MetricsCard.tsx new file mode 100644 index 000000000..d83bf91ef --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/MetricsCard.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +interface MetricItem { + id: string; + label: string; + value: string | number; + type?: 'status' | 'progress' | 'text' | 'address'; + color?: string; + progress?: number; + icon?: string; +} + +interface MetricsCardProps { + title?: string; + metrics: MetricItem[]; + columns?: number; + className?: string; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } +}; + +export const MetricsCard: React.FC = ({ + title, + metrics, + columns = 3, + className = "bg-[#1E1F26] rounded-xl border border-[#2A2C35] p-4 mb-6" + }) => { + const gridCols = { + 1: 'grid-cols-1', + 2: 'grid-cols-2', + 3: 'grid-cols-3', + 4: 'grid-cols-4' + }; + + const renderMetric = (metric: MetricItem) => { + switch (metric.type) { + case 'status': + return ( +
+
+
+
{metric.label}
+
{metric.value}
+
+
+ ); + + case 'progress': + return ( +
+
{metric.label}
+
+
+
+
+ {metric.progress}% complete +
+
+ ); + + case 'address': + return ( +
+
{metric.label}
+
{metric.value}
+
+ ); + + default: + return ( +
+
{metric.label}
+
{metric.value}
+
+ ); + } + }; + + return ( + + {title && ( +

{title}

+ )} +
+ {metrics.map(renderMetric)} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/MonitoringSkeleton.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/MonitoringSkeleton.tsx new file mode 100644 index 000000000..4605d82d3 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/MonitoringSkeleton.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +export default function MonitoringSkeleton(): JSX.Element { + + return ( + +
+ {/* Node selector skeleton */} +
+
+
+
+ + {/* Node status skeleton */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Two column layout skeleton */} +
+ {/* Left column */} +
+ {/* Network peers skeleton */} +
+
+

Network Peers

+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Performance metrics skeleton */} +
+
+

Performance Metrics

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Right column */} +
+ {/* Node logs skeleton */} +
+
+

Node Logs

+
+
+
+
+
+
+
+
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+
+
+
+
+
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkPeers.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkPeers.tsx new file mode 100644 index 000000000..d8c675ba2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkPeers.tsx @@ -0,0 +1,53 @@ +import { useManifest } from '@/hooks/useManifest'; +import React from 'react'; + +interface NetworkPeersProps { + networkPeers: { + totalPeers: number; + connections: { in: number; out: number }; + peerId: string; + networkAddress: string; + publicKey: string; + peers: Array<{ + address: { + publicKey: string; + netAddress: string; + }; + isOutbound: boolean; + isValidator: boolean; + isMustConnect: boolean; + isTrusted: boolean; + reputation: number; + }>; + }; +} + +export default function NetworkPeers({ networkPeers }: NetworkPeersProps): JSX.Element { + return ( +
+

Network Peers

+
+
+
Total Peers
+
{networkPeers.totalPeers}
+
+
+
Connections
+
+ {networkPeers.connections.in} in / {networkPeers.connections.out} Out +
+
+
+
+
+
Peer ID
+
{networkPeers.peerId}
+
+
+
Network Address
+
{networkPeers.networkAddress}
+
+
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkStatsCard.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkStatsCard.tsx new file mode 100644 index 000000000..c1d550f0a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkStatsCard.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +interface NetworkStatsCardProps { + totalPeers: number; + connections: { in: number; out: number }; + peerId: string; + networkAddress: string; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } +}; + +export const NetworkStatsCard: React.FC = ({ + totalPeers, + connections, + peerId, + networkAddress + }) => { + const networkStats = [ + { + id: 'totalPeers', + label: 'Total Peers', + value: totalPeers, + color: 'text-[#6fe3b4]' + }, + { + id: 'connections', + label: 'Connections', + value: `${connections.in} in / ${connections.out} out`, + color: 'text-white' + } + ]; + + return ( + +

Network Peers

+
+ {networkStats.map((stat) => ( +
+
{stat.label}
+
{stat.value}
+
+ ))} +
+
+
+
Peer ID
+
{peerId}
+
+
+
Network Address
+
{networkAddress}
+
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/NodeLogs.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/NodeLogs.tsx new file mode 100644 index 000000000..643888eb6 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NodeLogs.tsx @@ -0,0 +1,147 @@ +import { useManifest } from '@/hooks/useManifest'; +import React, { useMemo, useCallback, useRef, useEffect } from 'react'; + +interface NodeLogsProps { + logs: string[]; + isPaused: boolean; + onPauseToggle: () => void; + onClearLogs: () => void; + onExportLogs: () => void; +} + +export default function NodeLogs({ + logs, + isPaused, + onPauseToggle, + onClearLogs, + onExportLogs + }: NodeLogsProps): JSX.Element { + const containerRef = useRef(null); + const ITEMS_PER_PAGE = 50; + const MAX_LOGS = 1000; + + const limitedLogs = useMemo(() => { + return logs.slice(-MAX_LOGS); + }, [logs]); + + const formatLogLine = useCallback((line: string) => { + const patterns = [ + [/\[90m/g, ''], + [/\[0m/g, ''], + [/\[34mDEBUG/g, 'DEBUG'], + [/\[32mINFO/g, 'INFO'], + [/\[33mWARN/g, 'WARN'], + [/\[31mERROR/g, 'ERROR'], + [/(node-\d+)/g, '$1'], + [/(PROPOSE|PROPOSE_VOTE|PRECOMMIT_VOTE)/g, '$1'], + [/(🔒|Locked on proposal)/g, '$1'], + [/(👑|Proposer is)/g, '$1'], + [/(Validating proposal from leader)/g, '$1'], + [/(Applying block)/g, '$1'], + [/(✅|is valid)/g, '$1'], + [/(VDF disabled)/g, '$1'], + [/([a-f0-9]{8,})/g, '$1'], + [/(message from proposer:)/g, '$1'], + [/(Process time|Wait time)/g, '$1'], + [/(Self sending)/g, '$1'], + [/(Sending to \d+ replicas)/g, '$1'], + [/(Adding vote from replica)/g, '$1'], + [/(Received.*message from)/g, '$1'], + [/(Committing to store)/g, '$1'], + [/(Indexing block)/g, '$1'], + [/(TryCommit block)/g, '$1'], + [/(Handling peer block)/g, '$1'], + [/(Handling block message)/g, '$1'], + [/(Gossiping certificate)/g, '$1'], + [/(Sent peer book request)/g, '$1'], + [/(Reset BFT)/g, '$1'], + [/(NEW_HEIGHT|NEW_COMMITTEE)/g, '$1'], + [/(Updating must connects)/g, '$1'], + [/(Updating root chain info)/g, '$1'], + [/(Done checking mempool)/g, '$1'], + [/(Validating mempool)/g, '$1'], + [/(🔒|Committed block)/g, '$1'], + [/(✉️|Received new block)/g, '$1'], + [/(🗳️|Self is a leader candidate)/g, '$1'], + [/(Voting.*as the proposer)/g, '$1'], + [/(No election candidates)/g, '$1'], + [/(falling back to weighted pseudorandom)/g, '$1'], + [/(Self is the proposer)/g, '$1'], + [/(Producing proposal as leader)/g, '$1'] + ]; + + let html = line; + for (const [pattern, replacement] of patterns) { + html = html.replace(pattern, replacement as string); + } + + return ; + }, []); + + const visibleLogs = useMemo(() => { + const start = Math.max(0, limitedLogs.length - ITEMS_PER_PAGE); + const end = limitedLogs.length; + return limitedLogs.slice(start, end); + }, [limitedLogs]); + + useEffect(() => { + if (containerRef.current && !isPaused) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [visibleLogs, isPaused]); + + const LogLine = React.memo(({ log, index }: { log: string; index: number }) => ( +
+ {formatLogLine(log)} +
+ )); + return ( +
+
+
+

+ Node Logs +

+

+ ({limitedLogs.length} lines, showing last {ITEMS_PER_PAGE}) +

+
+
+ + + +
+
+
+ {visibleLogs.length > 0 ? ( + visibleLogs.map((log, index) => ( + + )) + ) : ( +
No logs available
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/NodeStatus.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/NodeStatus.tsx new file mode 100644 index 000000000..7a5195e76 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NodeStatus.tsx @@ -0,0 +1,120 @@ +import React from "react"; + +interface NodeStatusProps { + nodeStatus: { + synced: boolean; + blockHeight: number; + syncProgress: number; + nodeAddress: string; + phase: string; + round: number; + networkID: number; + chainId: number; + status: string; + blockHash: string; + resultsHash: string; + proposerAddress: string; + }; + selectedNode: string; + availableNodes: Array<{ + id: string; + name: string; + address: string; + netAddress?: string; + }>; + onNodeChange: (node: string) => void; + onCopyAddress: () => void; +} + +export default function NodeStatus({ + nodeStatus, + selectedNode, + availableNodes, + onNodeChange, + onCopyAddress, +}: NodeStatusProps): JSX.Element { + const formatTruncatedAddress = (address: string) => { + return ( + address.substring(0, 8) + "..." + address.substring(address.length - 4) + ); + }; + + const currentNode = + availableNodes.find((node) => node.id === selectedNode) || + availableNodes[0]; + + return ( + <> + {/* Current node info and copy address */} +
+
+
+
+

+ {currentNode?.name || "Current Node"} +

+ {currentNode?.netAddress && ( +

+ {currentNode.netAddress} +

+ )} +
+
+ +
+ + {/* Node Status */} +
+
+
+
+
+
Sync Status
+
+ {nodeStatus.synced ? "SYNCED" : "CONNECTING"} +
+
+
+
+
Block Height
+
+ #{nodeStatus.blockHeight.toLocaleString()} +
+
+
+
Round Progress
+
+
+
+
+
+

+ {nodeStatus.syncProgress}% complete +

+
+
+
Node Address
+
+ {nodeStatus.nodeAddress + ? formatTruncatedAddress(nodeStatus.nodeAddress) + : "Connecting..."} +
+
+
+
+ + ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetrics.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetrics.tsx new file mode 100644 index 000000000..5927ab1f7 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetrics.tsx @@ -0,0 +1,86 @@ +import React from 'react'; + +interface PerformanceMetricsProps { + metrics: { + processCPU: number; + systemCPU: number; + processRAM: number; + systemRAM: number; + diskUsage: number; + networkIO: number; + totalRAM: number; + availableRAM: number; + usedRAM: number; + freeRAM: number; + totalDisk: number; + usedDisk: number; + freeDisk: number; + receivedBytes: number; + writtenBytes: number; + }; +} + +export default function PerformanceMetrics({ metrics }: PerformanceMetricsProps): JSX.Element { + const performanceData = [ + { + label: 'Process CPU', + value: metrics.processCPU.toFixed(2), + unit: '%', + percentage: Math.max(metrics.processCPU, 0.5) + }, + { + label: 'System CPU', + value: metrics.systemCPU.toFixed(2), + unit: '%', + percentage: Math.max(metrics.systemCPU, 0.5) + }, + { + label: 'Process RAM', + value: metrics.processRAM.toFixed(2), + unit: '%', + percentage: Math.min(metrics.processRAM, 100) + }, + { + label: 'System RAM', + value: metrics.systemRAM.toFixed(2), + unit: '%', + percentage: Math.min(metrics.systemRAM, 100) + }, + { + label: 'Disk Usage', + value: metrics.diskUsage.toFixed(2), + unit: '%', + percentage: Math.min(metrics.diskUsage, 100) + }, + { + label: 'Network I/O', + value: metrics.networkIO.toFixed(2), + unit: ' MB/s', + percentage: Math.min((metrics.networkIO / 10) * 100, 100) + } + ]; + + return ( +
+

Performance Metrics

+
+ {performanceData.map((metric, index) => ( +
+
{metric.label}
+
+
+ + {metric.value}{metric.unit} + +
+
+
+
+ ))} +
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetricsCard.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetricsCard.tsx new file mode 100644 index 000000000..97c57653b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetricsCard.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +interface PerformanceMetricsCardProps { + processCPU: number; + systemCPU: number; + memoryUsage: number; + diskIO: number; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } +}; + +export const PerformanceMetricsCard: React.FC = ({ + processCPU, + systemCPU, + memoryUsage, + diskIO + }) => { + const performanceMetrics = [ + { + id: 'processCPU', + label: 'Process CPU', + value: processCPU, + color: '#6fe3b4' + }, + { + id: 'systemCPU', + label: 'System CPU', + value: systemCPU, + color: '#f59e0b' + }, + { + id: 'memoryUsage', + label: 'Memory Usage', + value: memoryUsage, + color: '#ef4444' + }, + { + id: 'diskIO', + label: 'Disk I/O', + value: diskIO, + color: '#8b5cf6' + } + ]; + + const renderMetricBar = (metric: typeof performanceMetrics[0]) => ( +
+
{metric.label}
+
+
+ {metric.value.toFixed(2)}% +
+
+
+
+ ); + + return ( + +

Performance Metrics

+
+ {performanceMetrics.map(renderMetricBar)} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/RawJSON.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/RawJSON.tsx new file mode 100644 index 000000000..a530d9e9d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/RawJSON.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { useDSFetcher } from '@/core/dsFetch'; +import { useQuery } from '@tanstack/react-query'; + +interface RawJSONProps { + activeTab: 'quorum' | 'logger' | 'config' | 'peerInfo' | 'peerBook'; + onTabChange: (tab: 'quorum' | 'logger' | 'config' | 'peerInfo' | 'peerBook') => void; + onExportLogs: () => void; +} + +export default function RawJSON({ + activeTab, + onTabChange, + onExportLogs + }: RawJSONProps): JSX.Element { + const dsFetch = useDSFetcher(); + + const tabData = [ + { + id: 'quorum' as const, + label: 'Quorum', + icon: 'fa-users', + dsKey: 'admin.consensusInfo' + }, + { + id: 'logger' as const, + label: 'Logger', + icon: 'fa-list', + dsKey: 'admin.log' + }, + { + id: 'config' as const, + label: 'Config', + icon: 'fa-gear', + dsKey: 'admin.config' + }, + { + id: 'peerInfo' as const, + label: 'Peer Info', + icon: 'fa-circle-info', + dsKey: 'admin.peerInfo' + }, + { + id: 'peerBook' as const, + label: 'Peer Book', + icon: 'fa-address-book', + dsKey: 'admin.peerBook' + } + ]; + + // Fetch data for active tab + const currentTab = tabData.find(t => t.id === activeTab); + const { data: tabContentData, isLoading } = useQuery({ + queryKey: ['rawJSON', activeTab], + enabled: !!currentTab, + queryFn: async () => { + if (!currentTab) return null; + try { + return await dsFetch(currentTab.dsKey, {}); + } catch (error) { + console.error(`Error fetching ${currentTab.label}:`, error); + return null; + } + }, + refetchInterval: 10000, + staleTime: 5000 + }); + + const handleExportJSON = () => { + if (!tabContentData) return; + + const dataStr = JSON.stringify(tabContentData, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${activeTab}-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+
+

Raw JSON

+ +
+ + {/* Tab buttons */} +
+ {tabData.map((tab) => ( + + ))} +
+ + {/* JSON content */} +
+ {isLoading ? ( +
+ + Loading... +
+ ) : tabContentData ? ( +
+                        {JSON.stringify(tabContentData, null, 2)}
+                    
+ ) : ( +
+ No data available +
+ )} +
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResources.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResources.tsx new file mode 100644 index 000000000..ad6d339dd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResources.tsx @@ -0,0 +1,55 @@ +import { useManifest } from '@/hooks/useManifest'; +import React from 'react'; + +interface SystemResourcesProps { + systemResources: { + threadCount: number; + fileDescriptors: number; + maxFileDescriptors: number; + }; +} + +export default function SystemResources({ systemResources }: SystemResourcesProps): JSX.Element { + // Calculate percentage for file descriptors (using realistic max of 1024 for typical process) + const fileDescriptorPercentage = systemResources.maxFileDescriptors + ? (systemResources.fileDescriptors / systemResources.maxFileDescriptors) * 100 + : (systemResources.fileDescriptors / 1024) * 100; + + // Calculate percentage for thread count (using realistic max of 100 threads for typical process) + const threadPercentage = Math.min((systemResources.threadCount / 100) * 100, 100); + + + return ( +
+

System Resources

+
+
+
Thread Count
+
+
+ {systemResources.threadCount} threads +
+
+
+
+
+
File Descriptors
+
+
+ + {systemResources.fileDescriptors.toLocaleString()} / {systemResources.maxFileDescriptors ? systemResources.maxFileDescriptors.toLocaleString() : '1,024'} + +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResourcesCard.tsx b/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResourcesCard.tsx new file mode 100644 index 000000000..1415a9974 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResourcesCard.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +interface SystemResourcesCardProps { + threadCount: number; + memoryUsage: number; + diskUsage: number; + networkLatency: number; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } +}; + +export const SystemResourcesCard: React.FC = ({ + threadCount, + memoryUsage, + diskUsage, + networkLatency + }) => { + const systemStats = [ + { + id: 'threadCount', + label: 'Thread Count', + value: threadCount, + icon: 'fa-solid fa-microchip' + }, + { + id: 'memoryUsage', + label: 'Memory Usage', + value: `${memoryUsage}%`, + icon: 'fa-solid fa-memory' + }, + { + id: 'diskUsage', + label: 'Disk Usage', + value: `${diskUsage}%`, + icon: 'fa-solid fa-hard-drive' + }, + { + id: 'networkLatency', + label: 'Network Latency', + value: `${networkLatency}ms`, + icon: 'fa-solid fa-network-wired' + } + ]; + + return ( + +

System Resources

+
+ {systemStats.map((stat) => ( +
+
+ +
+
+
{stat.label}
+
{stat.value}
+
+
+ ))} +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/monitoring/index.ts b/cmd/rpc/web/wallet-new/src/components/monitoring/index.ts new file mode 100644 index 000000000..1d7f8a42c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/index.ts @@ -0,0 +1,4 @@ +export { MetricsCard } from './MetricsCard'; +export { NetworkStatsCard } from './NetworkStatsCard'; +export { SystemResourcesCard } from './SystemResourcesCard'; +export { PerformanceMetricsCard } from './PerformanceMetricsCard'; diff --git a/cmd/rpc/web/wallet-new/src/components/staking/StatsCards.tsx b/cmd/rpc/web/wallet-new/src/components/staking/StatsCards.tsx new file mode 100644 index 000000000..8e0270892 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/StatsCards.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { useStakedBalanceHistory } from '@/hooks/useStakedBalanceHistory'; + +interface StatsCardsProps { + totalStaked: number; + totalRewards: number; + validatorsCount: number; + chainCount: number; + activeValidatorsCount: number; +} + +const formatStakedAmount = (amount: number) => { + if (!amount && amount !== 0) return '0.00'; + return (amount / 1000000).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +}; + +const formatRewards = (amount: number) => { + if (!amount && amount !== 0) return '+0.00'; + return `+${(amount / 1000000).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; +}; + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } +}; + +export const StatsCards: React.FC = ({ + totalStaked, + totalRewards, + validatorsCount, + chainCount, + activeValidatorsCount + }) => { + const { data: stakedHistory, isLoading: stakedHistoryLoading } = useStakedBalanceHistory(); + const stakedChangePercentage = stakedHistory?.changePercentage || 0; + + const statsData = [ + { + id: 'totalStaked', + title: 'Total Staked', + value: `${formatStakedAmount(totalStaked)} CNPY`, + subtitle: stakedHistoryLoading ? ( + 'Loading 24h change...' + ) : stakedHistory ? ( + = 0 ? 'text-primary' : 'text-status-error'}`}> + + + + {stakedChangePercentage >= 0 ? '+' : ''}{stakedChangePercentage.toFixed(1)}% 24h change + + ) : ( + `Across ${validatorsCount} validators` + ), + icon: 'fa-solid fa-coins', + iconColor: 'text-primary', + valueColor: 'text-white' + }, + { + id: 'rewardsEarned', + title: 'Rewards Earned', + value: `${formatRewards(totalRewards)} CNPY`, + subtitle: 'Last 24 hours', + icon: 'fa-solid fa-ellipsis', + iconColor: 'text-text-muted', + valueColor: 'text-primary', + hasButton: true + }, + { + id: 'activeValidators', + title: 'Active Validators', + value: validatorsCount.toString(), + subtitle: ( + + + {'All online'} + + ), + icon: 'fa-solid fa-shield-halved', + iconColor: 'text-text-secondary', + valueColor: 'text-white' + }, + { + id: 'chainsStaked', + title: 'Chains Staked', + value: (chainCount || 0).toString(), + icon: 'fa-solid fa-link', + iconColor: 'text-text-secondary', + valueColor: 'text-white' + } + ]; + + return ( +
+ {statsData.map((stat) => ( + +
+

+ {stat.title} +

+ {stat.hasButton ? ( + + ) : ( + + )} +
+

+ {stat.value} +

+
+ {stat.subtitle} +
+
+ ))} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/staking/Toolbar.tsx b/cmd/rpc/web/wallet-new/src/components/staking/Toolbar.tsx new file mode 100644 index 000000000..d91269fbe --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/Toolbar.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Download, Filter, Plus } from "lucide-react"; + +interface ToolbarProps { + searchTerm: string; + onSearchChange: (value: string) => void; + onAddStake: () => void; + onExportCSV: () => void; + activeValidatorsCount: number; +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }, +}; + +export const Toolbar: React.FC = ({ + searchTerm, + onSearchChange, + onAddStake, + onExportCSV, + activeValidatorsCount, +}) => { + return ( + + {/* Title section */} +
+

+ All Validators + + {activeValidatorsCount} active + +

+
+ + {/* Controls section - responsive grid */} +
+ {/* Search bar - grows to take available space */} +
+ onSearchChange(e.target.value)} + className="w-full bg-bg-secondary border border-gray-600 rounded-lg pl-10 pr-4 py-2 text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50" + /> + +
+ + {/* Action buttons - group together */} +
+ {/* Filter button */} + + + {/* Add Stake button */} + + + {/* Export CSV button */} + +
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/staking/ValidatorCard.tsx b/cmd/rpc/web/wallet-new/src/components/staking/ValidatorCard.tsx new file mode 100644 index 000000000..3021037d8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/ValidatorCard.tsx @@ -0,0 +1,224 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { useManifest } from "@/hooks/useManifest"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { useValidatorRewardsHistory } from "@/hooks/useValidatorRewardsHistory"; +import { useActionModal } from "@/app/providers/ActionModalProvider"; +import {LockOpen, Pause, Pen, Play} from "lucide-react"; + +interface ValidatorCardProps { + validator: { + address: string; + nickname?: string; + stakedAmount: number; + status: "Staked" | "Paused" | "Unstaking"; + rewards24h: number; + committees?: string[]; + isSynced: boolean; + }; + index: number; +} + +const formatStakedAmount = (amount: number) => { + if (!amount && amount !== 0) return "0.00"; + return (amount / 1000000).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +}; + +const formatRewards = (amount: number) => { + if (!amount && amount !== 0) return "+0.00"; + return `+${(amount / 1000000).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; +}; + +const truncateAddress = (address: string) => + `${address.substring(0, 4)}…${address.substring(address.length - 4)}`; + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }, +}; + +export const ValidatorCard: React.FC = ({ + validator, + index, +}) => { + const { copyToClipboard } = useCopyToClipboard(); + const { openAction } = useActionModal(); + + // Fetch real rewards data using block height comparison + const { data: rewardsHistory, isLoading: rewardsLoading } = + useValidatorRewardsHistory(validator.address); + + const handlePauseUnpause = () => { + const actionId = + validator.status === "Staked" ? "pauseValidator" : "unpauseValidator"; + openAction(actionId, { + prefilledData: { + validatorAddress: validator.address, + }, + }); + }; + + const handleEditStake = () => { + openAction("stake", { + prefilledData: { + operator: validator.address, + selectCommittees: validator.committees || [], + }, + }); + }; + + const handleUnstake = () => { + openAction("unstake", { + prefilledData: { + validatorAddress: validator.address, + }, + }); + }; + + return ( + +
+ {/* Grid layout for responsive design */} +
+ {/* Validator identity - takes 3 columns on large screens */} +
+
+
+ + {validator.nickname || `Node ${index + 1}`} + + +
+
+ {truncateAddress(validator.address)} +
+ + + {/* Chain badges */} +
+ {(validator.committees || []).slice(0, 2).map((chain, i) => ( + + {chain} + + ))} + {(validator.committees || []).length > 2 && ( + + +{(validator.committees || []).length - 2} more + + )} +
+
+
+ + {/* Stats section - responsive grid */} +
+ {/* Total Staked */} +
+
+ {formatStakedAmount(validator.stakedAmount)} CNPY +
+
Total Staked
+
+ + {/* 24h Rewards */} +
+
+ {rewardsLoading + ? "..." + : formatRewards(rewardsHistory?.change24h || 0)} +
+
24h Rewards
+
+
+ + {/* Status and Actions - takes 3 columns on large screens */} +
+ {/* Status badges */} +
+ + {validator.status} + + +
+ + {/* Action buttons */} + {validator.status !== "Unstaking" && ( +
+ + + + +
+ )} +
+
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/staking/ValidatorList.tsx b/cmd/rpc/web/wallet-new/src/components/staking/ValidatorList.tsx new file mode 100644 index 000000000..610d4418f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/ValidatorList.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {motion} from 'framer-motion'; +import {ValidatorCard} from './ValidatorCard'; + +interface Validator { + address: string; + nickname?: string; + stakedAmount: number; + status: 'Staked' | 'Paused' | 'Unstaking'; + rewards24h: number; + chains?: string[]; + isSynced: boolean; +} + +interface ValidatorListProps { + validators: Validator[]; +} + +const itemVariants = { + hidden: {opacity: 0, y: 20}, + visible: {opacity: 1, y: 0, transition: {duration: 0.4}} +}; + +export const ValidatorList: React.FC = ({ validators }) => { + + if (validators.length === 0) { + return ( + +
+ {'No validators found'} +
+
+ ); + } + + return ( +
+ {validators.map((validator, index) => ( + + ))} +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/AlertModal.tsx b/cmd/rpc/web/wallet-new/src/components/ui/AlertModal.tsx new file mode 100644 index 000000000..4e94979b2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/AlertModal.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface AlertModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + message: string; + type: 'success' | 'error' | 'warning' | 'info'; + confirmText?: string; + onConfirm?: () => void; + showCancel?: boolean; + cancelText?: string; +} + +export const AlertModal: React.FC = ({ + isOpen, + onClose, + title, + message, + type, + confirmText = 'OK', + onConfirm, + showCancel = false, + cancelText = 'Cancel' +}) => { + const getTypeStyles = () => { + switch (type) { + case 'success': + return { + icon: 'fa-solid fa-check-circle', + iconColor: 'text-green-400', + iconBg: 'bg-green-500/20', + buttonColor: 'bg-green-500 hover:bg-green-600', + borderColor: 'border-green-500/30' + }; + case 'error': + return { + icon: 'fa-solid fa-exclamation-circle', + iconColor: 'text-red-400', + iconBg: 'bg-red-500/20', + buttonColor: 'bg-red-500 hover:bg-red-600', + borderColor: 'border-red-500/30' + }; + case 'warning': + return { + icon: 'fa-solid fa-exclamation-triangle', + iconColor: 'text-yellow-400', + iconBg: 'bg-yellow-500/20', + buttonColor: 'bg-yellow-500 hover:bg-yellow-600', + borderColor: 'border-yellow-500/30' + }; + case 'info': + return { + icon: 'fa-solid fa-info-circle', + iconColor: 'text-blue-400', + iconBg: 'bg-blue-500/20', + buttonColor: 'bg-blue-500 hover:bg-blue-600', + borderColor: 'border-blue-500/30' + }; + default: + return { + icon: 'fa-solid fa-info-circle', + iconColor: 'text-blue-400', + iconBg: 'bg-blue-500/20', + buttonColor: 'bg-blue-500 hover:bg-blue-600', + borderColor: 'border-blue-500/30' + }; + } + }; + + const styles = getTypeStyles(); + + const handleConfirm = () => { + if (onConfirm) { + onConfirm(); + } + onClose(); + }; + + if (!isOpen) return null; + + return ( + + + e.stopPropagation()} + > +
+
+ +
+
+

{title}

+
+
+ +
+

{message}

+
+ +
+ {showCancel && ( + + )} + +
+
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/AnimatedNumber.tsx b/cmd/rpc/web/wallet-new/src/components/ui/AnimatedNumber.tsx new file mode 100644 index 000000000..e9208419c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/AnimatedNumber.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import NumberFlow from '@number-flow/react' + +interface AnimatedNumberProps { + value: number + format?: { + notation?: 'standard' | 'compact' + maximumFractionDigits?: number + minimumFractionDigits?: number + } + locales?: Intl.LocalesArgument + prefix?: string + suffix?: string + className?: string + trend?: number | ((oldValue: number, value: number) => number) + animated?: boolean + respectMotionPreference?: boolean +} + +const AnimatedNumber: React.FC = ({ + value, + format, + locales = 'en-US', + prefix, + suffix, + className = '', + trend, + animated = true, + respectMotionPreference = true, +}) => { + // Ensure value is a valid number + const numericValue = typeof value === 'number' && !isNaN(value) ? value : 0; + + return ( + + ) +} + +export default AnimatedNumber diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Badge.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Badge.tsx new file mode 100644 index 000000000..aba55e812 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Badge.tsx @@ -0,0 +1,34 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import {cx} from "@/ui/cx"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Button.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Button.tsx new file mode 100644 index 000000000..759ad492c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Button.tsx @@ -0,0 +1,58 @@ +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; +import {cx} from "@/ui/cx"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90 rounded-md", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 rounded-md", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground rounded-md", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + + + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Card.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Card.tsx new file mode 100644 index 000000000..fe3e90be2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Card.tsx @@ -0,0 +1,82 @@ +import * as React from "react"; +import {cx} from "@/ui/cx"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/ConfirmModal.tsx b/cmd/rpc/web/wallet-new/src/components/ui/ConfirmModal.tsx new file mode 100644 index 000000000..019616aed --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/ConfirmModal.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface ConfirmModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + type?: 'warning' | 'danger' | 'info'; +} + +export const ConfirmModal: React.FC = ({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = 'Confirm', + cancelText = 'Cancel', + type = 'warning' +}) => { + const getTypeStyles = () => { + switch (type) { + case 'danger': + return { + icon: 'fa-solid fa-exclamation-triangle', + iconColor: 'text-red-400', + iconBg: 'bg-red-500/20', + buttonColor: 'bg-red-500 hover:bg-red-600', + borderColor: 'border-red-500/30' + }; + case 'warning': + return { + icon: 'fa-solid fa-exclamation-triangle', + iconColor: 'text-yellow-400', + iconBg: 'bg-yellow-500/20', + buttonColor: 'bg-yellow-500 hover:bg-yellow-600', + borderColor: 'border-yellow-500/30' + }; + case 'info': + return { + icon: 'fa-solid fa-info-circle', + iconColor: 'text-blue-400', + iconBg: 'bg-blue-500/20', + buttonColor: 'bg-blue-500 hover:bg-blue-600', + borderColor: 'border-blue-500/30' + }; + default: + return { + icon: 'fa-solid fa-exclamation-triangle', + iconColor: 'text-yellow-400', + iconBg: 'bg-yellow-500/20', + buttonColor: 'bg-yellow-500 hover:bg-yellow-600', + borderColor: 'border-yellow-500/30' + }; + } + }; + + const styles = getTypeStyles(); + + const handleConfirm = () => { + onConfirm(); + onClose(); + }; + + if (!isOpen) return null; + + return ( + + + e.stopPropagation()} + > +
+
+ +
+
+

{title}

+
+
+ +
+

{message}

+
+ +
+ + +
+
+
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/LucideIcon.tsx b/cmd/rpc/web/wallet-new/src/components/ui/LucideIcon.tsx new file mode 100644 index 000000000..b180526b9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/LucideIcon.tsx @@ -0,0 +1,47 @@ +import React, { Suspense } from 'react'; +import dynamicIconImports from 'lucide-react/dynamicIconImports'; + +type Props = { name?: string; className?: string }; +type Importer = () => Promise<{ default: React.ComponentType }>; +const LIB = dynamicIconImports as Record; + +const normalize = (n?: string) => { + if (!n) return 'help-circle'; + return n + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // separa mayúsculas con "-" + .replace(/[_\s]+/g, '-') // convierte espacios o guiones bajos en "-" + .toLowerCase() + .trim(); +}; + +const FALLBACKS = ['HelpCircle', 'Zap', 'Circle', 'Square']; // keys que existen en casi todas las versiones + +const cache = new Map>>(); + +export function LucideIcon({ name = 'HelpCircle', className }: Props) { + const key = normalize(name); + + const resolvedName = + (LIB[key] && key) || + FALLBACKS.find(k => !!LIB[k]) || + Object.keys(LIB)[0]; + + + const importer = resolvedName ? LIB[resolvedName] : undefined; + + if (!importer || typeof importer !== 'function') { + return ; + } + + let Icon = cache.get(resolvedName); + if (!Icon) { + Icon = React.lazy(importer); + cache.set(resolvedName, Icon); + } + + return ( + }> + + + ); +} diff --git a/cmd/rpc/web/wallet-new/src/components/ui/PauseUnpauseModal.tsx b/cmd/rpc/web/wallet-new/src/components/ui/PauseUnpauseModal.tsx new file mode 100644 index 000000000..8fe58af26 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/PauseUnpauseModal.tsx @@ -0,0 +1,499 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { AlertModal } from "./AlertModal"; + +interface PauseUnpauseModalProps { + isOpen: boolean; + onClose: () => void; + validatorAddress: string; + validatorNickname?: string; + action: "pause" | "unpause"; + allValidators?: Array<{ + address: string; + nickname?: string; + }>; + isBulkAction?: boolean; +} + +export const PauseUnpauseModal: React.FC = ({ + isOpen, + onClose, + validatorAddress, + validatorNickname, + action, + allValidators = [], + isBulkAction = false, +}) => { + const { accounts } = useAccounts(); + const { chain } = useConfig(); + const [formData, setFormData] = useState({ + account: validatorNickname || accounts[0]?.nickname || "", + signer: validatorNickname || accounts[0]?.nickname || "", + memo: "", + fee: 0.01, + password: "", + }); + + // Update form data when validator changes + React.useEffect(() => { + if (validatorNickname) { + setFormData((prev) => ({ + ...prev, + account: validatorNickname, + signer: validatorNickname, + })); + } + }, [validatorNickname]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [selectedValidators, setSelectedValidators] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [alertModal, setAlertModal] = useState<{ + isOpen: boolean; + title: string; + message: string; + type: "success" | "error" | "warning" | "info"; + }>({ + isOpen: false, + title: "", + message: "", + type: "info", + }); + + const handleInputChange = (field: string, value: string | number) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleValidatorSelect = (validatorAddress: string) => { + setSelectedValidators((prev) => { + if (prev.includes(validatorAddress)) { + return prev.filter((addr) => addr !== validatorAddress); + } else { + return [...prev, validatorAddress]; + } + }); + }; + + const handleSelectAll = () => { + if (selectAll) { + setSelectedValidators([]); + setSelectAll(false); + } else { + const allAddresses = sortedValidators.map((v) => v.address); + setSelectedValidators(allAddresses); + setSelectAll(true); + } + }; + + // Sort validators by node number + const sortedValidators = React.useMemo(() => { + if (!allValidators || allValidators.length === 0) return []; + + return [...allValidators].sort((a, b) => { + // Extract node number from nickname (e.g., "node_1" -> 1, "node_2" -> 2) + const getNodeNumber = (validator: any) => { + const nickname = validator.nickname || ""; + const match = nickname.match(/node_(\d+)/); + return match ? parseInt(match[1]) : 999; // Put nodes without numbers at the end + }; + + return getNodeNumber(a) - getNodeNumber(b); + }); + }, [allValidators]); + + // Initialize selected validators when modal opens + React.useEffect(() => { + if (isBulkAction && sortedValidators.length > 0) { + setSelectedValidators(sortedValidators.map((v) => v.address)); + setSelectAll(true); + } else { + setSelectedValidators([validatorAddress]); + setSelectAll(false); + } + }, [isBulkAction, sortedValidators, validatorAddress]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(null); + + try { + // Find the account by nickname + const account = accounts.find( + (acc: any) => acc.nickname === formData.account, + ); + const signer = accounts.find( + (acc: any) => acc.nickname === formData.signer, + ); + + if (!account || !signer) { + setAlertModal({ + isOpen: true, + title: "Account Not Found", + message: + "The selected account or signer was not found. Please check your selection.", + type: "error", + }); + return; + } + + if (selectedValidators.length === 0) { + setAlertModal({ + isOpen: true, + title: "No Validators Selected", + message: "Please select at least one validator to proceed.", + type: "warning", + }); + return; + } + + const feeInMicroUnits = formData.fee * 1000000; // Convert to micro-units + + // Process each selected validator + const promises = selectedValidators.map(async (validatorAddr) => { + // Note: These transaction endpoints would need to be added to chain.json DS config + // For now, using direct admin endpoint calls with DS pattern structure + const txEndpoint = action === "pause" ? "tx-pause" : "tx-unpause"; + + try { + // This would ideally use DS pattern once tx endpoints are added to chain.json + const response = await fetch( + `${chain?.rpc?.admin}/v1/admin/${txEndpoint}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: validatorAddr, + pubKey: "", + netAddress: "", + committees: "", + amount: 0, + delegate: false, + earlyWithdrawal: false, + output: "", + signer: signer.address, + memo: formData.memo, + fee: feeInMicroUnits, + submit: true, + password: formData.password, + }), + }, + ); + + if (!response.ok) { + throw new Error(`Transaction failed: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`Error executing ${action} transaction:`, error); + throw error; + } + }); + + await Promise.all(promises); + + setSuccess(true); + setTimeout(() => { + onClose(); + setSuccess(false); + setFormData({ + account: validatorNickname || accounts[0]?.nickname || "", + signer: validatorNickname || accounts[0]?.nickname || "", + memo: "", + fee: 0.01, + password: "", + }); + setSelectedValidators([]); + setSelectAll(false); + }, 2000); + } catch (err) { + setAlertModal({ + isOpen: true, + title: "Transaction Failed", + message: + err instanceof Error + ? err.message + : "An unexpected error occurred while processing the transaction.", + type: "error", + }); + } finally { + setIsLoading(false); + } + }; + + if (!isOpen) return null; + + return ( + + + e.stopPropagation()} + > +
+

+ {action} Validator +

+ +
+ + {success ? ( + +
+ +
+

+ Transaction Successful! +

+

+ Validator {action}d successfully +

+
+ ) : ( +
+ {/* Validator Selection */} + {isBulkAction && sortedValidators.length > 0 && ( +
+
+ + + {selectedValidators.length} of {sortedValidators.length}{" "} + selected + +
+ + {/* Simple Select All */} +
+ +
+ + {/* Simple Validator List */} +
+ {sortedValidators.map((validator) => { + const matchingAccount = accounts?.find( + (acc: any) => acc.address === validator.address, + ); + const displayName = + matchingAccount?.nickname || + validator.nickname || + `Node ${validator.address.substring(0, 8)}`; + const isSelected = selectedValidators.includes( + validator.address, + ); + + return ( + + ); + })} +
+
+ )} + + {/* Form Fields */} +
+ {/* Account */} +
+ + +
+ + {/* Signer */} +
+ + +
+
+ + {/* Memo */} +
+ + handleInputChange("memo", e.target.value)} + placeholder="Optional note attached with the transaction" + className="w-full px-3 py-2 bg-bg-tertiary border border-bg-accent rounded-lg text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors" + maxLength={200} + /> +

+ {formData.memo.length}/200 characters +

+
+ + {/* Transaction Fee */} +
+ +
+ + handleInputChange("fee", parseFloat(e.target.value) || 0) + } + step="0.001" + min="0" + className="w-full px-3 py-2 pr-12 bg-bg-tertiary border border-bg-accent rounded-lg text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors" + required + /> +
+ + CNPY + +
+
+

+ Recommended: 0.01 CNPY +

+
+ + {/* Password */} +
+ + + handleInputChange("password", e.target.value) + } + placeholder="Enter your key password" + className="w-full px-3 py-2 bg-bg-tertiary border border-bg-accent rounded-lg text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors" + required + /> +
+ +
+ +
+
+ )} +
+
+ + {/* Alert Modal */} + setAlertModal((prev) => ({ ...prev, isOpen: false }))} + title={alertModal.title} + message={alertModal.message} + type={alertModal.type} + /> +
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Select.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Select.tsx new file mode 100644 index 000000000..d5e4dbe01 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Select.tsx @@ -0,0 +1,158 @@ +"use client"; + +import * as SelectPrimitive from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import * as React from "react"; +import { cx } from "@/ui/cx"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/cmd/rpc/web/wallet-new/src/core/actionForm.ts b/cmd/rpc/web/wallet-new/src/core/actionForm.ts new file mode 100644 index 000000000..c80731455 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/actionForm.ts @@ -0,0 +1,131 @@ +import { template } from "@/core/templater"; +import type { Action, Field } from "@/manifest/types"; + +/** Get fields from manifest */ +export const getFieldsFromAction = (action?: Action): Field[] => + Array.isArray(action?.form?.fields) ? (action!.form!.fields as Field[]) : []; + +/** Hints for field names */ +const NUMERIC_HINTS = new Set([ + "amount", + "receiveAmount", + "fee", + "gas", + "gasPrice", +]); +const BOOL_HINTS = new Set(["delegate", "earlyWithdrawal", "submit"]); + +/** Normalize form according to Fields + hints: + * - number: convert "1,234.56" to 1234.56 + * - boolean (by name): 'true'/'false' to boolean + */ +export function normalizeFormForAction( + action: Action | undefined, + form: Record, +) { + const out: Record = { ...form }; + const fields = (action?.form?.fields ?? []) as Field[]; + + const asNum = (v: any) => { + if (v === "" || v == null) return v; + const s = String(v).replace(/,/g, ""); + const n = Number(s); + return Number.isNaN(n) ? v : n; + }; + const asBool = (v: any) => + v === true || v === "true" || v === 1 || v === "1" || v === "on"; + + for (const f of fields) { + const n = f?.name; + if (n == null || !(n in out)) continue; + + // por tipo + if (f.type === "amount" || NUMERIC_HINTS.has(n)) out[n] = asNum(out[n]); + // por “hint” de nombre (p.ej. select true/false) + if (BOOL_HINTS.has(n)) out[n] = asBool(out[n]); + } + return out; +} + +export type BuildPayloadCtx = { + form: Record; + chain?: any; + session?: { password?: string }; + account?: any; + fees?: { raw?: any; amount?: number | string; denom?: string }; + extra?: Record; +}; + +export function buildPayloadFromAction(action: Action, ctx: any) { + const result: Record = {}; + + for (const [key, val] of Object.entries(action.payload || {})) { + // caso 1: simple string => resolver plantilla + if (typeof val === "string") { + result[key] = template(val, ctx); + continue; + } + + if (typeof val === "object" && val?.value !== undefined) { + let resolved: any = template(val?.value, ctx); + + if (val?.coerce) { + switch (val.coerce) { + case "number": + //@ts-ignore + resolved = Number(resolved); + break; + case "string": + resolved = String(resolved); + break; + case "boolean": + const resolvedStr = String(resolved).toLowerCase(); + resolved = resolvedStr === "true" || resolvedStr === "1"; + break; + } + } + + result[key] = resolved; + continue; + } + // fallback + result[key] = val; + } + + return result; +} + +export function buildConfirmSummary( + action: Action | undefined, + data: { + form: Record; + chain?: any; + fees?: { effective?: number | string }; + }, +) { + const items = action?.confirm?.summary ?? []; + return items.map((s) => ({ label: s.label, value: template(s.value, data) })); +} + +export function selectQuickActions( + actions: Action[] | undefined, + chain: any, + max?: number, +) { + const limit = max ?? 8; + const hasFeature = (a: Action) => !a.requiresFeature; + const rank = (a: Action) => + typeof a.priority === "number" + ? a.priority + : typeof a.order === "number" + ? a.order + : 0; + + return (actions ?? []) + .filter( + (a) => !a.hidden && Array.isArray(a.tags) && a.tags.includes("quick"), + ) + .filter(hasFeature) + .sort((a, b) => rank(b) - rank(a)) + .slice(0, limit); +} diff --git a/cmd/rpc/web/wallet-new/src/core/address.ts b/cmd/rpc/web/wallet-new/src/core/address.ts new file mode 100644 index 000000000..1e46edee8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/address.ts @@ -0,0 +1,8 @@ +import {isAddress, getAddress} from 'viem' + +export function normalizeEvmAddress(input: string) { + if (!input) return {ok: false as const, value: '', reason: 'empty'}; + const s = input.startsWith('0x') ? input : `0x${input}`; + const ok = isAddress(s, {strict: false}); + return ok ? {ok: true as const, value: getAddress(s)} : {ok: false as const, value: '', reason: 'invalid-evm'} +} diff --git a/cmd/rpc/web/wallet-new/src/core/dsCore.ts b/cmd/rpc/web/wallet-new/src/core/dsCore.ts new file mode 100644 index 000000000..107e69fac --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/dsCore.ts @@ -0,0 +1,210 @@ +export type Source = { + base: 'rpc' | 'admin' + path: string + method?: 'GET' | 'POST' + headers?: Record + /** 'text' => body crudo (string). 'json' (default) => JSON.stringify(body). */ + encoding?: 'json' | 'text' +} +export type CoerceSpec = Record + +export type DsLeaf = { + source: Source + body?: any + selector?: string + cache?: { staleTimeMs?: number; refetchIntervalMs?: number } + coerce?: { + ctx?: CoerceSpec + body?: CoerceSpec + /** "" = root */ + response?: CoerceSpec + } + page?: { + strategy: 'page'|'cursor' + param?: { page?: string; perPage?: string; cursor?: string; limit?: string } + response?: { items?: string; totalPages?: string; nextPage?: string; nextCursor?: string } + defaults?: { perPage?: number; startPage?: number; limit?: number } + } +} +export type DsNode = DsLeaf | Record +export type ChainLike = any + +export const getAt = (o: any, p?: string) => (!p ? o : p.split('.').reduce((a,k)=>a?.[k], o)) + +// Import the main templating system +import { resolveTemplatesDeep } from './templater' + +// Use the main templating system instead of custom implementation +export const renderDeep = resolveTemplatesDeep + +export const coerceValue = (v: any, t: string) => { + switch (t) { + case 'number': + case 'float': { + if (v === '' || v == null) return v + const n = Number(String(v).replace(/,/g,'')); return Number.isNaN(n) ? v : n + } + case 'int': { + if (v === '' || v == null) return v + const n = parseInt(String(v).replace(/,/g,''), 10); return Number.isNaN(n) ? v : n + } + case 'boolean': return v === true || v === 'true' || v === 1 || v === '1' || v === 'on' + case 'null': return null + case 'string': + default: return v == null ? v : String(v) + } +} + +export const applyCoerce = (obj: any, spec?: Record) => { + if (!spec) return obj + const mutate = (target: any, path: string, type: string) => { + if (path === '' || path == null) return coerceValue(target, type) + if (typeof target !== 'object' || target == null) return target + const parts = path.split('.'); const last = parts.pop()! + const parent = parts.reduce((o,k)=> (o && typeof o==='object') ? o[k] : undefined, target) + if (parent && Object.prototype.hasOwnProperty.call(parent, last)) parent[last] = coerceValue(parent[last], type) + return target + } + let out = (typeof obj === 'object' && obj !== null) ? structuredClone(obj) : obj + for (const [p,t] of Object.entries(spec)) out = mutate(out, p, t) + return out +} + +export const hasDsKey = (chain: any, key: string) => { + const read = (root: any) => key.split('.').reduce((a, k) => a?.[k], root) + return Boolean(read(chain?.ds) ?? read(chain?.metrics)) +} + + +/* ---------------- resolver & URL ---------------- */ +export function resolveLeaf(chain: ChainLike, key: string): DsLeaf | null { + const read = (root:any) => key.split('.').reduce((a,k)=>a?.[k], root) + const node: DsNode | undefined = read(chain?.ds) ?? read(chain?.metrics) + return (node && (node as any).source) ? (node as DsLeaf) : null +} + +export function makeUrl(chain: ChainLike, leaf: DsLeaf): string { + const base = leaf.source.base === 'admin' ? chain?.rpc?.admin : chain?.rpc?.base + return base && leaf.source.path ? `${base}${leaf.source.path}` : '' +} + +/* ---------------- request/response ---------------- */ +export type BuiltRequest = { + url: string + init: RequestInit + debug: { tplCtx: any; rendered?: any; coerced?: any } +} + +export function buildRequest(chain: ChainLike, leaf: DsLeaf, ctx?: Record): BuiltRequest { + const method = leaf.source.method ?? (leaf.body ? 'POST' : 'GET') + const headers: Record = { accept: 'application/json', ...(leaf.source.headers ?? {}) } + + const tplCtxRaw = { ...(ctx ?? {}), chain } + const tplCtx = leaf?.coerce?.ctx ? applyCoerce(tplCtxRaw, leaf.coerce.ctx) : tplCtxRaw + + let body: any = undefined + let rendered: any = undefined + let coerced: any = undefined + + if (method !== 'GET' && 'body' in leaf) { + rendered = renderDeep(leaf.body, tplCtx) + coerced = applyCoerce(rendered, leaf.coerce?.body) + + headers['content-type'] = headers['content-type'] ?? 'application/json' + body = leaf.source.encoding === 'text' + ? (typeof coerced === 'string' ? coerced : JSON.stringify(coerced)) + : JSON.stringify(coerced) + } + + const url = makeUrl(chain, leaf) + return { url, init: { method, headers, body }, debug: { tplCtx, rendered, coerced } } +} + +const looksLikeJson = (s: string) => typeof s === 'string' && /^\s*[{\[]/.test(s) +const tryParseOnce = (s: string) => { try { return JSON.parse(s) } catch { return s } } + +/** Normaliza 1 nivel: + * - si es string con JSON -> JSON.parse + * - si es array -> intenta parsear c/u si son strings-JSON + * - si es objeto/number/bool -> lo deja igual + */ +const normalizeJsonishOneLevel = (v: any) => { + if (typeof v === 'string') return looksLikeJson(v) ? tryParseOnce(v) : v + if (Array.isArray(v)) return v.map(x => (typeof x === 'string' && looksLikeJson(x) ? tryParseOnce(x) : x)) + return v +} + + +export async function parseResponse(res: Response, leaf: DsLeaf): Promise { + const ct = res.headers.get('content-type') || '' + const raw = ct.includes('application/json') ? await res.json() : await res.text() + + const normalized1 = normalizeJsonishOneLevel(raw) + + const coerced = leaf?.coerce?.response ? applyCoerce(normalized1, leaf.coerce.response) : normalized1 + + let selected = leaf.selector ? getAt(coerced, leaf.selector) : coerced + + if (selected === undefined && Array.isArray(coerced) && leaf.selector) { + selected = coerced.map(item => getAt(item, leaf.selector)) + } + + selected = normalizeJsonishOneLevel(selected) + + if ((leaf as any).selectorEach && Array.isArray(selected)) { + const each = (leaf as any).selectorEach as string + selected = selected.map(item => getAt(item, each)) + } + + return selected +} + +export async function fetchDsOnce(chain: ChainLike, key: string, ctx?: Record): Promise { + const leaf = resolveLeaf(chain, key) + if (!leaf) throw new Error(`DS key not found: ${key}`) + const { url, init } = buildRequest(chain, leaf, ctx) + if (!url) throw new Error(`Invalid DS url for key ${key}`) + const res = await fetch(url, init) + if (!res.ok) throw new Error(`RPC ${res.status}`) + const parsed = await parseResponse(res, leaf) + return parsed as T +} + +export type PageRuntime = { page?: number; perPage?: number; cursor?: string | undefined; limit?: number } + +export function buildPagingCtx(baseCtx: Record | undefined, chain: any, page: PageRuntime) { + return { ...(baseCtx ?? {}), ...page, chain } +} + +export function selectItemsFromResponse(raw: any, itemsPath?: string | string[], fallbackSelector?: string): T[] { + const paths = (Array.isArray(itemsPath) ? itemsPath : [itemsPath ?? fallbackSelector]).filter(Boolean) as string[] + if (paths.length === 0) { + const v = raw + return Array.isArray(v) ? v : (v != null ? [v as T] : []) + } + return paths.flatMap(sel => { + const v = sel ? getAt(raw, sel) : raw + return Array.isArray(v) ? v : (v != null ? [v as T] : []) + }) +} + +export function computeNextParam( + strategy: 'page'|'cursor'|undefined, + respCfg: { totalPages?: string; nextPage?: string; nextCursor?: string }, + raw: any, + nowPage: number, + perPage: number, + itemsLen: number +) { + if (strategy === 'cursor') { + const cursor = respCfg.nextCursor ? getAt(raw, respCfg.nextCursor) : raw?.next || raw?.nextCursor + return cursor ? { cursor } : undefined + } + // page-based + const totalPages = respCfg.totalPages ? getAt(raw, respCfg.totalPages) : undefined + const explicitNext = respCfg.nextPage ? getAt(raw, respCfg.nextPage) : undefined + if (typeof explicitNext === 'number') return { page: explicitNext } + if (typeof totalPages === 'number' && nowPage < totalPages) return { page: nowPage + 1 } + if (itemsLen >= perPage) return { page: nowPage + 1 } // heurística + return undefined +} diff --git a/cmd/rpc/web/wallet-new/src/core/dsFetch.ts b/cmd/rpc/web/wallet-new/src/core/dsFetch.ts new file mode 100644 index 000000000..c8a70e927 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/dsFetch.ts @@ -0,0 +1,7 @@ +import { useConfig } from '@/app/providers/ConfigProvider' +import { fetchDsOnce } from './dsCore' + +export function useDSFetcher() { + const { chain } = useConfig() + return (key: string, ctx?: Record) => fetchDsOnce(chain, key, ctx) +} diff --git a/cmd/rpc/web/wallet-new/src/core/fees.ts b/cmd/rpc/web/wallet-new/src/core/fees.ts new file mode 100644 index 000000000..2e1664231 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/fees.ts @@ -0,0 +1,156 @@ +// fees.ts (arriba) +export type FeeBuckets = Record< + string, + { multiplier: number; default?: boolean } +>; +export type FeeProviderQuery = { + type: "query"; + base: "rpc" | "admin"; + path: string; + method?: "GET" | "POST"; + encoding?: "json" | "text"; + headers?: Record; + body?: any; + selector?: string; // ej: "fee" para tomar sólo el bloque fee del /params +}; +export type FeeProviderStatic = { + type: "static"; + data: any; // objeto fee literal +}; +export type FeeProviderExternal = { + type: "external"; + url: string; + method?: "GET" | "POST"; + headers?: Record; + body?: any; + selector?: string; +}; + +export type FeesConfig = { + denom: string; // ej: "{{chain.denom.base}}" + refreshMs?: number; + providers: Array; + buckets?: FeeBuckets; +}; + +export type ResolvedFees = { + /** Entier Object fee (ex: { sendFee, stakeFee, ... }) */ + raw: any; + amount?: number; + bucket?: string; + /** denom (ex: ucnpy) */ + denom: string; +}; +// Decide qué clave de fee usar según la acción +const feeKeyForAction = (actionId?: string) => { + // mapea lo que tengas en manifest: 'send'|'stake'|'unstake'... + if (actionId === "send") return "sendFee"; + if (actionId === "stake") return "stakeFee"; + if (actionId === "unstake") return "unstakeFee"; + return "sendFee"; // fallback sensato +}; + +// Aplica bucket (multiplier) si está definido +const applyBucket = (base: number, bucket?: { multiplier?: number }) => + typeof base === "number" && bucket?.multiplier + ? base * bucket.multiplier + : base; + +async function runProvider( + p: FeesConfig["providers"][number], + ctx: any, +): Promise { + if (p.type === "static") return p.data; + + if (p.type === "query") { + const base = p.base === "admin" ? ctx.chain.rpc.admin : ctx.chain.rpc.base; + const url = `${base}${p.path}`; + const init: RequestInit = { + method: p.method || "POST", + headers: { "Content-Type": "application/json", ...(p.headers || {}) }, + }; + if (p.method !== "GET" && p.body !== undefined) + init.body = typeof p.body === "string" ? p.body : JSON.stringify(p.body); + const res = await fetch(url, init); + const text = await res.text(); + const data = p.encoding === "text" ? JSON.parse(text) : JSON.parse(text); + return p.selector + ? p.selector.split(".").reduce((a, k) => a?.[k], data) + : data; + } + + if (p.type === "external") { + const init: RequestInit = { + method: p.method || "GET", + headers: { "Content-Type": "application/json", ...(p.headers || {}) }, + }; + if ((p.method || "GET") !== "GET" && p.body !== undefined) + init.body = typeof p.body === "string" ? p.body : JSON.stringify(p.body); + const res = await fetch(p.url, init); + const text = await res.text(); + const data = JSON.parse(text); + return p.selector + ? p.selector.split(".").reduce((a, k) => a?.[k], data) + : data; + } +} + +import { useEffect, useMemo, useRef, useState } from "react"; + +export function useResolvedFees( + feesConfig: FeesConfig, + opts: { actionId?: string; bucket?: string; ctx: any }, +): ResolvedFees { + const { denom, refreshMs = 30000, providers, buckets } = feesConfig; + const [raw, setRaw] = useState(null); + const timerRef = useRef(null); + + const ctxRef = useRef(opts.ctx); + useEffect(() => { + ctxRef.current = opts.ctx; + }, [opts.ctx]); + + useEffect(() => { + let cancelled = false; + + const fetchOnce = async () => { + for (const p of providers) { + try { + const data = await runProvider(p, ctxRef.current); + if (!cancelled && data) { + setRaw(data); + break; + } + } catch (e) { + console.error(`Error fetching fees from ${p.type}:`, e); + } + } + }; + + if (timerRef.current) clearInterval(timerRef.current); + + fetchOnce(); + + if (refreshMs > 0) { + timerRef.current = setInterval(fetchOnce, refreshMs); + } + + return () => { + cancelled = true; + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [refreshMs, JSON.stringify(providers)]); + + const amount = useMemo(() => { + if (!raw) return undefined; + const key = feeKeyForAction(opts.actionId); + const base = Number(raw?.[key] ?? 0); + const bucket = + opts.bucket || + Object.entries(buckets || {}).find(([, b]) => b?.default)?.[0]; + const bucketDef = bucket ? (buckets || {})[bucket] : undefined; + return applyBucket(base, bucketDef); + }, [raw, opts.actionId, opts.bucket, buckets]); + + return { raw, amount, denom }; +} diff --git a/cmd/rpc/web/wallet-new/src/core/format.ts b/cmd/rpc/web/wallet-new/src/core/format.ts new file mode 100644 index 000000000..82033a78c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/format.ts @@ -0,0 +1,3 @@ +export const microToDisplay = (amt: number, decimals: number) => amt / Math.pow(10, decimals) +export const withSymbol = (v: number, symbol: string, frac=2) => + `${v.toLocaleString(undefined, { maximumFractionDigits: frac })} ${symbol}` diff --git a/cmd/rpc/web/wallet-new/src/core/normalizeDsConfig.ts b/cmd/rpc/web/wallet-new/src/core/normalizeDsConfig.ts new file mode 100644 index 000000000..11d1aee98 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/normalizeDsConfig.ts @@ -0,0 +1,41 @@ +// utils/normalizeDsConfig.ts +export type NormalizedDs = { + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", + path: string, + query?: Record, + body?: any, + headers?: Record, + baseUrl?: string, +}; + +const RESERVED = new Set(["__options","method","path","query","body","headers","baseUrl"]); + +export function normalizeDsConfig(name: string, raw: any): NormalizedDs { + if (!raw || typeof raw !== "object") return { method: "GET", path: `/${name}` }; + + if (raw.method || raw.path || raw.query || raw.body) { + return { + method: (raw.method ?? "GET").toUpperCase() as any, + path: raw.path ?? `/${name}`, + query: raw.query, + body: raw.body, + headers: raw.headers, + baseUrl: raw.baseUrl, + }; + } + + const keys = Object.keys(raw).filter(k => !RESERVED.has(k)); + if (keys.length === 1) { + const k = keys[0]; + const params = raw[k] ?? {}; + return { + method: "GET", + path: `/${k}`, + query: params, + headers: raw.headers, + baseUrl: raw.baseUrl, + }; + } + + return { method: "GET", path: `/${name}`, headers: raw.headers, baseUrl: raw.baseUrl }; +} diff --git a/cmd/rpc/web/wallet-new/src/core/queryKeys.ts b/cmd/rpc/web/wallet-new/src/core/queryKeys.ts new file mode 100644 index 000000000..9ed4f7da9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/queryKeys.ts @@ -0,0 +1,4 @@ +export const QK = { + CHAINS: ['chains'] as const, + WALLETS: ['wallets'] as const, +}; diff --git a/cmd/rpc/web/wallet-new/src/core/rpc.ts b/cmd/rpc/web/wallet-new/src/core/rpc.ts new file mode 100644 index 000000000..83990fd0a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/rpc.ts @@ -0,0 +1,28 @@ +// src/core/rpc.ts +type Base = 'rpc' | 'admin'; + +export function makeRpc(base: Base = 'rpc', opts?: { headers?: Record }) { + const { chain } = (window as any).__configCtx ?? {}; + const host = + base === 'admin' + ? (chain?.rpc?.admin ?? chain?.rpc?.base ?? '') + : (chain?.rpc?.base ?? ''); + + async function request(path: string, init: RequestInit): Promise { + const res = await fetch(host + path, init); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()) as T; + } + + return { + get: (path: string, init?: RequestInit) => + request(path, { method: 'GET', ...(init ?? {}), headers: { ...(opts?.headers ?? {}) } }), + post: (path: string, body?: any, init?: RequestInit) => + request(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) }, + body: body == null ? undefined : JSON.stringify(body), + ...(init ?? {}), + }), + }; +} diff --git a/cmd/rpc/web/wallet-new/src/core/templater.ts b/cmd/rpc/web/wallet-new/src/core/templater.ts new file mode 100644 index 000000000..34e4b6aed --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/templater.ts @@ -0,0 +1,281 @@ +import { templateFns } from "./templaterFunctions"; + +const banned = + /(constructor|prototype|__proto__|globalThis|window|document|import|Function|eval)\b/; + +function splitArgs(src: string): string[] { + // divide por comas ignorando comillas y anidación <...>, (...), {{...}} + const out: string[] = []; + let cur = ""; + let depthAngle = 0, + depthParen = 0, + depthMustache = 0; + let inS = false, + inD = false; + + for (let i = 0; i < src.length; i++) { + const ch = src[i], + prev = src[i - 1]; + + if (!inS && !inD) { + if (ch === "<") depthAngle++; + else if (ch === ">") depthAngle = Math.max(0, depthAngle - 1); + else if (ch === "(") depthParen++; + else if (ch === ")") depthParen = Math.max(0, depthParen - 1); + else if (ch === "{" && src[i + 1] === "{") { + depthMustache++; + i++; + cur += "{{"; + continue; + } else if (ch === "}" && src[i + 1] === "}") { + depthMustache = Math.max(0, depthMustache - 1); + i++; + cur += "}}"; + continue; + } + } + if (ch === "'" && !inD && prev !== "\\") inS = !inS; + else if (ch === '"' && !inS && prev !== "\\") inD = !inD; + + if ( + ch === "," && + !inS && + !inD && + depthAngle === 0 && + depthParen === 0 && + depthMustache === 0 + ) { + out.push(cur.trim()); + cur = ""; + continue; + } + cur += ch; + } + if (cur.trim() !== "") out.push(cur.trim()); + return out; +} + +// evalúa una expresión JS segura usando contexto como argumentos +function evalJsExpression(expr: string, ctx: any): any { + if (banned.test(expr)) throw new Error("templater: forbidden token"); + const argNames = Object.keys(ctx); + const argVals = Object.values(ctx); + // return ( ...expr... ); + // eslint-disable-next-line no-new-func + const fn = new Function(...argNames, `return (${expr});`); + return fn(...argVals); +} + +function replaceBalanced( + input: string, + resolver: (expr: string) => string, +): string { + let out = ""; + let i = 0; + while (i < input.length) { + const start = input.indexOf("{{", i); + if (start === -1) { + out += input.slice(i); + break; + } + // texto antes del bloque + out += input.slice(i, start); + + // buscar cierre balanceado + let j = start + 2; + let depth = 1; + while (j < input.length && depth > 0) { + if (input.startsWith("{{", j)) { + depth += 1; + j += 2; + continue; + } + if (input.startsWith("}}", j)) { + depth -= 1; + j += 2; + if (depth === 0) break; + continue; + } + j += 1; + } + + // si no se cerró, copia resto y corta + if (depth !== 0) { + out += input.slice(start); + break; + } + + const exprRaw = input.slice(start + 2, j - 2); + const replacement = resolver(exprRaw.trim()); + out += replacement; + i = j; + } + return out; +} + +/** Evalúa una expresión: función tipo fn<...> o ruta a datos a.b.c */ +function evalExpr(expr: string, ctx: any): any { + if (banned.test(expr)) throw new Error("templater: forbidden token"); + + // 1) sintaxis: fn + const angleCall = expr.match(/^(\w+)<([\s\S]*)>$/); + if (angleCall) { + const [, fnName, rawArgs] = angleCall; + const argStrs = splitArgs(rawArgs); + const args = argStrs.map((a) => template(a, ctx)); // cada arg puede tener {{...}} anidado + const fn = (templateFns as Record)[fnName]; + if (typeof fn !== "function") return ""; + try { + return fn(...args); + } catch { + return ""; + } + } + + // 2) sintaxis: fn(arg1, arg2, ...) + const parenCall = expr.match(/^(\w+)\(([\s\S]*)\)$/); + if (parenCall) { + const [, fnName, rawArgs] = parenCall; + const argStrs = splitArgs(rawArgs); + const args = argStrs.map((a) => { + // si el arg es una expresión/plantilla, resuélvela; si es literal, evalúala + if (/{{.*}}/.test(a)) return template(a, ctx); + try { + return evalJsExpression(a, ctx); + } catch { + return template(a, ctx); + } + }); + const fn = (templateFns as Record)[fnName]; + if (typeof fn !== "function") return ""; + try { + return fn(...args); + } catch { + return ""; + } + } + + // 3) expresión JS libre (p. ej. form.amount * 0.05, Object.keys(ds)...) + try { + return evalJsExpression(expr, ctx); + } catch { + // 4) ruta normal: a.b.c + const path = expr + .split(".") + .map((s) => s.trim()) + .filter(Boolean); + let val: any = ctx; + for (const p of path) val = val?.[p]; + return val == null || typeof val === "object" ? val : String(val); + } +} + +export function resolveTemplatesDeep(obj: T, ctx: any): T { + if (obj == null) return obj as T; + if (typeof obj === "string") return templateAny(obj, ctx) as any; + if (Array.isArray(obj)) + return obj.map((x) => resolveTemplatesDeep(x, ctx)) as any; + if (typeof obj === "object") { + const out: any = {}; + for (const [k, v] of Object.entries(obj)) + out[k] = resolveTemplatesDeep(v, ctx); + return out; + } + return obj as T; +} + +export function extractTemplateDeps(str: string): string[] { + if (typeof str !== "string" || !str.includes("{{")) return []; + + const blocks: string[] = []; + const reBlock = /\{\{([\s\S]*?)\}\}/g; + let m: RegExpExecArray | null; + while ((m = reBlock.exec(str))) blocks.push(m[1]); + + const ROOTS = ["form", "chain", "params", "fees", "account", "session", "ds"]; + const rootGroup = ROOTS.join("|"); + const rePath = new RegExp( + `\\b(?:${rootGroup})\\s*(?:\\?\\.)?(?:\\.[A-Za-z0-9_]+|\\[(?:"[^"]+"|'[^']+')\\])+`, + "g", + ); + + const results: string[] = []; + + for (const code of blocks) { + const found = code.match(rePath) || []; + for (let raw of found) { + raw = raw.replace(/\?\./g, "."); + raw = raw.replace( + /\[("([^"]+)"|'([^']+)')\]/g, + (_s, _g1, g2, g3) => `.${g2 ?? g3}`, + ); + results.push(raw); + } + } + + return Array.from(new Set(results)); +} + +export function collectDepsFromObject(obj: any): string[] { + const acc = new Set(); + const walk = (node: any) => { + if (node == null) return; + if (typeof node === "string") { + extractTemplateDeps(node).forEach((d) => acc.add(d)); + return; + } + if (Array.isArray(node)) { + node.forEach(walk); + return; + } + if (typeof node === "object") { + Object.values(node).forEach(walk); + return; + } + }; + walk(obj); + return Array.from(acc); +} + +export function template(str: unknown, ctx: any): string { + if (str == null) return ""; + const input = String(str); + + const out = replaceBalanced(input, (expr) => evalExpr(expr, ctx)); + return out; +} + +export function templateAny(s: any, ctx: any) { + if (typeof s !== "string") return s; + const m = s.match(/^\s*{{\s*([\s\S]+?)\s*}}\s*$/); + if (m) return evalExpr(m[1], ctx); + return s.replace(/{{\s*([\s\S]+?)\s*}}/g, (_, e) => { + const v = evalExpr(e, ctx); + return v == null ? "" : String(v); + }); +} + +export function templateBool(tpl: any, ctx: Record = {}): boolean { + const v = templateAny(tpl, ctx); + return toBool(v); +} + +export function toBool(v: any): boolean { + if (typeof v === "boolean") return v; + if (typeof v === "number") return v !== 0 && !Number.isNaN(v); + if (v == null) return false; + if (Array.isArray(v)) return v.length > 0; + if (typeof v === "object") return Object.keys(v).length > 0; + const s = String(v).trim().toLowerCase(); + if ( + s === "" || + s === "0" || + s === "false" || + s === "no" || + s === "off" || + s === "null" || + s === "undefined" + ) + return false; + return true; +} diff --git a/cmd/rpc/web/wallet-new/src/core/templaterFunctions.ts b/cmd/rpc/web/wallet-new/src/core/templaterFunctions.ts new file mode 100644 index 000000000..d1bfdee3a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/templaterFunctions.ts @@ -0,0 +1,53 @@ +export const templateFns = { + // Convert from base denom (micro) to display denom - returns formatted string + formatToCoin: (v: any) => { + if (v === '' || v == null) return '' + const n = Number(v) + if (!Number.isFinite(n)) return '' + return (n / 1_000_000).toLocaleString(undefined, { maximumFractionDigits: 3 }) + }, + + // Convert from base denom (micro) to display denom - returns NUMBER (not string) + // Use this for field values, min, max, etc. + fromMicroDenom: (v: any) => { + if (v === '' || v == null) return 0 + const n = Number(v) + if (!Number.isFinite(n)) return 0 + return n / 1_000_000 + }, + + // Convert from display denom to base denom (micro) - returns NUMBER + // Use this for payload values that need to be sent to RPC + toMicroDenom: (v: any) => { + if (v === '' || v == null) return 0 + const n = Number(v) + if (!Number.isFinite(n)) return 0 + return Math.floor(n * 1_000_000) + }, + + // DEPRECATED: Use fromMicroDenom instead + formatToCoinNumber: (v: any) => { + const formatted = templateFns.formatToCoin(v) + if (formatted === '') return 0 + const n = Number(formatted) + if (!Number.isFinite(n)) return 0 + return n.toFixed(3) + }, + + // DEPRECATED: Use toMicroDenom instead + toBaseDenom: (v: any) => { + if (v === '' || v == null) return '' + const n = Number(v) + if (!Number.isFinite(n)) return '' + return (n * 1_000_000).toFixed(0) + }, + + numberToLocaleString: (v: any) => { + if (v === '' || v == null) return '' + const n = Number(v) + if (!Number.isFinite(n)) return '' + return n.toLocaleString(undefined, { maximumFractionDigits: 3 }) + }, + toUpper: (v: any) => String(v ?? "")?.toUpperCase(), + shortAddress: (v: any) => String(v ?? "")?.slice(0, 6) + "..." + String(v ?? "")?.slice(-6), +} diff --git a/cmd/rpc/web/wallet-new/src/core/useDSInfinite.ts b/cmd/rpc/web/wallet-new/src/core/useDSInfinite.ts new file mode 100644 index 000000000..db98e76fd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/useDSInfinite.ts @@ -0,0 +1,84 @@ +import { useInfiniteQuery } from '@tanstack/react-query' +import { useConfig } from '@/app/providers/ConfigProvider' +import { + resolveLeaf, buildRequest, parseResponse, + buildPagingCtx, selectItemsFromResponse, computeNextParam +} from './dsCore' + +type InfiniteOpts = { + selectItems?: (pageRaw: any) => T[] + getNextPageParam?: (pageRaw: any, allPages: any[]) => any + perPage?: number + startPage?: number + limit?: number + staleTimeMs?: number + refetchIntervalMs?: number + enabled?: boolean +} + +export function useDSInfinite(key: string, ctx?: Record, opts?: InfiniteOpts) { + const { chain } = useConfig() + const leaf = resolveLeaf(chain, key) + + const staleTime = + opts?.staleTimeMs ?? + leaf?.cache?.staleTimeMs ?? + chain?.params?.refresh?.staleTimeMs ?? 60_000 + + const refetchInterval = + opts?.refetchIntervalMs ?? + leaf?.cache?.refetchIntervalMs ?? + chain?.params?.refresh?.refetchIntervalMs + + const strategy = leaf?.page?.strategy + const respCfg = leaf?.page?.response ?? {} + const defaults = leaf?.page?.defaults ?? {} + + const startPage = opts?.startPage ?? defaults.startPage ?? 1 + const perPage = opts?.perPage ?? defaults.perPage ?? 20 + const limit = opts?.limit ?? defaults.limit ?? perPage + + const ctxKey = JSON.stringify(ctx ?? {}) + + return useInfiniteQuery({ + queryKey: ['ds.inf', chain?.chainId ?? 'chain', key, ctxKey, perPage, limit], + enabled: !!leaf && (opts?.enabled ?? true), + staleTime, + refetchInterval, + retry: 1, + placeholderData: (prev)=>prev, + structuralSharing: (old,data)=> (JSON.stringify(old)===JSON.stringify(data) ? old as any : data as any), + initialPageParam: strategy === 'cursor' ? { cursor: undefined } : { page: startPage }, + + queryFn: async ({ pageParam }: any) => { + // ctx + page + const pageCtx = buildPagingCtx(ctx, chain, { + page: pageParam?.page, perPage, cursor: pageParam?.cursor, limit + }) + if (!leaf) throw new Error(`DS key not found: ${key}`) + + // build + fetch + const { url, init } = buildRequest(chain, leaf, pageCtx) + if (!url) throw new Error(`Invalid DS url for key ${key}`) + const res = await fetch(url, init) + if (!res.ok) throw new Error(`RPC ${res.status}`) + + // parse + const raw = await parseResponse(res, leaf) + + // items + const items = opts?.selectItems + ? opts.selectItems(raw) + : selectItemsFromResponse(raw, respCfg.items, leaf?.selector) + + // next + const nextParam = opts?.getNextPageParam + ? opts.getNextPageParam(raw, []) + : computeNextParam(strategy, respCfg, raw, pageParam?.page ?? startPage, perPage, items.length) + + return { raw, items, nextParam } + }, + + getNextPageParam: (lastPage) => lastPage?.nextParam + }) +} diff --git a/cmd/rpc/web/wallet-new/src/core/useDebouncedValue.ts b/cmd/rpc/web/wallet-new/src/core/useDebouncedValue.ts new file mode 100644 index 000000000..0a0459073 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/useDebouncedValue.ts @@ -0,0 +1,10 @@ +import React from "react"; + +export default function useDebouncedValue(value: T, delay = 250) { + const [v, setV] = React.useState(value) + React.useEffect(() => { + const t = setTimeout(() => setV(value), delay) + return () => clearTimeout(t) + }, [value, delay]) + return v +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/core/useDs.ts b/cmd/rpc/web/wallet-new/src/core/useDs.ts new file mode 100644 index 000000000..6a62e5eaf --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/useDs.ts @@ -0,0 +1,91 @@ +// src/core/useDS.ts +import { useQuery } from '@tanstack/react-query' +import { useConfig } from '@/app/providers/ConfigProvider' +import { resolveLeaf, buildRequest, parseResponse } from './dsCore' + +export type DSOptions = { + // Query behavior + enabled?: boolean + select?: (d: any) => T + + // Caching & refetching + staleTimeMs?: number + gcTimeMs?: number + refetchIntervalMs?: number + refetchOnWindowFocus?: boolean + refetchOnMount?: boolean + refetchOnReconnect?: boolean + + // Error handling + retry?: number | boolean + retryDelay?: number + + // Scope for query key isolation + scope?: string +} + +export function useDS( + key: string, + ctx?: Record, + opts?: DSOptions +) { + const { chain } = useConfig() + const leaf = resolveLeaf(chain, key) + + // Stale time - how long data is considered fresh + const staleTime = + opts?.staleTimeMs ?? + leaf?.cache?.staleTimeMs ?? + chain?.params?.refresh?.staleTimeMs ?? + 60_000 + + // Garbage collection time - how long unused data stays in cache + const gcTime = + opts?.gcTimeMs ?? + 5 * 60_000 + + // Refetch interval - auto-refresh interval + const refetchInterval = + opts?.refetchIntervalMs ?? + leaf?.cache?.refetchIntervalMs ?? + chain?.params?.refresh?.refetchIntervalMs + + // Serialize context for query key + const ctxKey = JSON.stringify(ctx ?? {}) + + // Build scoped query key to prevent cache collisions + const queryKey = [ + 'ds', + chain?.chainId ?? 'chain', + key, + opts?.scope ?? 'global', + ctxKey + ] + + + return useQuery({ + queryKey, + enabled: !!leaf && (opts?.enabled ?? true), + staleTime, + gcTime, + refetchInterval, + refetchOnWindowFocus: opts?.refetchOnWindowFocus ?? false, + refetchOnMount: opts?.refetchOnMount ?? true, + refetchOnReconnect: opts?.refetchOnReconnect ?? false, + retry: opts?.retry ?? 1, + retryDelay: opts?.retryDelay, + // Don't use placeholderData - it causes stale data to show when params change + // placeholderData: (prev) => prev, + structuralSharing: (old, data) => + (JSON.stringify(old) === JSON.stringify(data) ? old as any : data as any), + queryFn: async () => { + if (!leaf) throw new Error(`DS key not found: ${key}`) + const { url, init } = buildRequest(chain, leaf, ctx) + if (!url) throw new Error(`Invalid DS url for key ${key}`) + const res = await fetch(url, init) + if (!res.ok) throw new Error(`RPC ${res.status}`) + return parseResponse(res, leaf) + }, + select: opts?.select as any + }) +} diff --git a/cmd/rpc/web/wallet-new/src/helpers/chain.ts b/cmd/rpc/web/wallet-new/src/helpers/chain.ts new file mode 100644 index 000000000..6bd4c003f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/helpers/chain.ts @@ -0,0 +1,6 @@ +export function getAbbreviateAmount(value: number) { + if (value >= 1_000_000_000) return (value / 1_000_000_000).toFixed(1) + 'B'; + if (value >= 1_000_000) return (value / 1_000_000).toFixed(1) + 'M'; + if (value >= 1_000) return (value / 1_000).toFixed(1) + 'K'; + return value.toString(); +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/helpers/download.ts b/cmd/rpc/web/wallet-new/src/helpers/download.ts new file mode 100644 index 000000000..1332b1928 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/helpers/download.ts @@ -0,0 +1,12 @@ +export function downloadJson(payload: unknown, filename: string) { + const dataStr = JSON.stringify(payload, null, 2); + const blob = new Blob([dataStr], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `${filename}.json`; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useAccountData.ts b/cmd/rpc/web/wallet-new/src/hooks/useAccountData.ts new file mode 100644 index 000000000..2ee798362 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useAccountData.ts @@ -0,0 +1,110 @@ +import { useQuery } from '@tanstack/react-query' +import { useConfig } from '@/app/providers/ConfigProvider' +import {useDSFetcher} from "@/core/dsFetch"; +import {hasDsKey} from "@/core/dsCore"; +import {useAccounts} from "@/app/providers/AccountsProvider"; + +interface AccountBalance { + address: string + amount: number + nickname?: string +} + +interface StakingData { + address: string + staked: number + rewards: number + nickname?: string +} + +const parseMaybeJson = (v: any) => + (typeof v === 'string' && /^\s*[{[]/.test(v)) ? JSON.parse(v) : v + + +export function useAccountData() { + const { accounts, loading: accountsLoading } = useAccounts() + const dsFetch = useDSFetcher() + const { chain } = useConfig() + + const chainId = chain?.chainId ?? 'chain' + const chainReadyBalances = !!chain && hasDsKey(chain, 'account') + const chainReadyValidators = !!chain && hasDsKey(chain, 'validators') + + // ---- BALANCES ---- + const balanceQuery = useQuery({ + queryKey: ['accountBalances.ds', chainId, accounts.map(a => a.address)], + enabled: !accountsLoading && accounts.length > 0 && chainReadyBalances, + staleTime: 10_000, + retry: 2, + retryDelay: 1000, + queryFn: async () => { + // doble guard por seguridad + if (!chainReadyBalances || accounts.length === 0) { + return { totalBalance: 0, balances: [] as AccountBalance[] } + } + + const balances = await Promise.all( + accounts.map(async (acc): Promise => { + try { + const res = await dsFetch('account', { account: { address: acc.address }}) + const val = typeof res === 'number' + ? res + : Number(parseMaybeJson(res)?.amount ?? 0) + + return { address: acc.address, amount: val || 0, nickname: acc.nickname } + } catch (err) { + // si el chain aún no estaba listo, regresamos 0 silenciosamente + return { address: acc.address, amount: 0, nickname: acc.nickname } + } + }) + ) + + const totalBalance = balances.reduce((s, b) => s + (b.amount || 0), 0) + return { totalBalance, balances } + } + }) + + // ---- STAKING ---- + const stakingQuery = useQuery({ + queryKey: ['stakingData.ds', chainId, accounts.map(a => a.address)], + enabled: !accountsLoading && accounts.length > 0 && chainReadyValidators, + staleTime: 10_000, + retry: 2, + retryDelay: 1000, + queryFn: async () => { + if (!chainReadyValidators || accounts.length === 0) { + return { totalStaked: 0, stakingData: [] as StakingData[] } + } + + const rows = await dsFetch('validators', {}) + const list = Array.isArray(rows) ? rows : [] + + const byAddr = new Map() + for (const v of list) { + const obj = parseMaybeJson(v) + const key = obj?.address ?? obj?.validatorAddress ?? obj?.operatorAddress + if (key) byAddr.set(String(key), obj) + } + + const stakingData = accounts.map((acc): StakingData => { + const v = byAddr.get(acc.address) + const staked = Number(v?.stakedAmount ?? v?.stake ?? 0) + return { address: acc.address, staked: staked || 0, rewards: 0, nickname: acc.nickname } + }) + + const totalStaked = stakingData.reduce((s, d) => s + (d.staked || 0), 0) + return { totalStaked, stakingData } + } + }) + + return { + totalBalance: balanceQuery.data?.totalBalance || 0, + totalStaked: stakingQuery.data?.totalStaked || 0, + balances: balanceQuery.data?.balances || [], + stakingData: stakingQuery.data?.stakingData || [], + loading: accountsLoading || balanceQuery.isLoading || stakingQuery.isLoading, + error: balanceQuery.error || stakingQuery.error, + refetchBalances: balanceQuery.refetch, + refetchStaking: stakingQuery.refetch, + } +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useAccounts.ts b/cmd/rpc/web/wallet-new/src/hooks/useAccounts.ts new file mode 100644 index 000000000..2aa7d83c3 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useAccounts.ts @@ -0,0 +1,88 @@ +import { useDS } from "@/core/useDs"; + +export interface Account { + address: string; + nickname?: string; + balance?: number; + stakedAmount?: number; + publicKey?: string; + type?: "local" | "imported"; +} + +export interface AccountsState { + accounts: Account[]; + selectedAccount: Account | null; + isLoading: boolean; +} + +export const useAccounts = () => { + const { data: accountsData, isLoading } = useDS( + "account", + {}, + { + staleTimeMs: 10000, + refetchIntervalMs: 30000, + refetchOnMount: true, + refetchOnWindowFocus: false, + select: (data) => { + if (!data) return { accounts: [], selectedAccount: null }; + + // Handle single account case + if (data.address) { + const account: Account = { + address: data.address, + nickname: data.nickname || "Account 1", + balance: data.amount || 0, + stakedAmount: data.stakedAmount || 0, + publicKey: data.publicKey, + type: "local", + }; + return { + accounts: [account], + selectedAccount: account, + }; + } + + // Handle multiple accounts case + if (Array.isArray(data)) { + const accounts = data.map((acc, index) => ({ + address: acc.address, + nickname: acc.nickname || `Account ${index + 1}`, + balance: acc.amount || 0, + stakedAmount: acc.stakedAmount || 0, + publicKey: acc.publicKey, + type: acc.type || "local", + })); + return { + accounts, + selectedAccount: accounts[0] || null, + }; + } + + return { accounts: [], selectedAccount: null }; + }, + }, + ); + + const accounts = accountsData?.accounts || []; + const selectedAccount = accountsData?.selectedAccount || null; + + return { + accounts, + selectedAccount, + isLoading, + // Helper methods + getAccount: (address: string) => + accounts.find((acc: Account) => acc.address === address), + hasAccount: (address: string) => + accounts.some((acc: Account) => acc.address === address), + totalBalance: accounts.reduce( + (sum: number, acc: Account) => sum + (acc.balance || 0), + 0, + ), + totalStaked: accounts.reduce( + (sum: number, acc: Account) => sum + (acc.stakedAmount || 0), + 0, + ), + }; +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useBalanceChart.ts b/cmd/rpc/web/wallet-new/src/hooks/useBalanceChart.ts new file mode 100644 index 000000000..e91c88e94 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useBalanceChart.ts @@ -0,0 +1,113 @@ +import { useQuery } from '@tanstack/react-query' +import { useDSFetcher } from '@/core/dsFetch' +import { useHistoryCalculation } from './useHistoryCalculation' +import {useAccounts} from "@/app/providers/AccountsProvider"; + +export interface ChartDataPoint { + timestamp: number; + value: number; + label: string; +} + +interface BalanceChartOptions { + points?: number; // Number of data points (default: 7 for last 7 days) + type?: 'balance' | 'staked'; // Type of data to fetch +} + +export function useBalanceChart({ points = 7, type = 'balance' }: BalanceChartOptions = {}) { + const { accounts, loading: accountsLoading } = useAccounts() + const addresses = accounts.map(a => a.address).filter(Boolean) + const dsFetch = useDSFetcher() + const { currentHeight, secondsPerBlock, isReady } = useHistoryCalculation() + + return useQuery({ + queryKey: ['balanceChart', type, addresses, currentHeight, points], + enabled: !accountsLoading && addresses.length > 0 && isReady, + staleTime: 60_000, // 1 minute + retry: 1, + + queryFn: async (): Promise => { + if (addresses.length === 0 || currentHeight === 0) { + return [] + } + + // Calculate blocks per hour using consistent logic + const blocksPerHour = Math.round((60 * 60) / secondsPerBlock) + const blocksPerDay = blocksPerHour * 24 + + + const hoursInterval = 24 / (points - 1) + + const heights: number[] = [] + for (let i = 0; i < points; i++) { + const hoursAgo = Math.round(hoursInterval * (points - 1 - i)) + const heightOffset = Math.round(blocksPerHour * hoursAgo) + const height = Math.max(0, currentHeight - heightOffset) + heights.push(height) + } + + // Obtener datos para cada altura + const dataPoints: ChartDataPoint[] = [] + + for (let i = 0; i < heights.length; i++) { + const height = heights[i] + const hoursAgo = Math.round(hoursInterval * (points - 1 - i)) + + try { + let totalValue = 0 + + if (type === 'balance') { + // Obtener balances de todas las addresses en esta altura + const balances = await Promise.all( + addresses.map(address => + dsFetch('accountByHeight', { address, height }) + .then(v => v || 0) + .catch(() => 0) + ) + ) + totalValue = balances.reduce((sum, v) => sum + v, 0) + } else if (type === 'staked') { + // Obtener staked amounts de todas las addresses en esta altura + const stakes = await Promise.all( + addresses.map(address => + dsFetch('validatorByHeight', { address, height }) + .then(v => v?.stakedAmount || 0) + .catch(() => 0) + ) + ) + totalValue = stakes.reduce((sum, v) => sum + v, 0) + } + + // Crear label apropiado para horas + let label = '' + if (hoursAgo === 0) { + label = 'Now' + } else if (hoursAgo === 1) { + label = '1h ago' + } else if (hoursAgo < 24) { + label = `${hoursAgo}h ago` + } else { + label = '24h ago' + } + + dataPoints.push({ + timestamp: height, + value: totalValue, + label + }) + } catch (error) { + console.warn(`Error fetching data for height ${height}:`, error) + // Agregar punto con valor 0 en caso de error + const errorLabel = hoursAgo === 0 ? 'Now' : hoursAgo === 24 ? '24h ago' : `${hoursAgo}h ago` + dataPoints.push({ + timestamp: height, + value: 0, + label: errorLabel + }) + } + } + + return dataPoints + } + }) +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useBalanceHistory.ts b/cmd/rpc/web/wallet-new/src/hooks/useBalanceHistory.ts new file mode 100644 index 000000000..9b5997fdb --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useBalanceHistory.ts @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query' +import { useDSFetcher } from '@/core/dsFetch' +import { useHistoryCalculation, HistoryResult } from './useHistoryCalculation' +import {useAccounts} from "@/app/providers/AccountsProvider"; + +export function useBalanceHistory() { + const { accounts, loading: accountsLoading } = useAccounts() + const addresses = accounts.map(a => a.address).filter(Boolean) + const dsFetch = useDSFetcher() + const { currentHeight, height24hAgo, calculateHistory, isReady } = useHistoryCalculation() + + return useQuery({ + queryKey: ['balanceHistory', addresses, currentHeight], + enabled: !accountsLoading && addresses.length > 0 && isReady, + staleTime: 30_000, + retry: 2, + retryDelay: 2000, + + queryFn: async (): Promise => { + if (addresses.length === 0) { + return { current: 0, previous24h: 0, change24h: 0, changePercentage: 0, progressPercentage: 0 } + } + + // Fetch current and previous balances in parallel + const currentPromises = addresses.map(address => + dsFetch('accountByHeight', { address: address, height: currentHeight }) + ) + const previousPromises = addresses.map(address => + dsFetch('accountByHeight', { address, height: height24hAgo }) + ) + + const [currentBalances, previousBalances] = await Promise.all([ + Promise.all(currentPromises), + Promise.all(previousPromises), + ]) + + const currentTotal = currentBalances.reduce((sum: any, v: any) => sum + (v || 0), 0) + const previousTotal = previousBalances.reduce((sum: any, v: any) => sum + (v || 0), 0) + + return calculateHistory(currentTotal, previousTotal) + } + }) +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useBlockProducerData.ts b/cmd/rpc/web/wallet-new/src/hooks/useBlockProducerData.ts new file mode 100644 index 000000000..b5d3fe66d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useBlockProducerData.ts @@ -0,0 +1,126 @@ +import { useQuery } from "@tanstack/react-query"; +import { useDSFetcher } from "@/core/dsFetch"; + +interface BlockProducerData { + blocksProduced: number; + rewards24h: number; + lastProposedHeight?: number; +} + +interface UseBlockProducerDataProps { + validatorAddress: string; + enabled?: boolean; +} + +export function useBlockProducerData({ + validatorAddress, + enabled = true, +}: UseBlockProducerDataProps) { + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ["blockProducerData", validatorAddress], + queryFn: async (): Promise => { + try { + // Get current height using DS pattern + const currentHeight = await dsFetch("height"); + + // Get last proposers (this gives us recent block proposers) + const lastProposersResponse = await dsFetch("lastProposers", { + height: 0, + count: 100, + }); + const proposers = lastProposersResponse.addresses || []; + + // Count how many times this validator has proposed blocks recently + const blocksProduced = proposers.filter( + (addr: string) => addr === validatorAddress, + ).length; + + // Get parameters for accurate reward calculation + const params = await dsFetch("params"); + const mintPerBlock = params.MintPerBlock || 80000000; // 80 CNPY per block + const proposerCut = params.ProposerCut || 70; // 70% goes to proposer + + // Calculate rewards per block for this validator + // Proposer gets a percentage of the mint per block + const rewardsPerBlock = (mintPerBlock * proposerCut) / 100 / 1000000; // Convert to CNPY + const rewards24h = blocksProduced * rewardsPerBlock; + + // Find the last height this validator proposed + const lastProposedHeight = + proposers.lastIndexOf(validatorAddress) >= 0 + ? currentHeight - proposers.lastIndexOf(validatorAddress) + : undefined; + + return { + blocksProduced, + rewards24h, + lastProposedHeight, + }; + } catch (error) { + console.error("Error fetching block producer data:", error); + return { + blocksProduced: 0, + rewards24h: 0, + }; + } + }, + enabled: enabled && !!validatorAddress, + refetchInterval: 30000, // Refetch every 30 seconds + staleTime: 15000, // Consider data stale after 15 seconds + }); +} + +// Hook for multiple validators +export function useMultipleBlockProducerData(validatorAddresses: string[]) { + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ["multipleBlockProducerData", validatorAddresses], + queryFn: async (): Promise> => { + try { + const currentHeight = await dsFetch("height"); + const lastProposersResponse = await dsFetch("lastProposers", { + height: 0, + count: 100, + }); + const proposers = lastProposersResponse.addresses || []; + + const results: Record = {}; + + // Get parameters for accurate reward calculation + const params = await dsFetch("params"); + const mintPerBlock = params.MintPerBlock || 80000000; // 80 CNPY per block + const proposerCut = params.ProposerCut || 70; // 70% goes to proposer + + for (const address of validatorAddresses) { + const blocksProduced = proposers.filter( + (addr: string) => addr === address, + ).length; + const rewardsPerBlock = (mintPerBlock * proposerCut) / 100 / 1000000; // Convert to CNPY + const rewards24h = blocksProduced * rewardsPerBlock; + + const lastProposedHeight = + proposers.lastIndexOf(address) >= 0 + ? currentHeight - proposers.lastIndexOf(address) + : undefined; + + results[address] = { + blocksProduced, + rewards24h, + lastProposedHeight, + }; + } + + return results; + } catch (error) { + console.error("Error fetching multiple block producer data:", error); + return {}; + } + }, + enabled: validatorAddresses.length > 0, + refetchInterval: 30000, + staleTime: 15000, + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useBlockProducers.ts b/cmd/rpc/web/wallet-new/src/hooks/useBlockProducers.ts new file mode 100644 index 000000000..5182ebbff --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useBlockProducers.ts @@ -0,0 +1,113 @@ +import { useDS } from "@/core/useDs"; +import { useMemo } from "react"; + +interface BlockProposer { + address: string; + height: number; +} + +interface BlockProducerStats { + blocksProduced: number; + totalBlocksQueried: number; + productionRate: number; // percentage + lastBlockHeight: number; +} + +export const useBlockProducers = (count: number = 1000) => { + const { + data: proposers = [], + isLoading, + error, + } = useDS( + "lastProposers", + { count }, + { + enabled: true, + select: (data: any) => { + // The API returns an array of proposers + if (Array.isArray(data)) { + return data; + } + // If it returns an object with a results array + if (data && Array.isArray(data.results)) { + return data.results; + } + // If it returns an object with proposers directly + if (data && typeof data === "object") { + return Object.values(data).filter( + (item: any) => + item && typeof item === "object" && "address" in item, + ); + } + return []; + }, + }, + ); + + const getStatsForValidator = useMemo(() => { + return (validatorAddress: string): BlockProducerStats => { + if (!proposers || proposers.length === 0) { + return { + blocksProduced: 0, + totalBlocksQueried: 0, + productionRate: 0, + lastBlockHeight: 0, + }; + } + + const validatorBlocks = proposers.filter( + (proposer: any) => + proposer.address?.toLowerCase() === validatorAddress?.toLowerCase(), + ); + + const blocksProduced = validatorBlocks.length; + const totalBlocksQueried = proposers.length; + const productionRate = + totalBlocksQueried > 0 + ? (blocksProduced / totalBlocksQueried) * 100 + : 0; + + const lastBlock = + validatorBlocks.length > 0 + ? Math.max(...validatorBlocks.map((b: any) => b.height || 0)) + : 0; + + return { + blocksProduced, + totalBlocksQueried, + productionRate, + lastBlockHeight: lastBlock, + }; + }; + }, [proposers]); + + return { + proposers, + getStatsForValidator, + isLoading, + error, + }; +}; + +// Hook to get stats for multiple validators at once +export const useMultipleValidatorBlockStats = ( + addresses: string[], + count: number = 1000, +) => { + const { getStatsForValidator, isLoading, error } = + useBlockProducers(count); + + const stats = useMemo(() => { + const result: Record = {}; + addresses.forEach((address) => { + result[address] = getStatsForValidator(address); + }); + return result; + }, [addresses, getStatsForValidator]); + + return { + stats, + isLoading, + error, + }; +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useCopyToClipboard.tsx b/cmd/rpc/web/wallet-new/src/hooks/useCopyToClipboard.tsx new file mode 100644 index 000000000..05cb664aa --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useCopyToClipboard.tsx @@ -0,0 +1,34 @@ +import { useToast } from "@/toast/ToastContext"; +import { Copy, Check } from "lucide-react"; +import { useCallback } from "react"; + +export const useCopyToClipboard = () => { + const toast = useToast(); + + const copyToClipboard = useCallback(async (text: string, label?: string) => { + try { + await navigator.clipboard.writeText(text); + + toast.success({ + title: "Copied to clipboard", + description: label || "Text copied successfully", + icon: , + durationMs: 2000, + }); + + return true; + } catch (err) { + toast.error({ + title: "Failed to copy", + description: "Unable to copy to clipboard. Please try again.", + icon: , + sticky: false, + durationMs: 3000, + }); + + return false; + } + }, [toast]); + + return { copyToClipboard }; +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useDashboard.ts b/cmd/rpc/web/wallet-new/src/hooks/useDashboard.ts new file mode 100644 index 000000000..e7f3f4de9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useDashboard.ts @@ -0,0 +1,153 @@ +import {useDSInfinite} from "@/core/useDSInfinite"; +import React, {useMemo} from "react"; +import {Transaction} from "@/components/dashboard/RecentTransactionsCard"; +import {useAccounts} from "@/app/providers/AccountsProvider"; +import {useManifest} from "@/hooks/useManifest"; +import {Action as ManifestAction} from "@/manifest/types"; + +export const useDashboard = () => { + const [isActionModalOpen, setIsActionModalOpen] = React.useState(false); + const [selectedActions, setSelectedActions] = React.useState([]); + const { manifest ,loading: manifestLoading } = useManifest(); + + + const { selectedAddress, isReady: isAccountReady } = useAccounts() + + + const txSentQuery = useDSInfinite( + 'txs.sent', + {account: {address: selectedAddress}}, + { + enabled: !!selectedAddress && isAccountReady, + refetchIntervalMs: 15_000, + perPage: 20, + getNextPageParam: (lastPage, pages) => { + if (lastPage.length < 20) return undefined; + return pages.length + 1; + }, + selectItems: (d: any) => { + return Array.isArray(d?.results) ? d.results : (Array.isArray(d) ? d : []); + } + + } + ) + + const txReceivedQuery = useDSInfinite( + 'txs.received', + {account: {address: selectedAddress}}, + { + enabled: !!selectedAddress && isAccountReady, + refetchIntervalMs: 15_000, + perPage: 20, + getNextPageParam: (lastPage, pages) => { + if (lastPage.length < 20) return undefined; + return pages.length + 1; + }, + selectItems: (d: any) => { + return Array.isArray(d?.results) ? d.results : (Array.isArray(d) ? d : []); + } + } + ) + + const txFailedQuery = useDSInfinite( + 'txs.failed', + {account: {address: selectedAddress}}, + { + enabled: !!selectedAddress && isAccountReady, + refetchIntervalMs: 15_000, + perPage: 20, + getNextPageParam: (lastPage, pages) => { + if (lastPage.length < 20) return undefined; + return pages.length + 1; + }, + selectItems: (d: any) => { + return Array.isArray(d?.results) ? d.results : (Array.isArray(d) ? d : []); + } + } + ) + + + const isTxLoading = txSentQuery.isLoading || txReceivedQuery.isLoading || txFailedQuery.isLoading; + + const allTxs = useMemo(() => { + const sent = + txSentQuery.data?.pages.flatMap(p => + p.items.map(i => ({ + ...i, + transaction: { + // @ts-ignore + ...i.transaction, + }, + })) + ) ?? []; + + const received = + txReceivedQuery.data?.pages.flatMap(p => + p.items.map(i => ({ + ...i, + transaction: { + // @ts-ignore + ...i.transaction, + type: 'receive', + }, + })) + ) ?? []; + + const failed = + txFailedQuery.data?.pages.flatMap(p => + p.items.map(i => ({ + ...i, + transaction: { + // @ts-ignore + ...i.transaction, + type: 'stake', + status: 'Failed', + }, + + })) + ) ?? []; + + + const mergedTxs = [...sent, ...received, ...failed] + + return mergedTxs.map(tx => { + return { + // @ts-ignore + hash: String(tx.txHash ?? ''), + type: tx.transaction.type, + amount: tx.transaction.msg.amount ?? 0, + fee: tx.transaction.fee, + //TODO: CHECK HOW TO GET THIS VALUE + status: tx.transaction.status ?? 'Confirmed', + time: tx?.transaction?.time, + // @ts-ignore + address: tx.address, + } as Transaction; + }).sort((a, b) => b.time - a.time); + + }, [txSentQuery.data, txReceivedQuery.data, txFailedQuery.data]) + + const onRunAction = (action: ManifestAction) => { + const actions = [action] ; + if (action.relatedActions) { + const relatedActions = manifest?.actions.filter(a => action?.relatedActions?.includes(a.id)) + + if (relatedActions) + actions.push(...relatedActions) + } + setSelectedActions(actions); + setIsActionModalOpen(true); + } + + return { + isActionModalOpen, + setIsActionModalOpen, + selectedActions, + setSelectedActions, + manifest, + manifestLoading, + isTxLoading, + allTxs, + onRunAction, + } +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/src/hooks/useGovernance.ts b/cmd/rpc/web/wallet-new/src/hooks/useGovernance.ts new file mode 100644 index 000000000..431d54eb6 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useGovernance.ts @@ -0,0 +1,327 @@ +import { useDS } from "@/core/useDs"; + +export interface Proposal { + id: string; // Hash of the proposal + hash: string; + title: string; + description: string; + status: "active" | "passed" | "rejected" | "pending"; + category: string; + result: "Pass" | "Fail" | "Pending"; + proposer: string; + submitTime: string; + endHeight: number; + startHeight: number; + yesPercent: number; + noPercent: number; + // Vote counts + yesVotes: number; + noVotes: number; + abstainVotes: number; + totalVotes?: number; + votingStartTime?: string; + votingEndTime?: string; + // Raw proposal data from backend + type?: string; + msg?: any; + approve?: boolean | null; + createdHeight?: number; + fee?: number; + memo?: string; + time?: number; +} + +export interface Poll { + id: string; + hash: string; + title: string; + description: string; + status: "active" | "passed" | "rejected"; + endTime: string; + yesPercent: number; + noPercent: number; + accountVotes: { + yes: number; + no: number; + }; + validatorVotes: { + yes: number; + no: number; + }; + // Raw data + approve?: boolean | null; + createdHeight?: number; + endHeight?: number; + time?: number; +} + +export const useGovernance = () => { + return useDS( + "gov.proposals", + {}, + { + staleTimeMs: 10000, + refetchIntervalMs: 30000, + refetchOnMount: true, + refetchOnWindowFocus: false, + select: (data) => { + // Handle null or undefined + if (!data) { + return []; + } + + // If it's already an array, return it + if (Array.isArray(data)) { + return data; + } + + // If it's an object with hash keys, transform it to an array + if (typeof data === "object") { + const proposals: Proposal[] = Object.entries(data).map( + ([hash, value]: [string, any]) => { + const proposalData = value?.proposal || value; + const msg = proposalData?.msg || {}; + + // Determine status and result based on approve field + let status: "active" | "passed" | "rejected" | "pending" = + "pending"; + let result: "Pass" | "Fail" | "Pending" = "Pending"; + + if (value?.approve === true) { + status = "passed"; + result = "Pass"; + } else if (value?.approve === false) { + status = "rejected"; + result = "Fail"; + } else if ( + value?.approve === null || + value?.approve === undefined + ) { + status = "active"; + result = "Pending"; + } + + // Calculate percentages (simplified for now) + const yesPercent = + value?.approve === true + ? 100 + : value?.approve === false + ? 0 + : 50; + const noPercent = 100 - yesPercent; + + // Get category from type + const categoryMap: Record = { + changeParameter: "Gov", + daoTransfer: "Subsidy", + default: "Other", + }; + const category = + categoryMap[proposalData?.type] || categoryMap.default; + + return { + id: hash, + hash: hash, + title: msg.parameterSpace + ? `${msg.parameterSpace.toUpperCase()}: ${msg.parameterKey}` + : proposalData?.memo || + `${proposalData?.type || "Unknown"} Proposal`, + description: msg.parameterSpace + ? `Change ${msg.parameterKey} to ${msg.parameterValue}` + : proposalData?.memo || "No description available", + status: status, + category: category, + result: result, + proposer: + msg.signer || + proposalData?.signature?.publicKey?.slice(0, 40) || + "Unknown", + submitTime: proposalData?.time + ? new Date(proposalData.time / 1000).toISOString() + : new Date().toISOString(), + endHeight: msg.endHeight || 0, + startHeight: msg.startHeight || 0, + yesPercent: yesPercent, + noPercent: noPercent, + // Vote counts (simplified for now) + yesVotes: value?.approve === true ? 1 : 0, + noVotes: value?.approve === false ? 1 : 0, + abstainVotes: 0, + totalVotes: 1, + votingStartTime: msg.startHeight + ? `Height ${msg.startHeight}` + : proposalData?.time + ? new Date(proposalData.time / 1000).toISOString() + : new Date().toISOString(), + votingEndTime: msg.endHeight + ? `Height ${msg.endHeight}` + : new Date( + Date.now() + 7 * 24 * 60 * 60 * 1000, + ).toISOString(), + // Include raw data + type: proposalData?.type, + msg: msg, + approve: value?.approve, + createdHeight: proposalData?.createdHeight, + fee: proposalData?.fee, + memo: proposalData?.memo, + time: proposalData?.time, + }; + }, + ); + + return proposals; + } + + return []; + }, + }, + ); +}; + +export const useProposal = (proposalId: string) => { + return useDS( + "gov.proposals", + {}, + { + enabled: !!proposalId, + staleTimeMs: 10000, + select: (data) => { + if (!data) return undefined; + + // If it's already an array + if (Array.isArray(data)) { + return data.find( + (p: Proposal) => p.id === proposalId || p.hash === proposalId, + ); + } + + // If it's the object format + if (typeof data === "object") { + const proposals: Proposal[] = Object.entries(data).map( + ([hash, value]: [string, any]) => { + const proposalData = value?.proposal || value; + const msg = proposalData?.msg || {}; + + let status: "active" | "passed" | "rejected" | "pending" = + "pending"; + let result: "Pass" | "Fail" | "Pending" = "Pending"; + + if (value?.approve === true) { + status = "passed"; + result = "Pass"; + } else if (value?.approve === false) { + status = "rejected"; + result = "Fail"; + } else { + status = "active"; + result = "Pending"; + } + + // Get category from type + const categoryMap: Record = { + changeParameter: "Gov", + daoTransfer: "Subsidy", + default: "Other", + }; + const category = + categoryMap[proposalData?.type] || categoryMap.default; + + // Calculate percentages + const yesPercent = + value?.approve === true + ? 100 + : value?.approve === false + ? 0 + : 50; + const noPercent = 100 - yesPercent; + + return { + id: hash, + hash: hash, + title: msg.parameterSpace + ? `${msg.parameterSpace.toUpperCase()}: ${msg.parameterKey}` + : proposalData?.memo || + `${proposalData?.type || "Unknown"} Proposal`, + description: msg.parameterSpace + ? `Change ${msg.parameterKey} in ${msg.parameterSpace} to ${msg.parameterValue}` + : proposalData?.memo || "No description available", + status: status, + category: category, + result: result, + proposer: + msg.signer || + proposalData?.signature?.publicKey?.slice(0, 40) || + "Unknown", + submitTime: proposalData?.time + ? new Date(proposalData.time / 1000).toISOString() + : new Date().toISOString(), + endHeight: msg.endHeight || 0, + startHeight: msg.startHeight || 0, + yesPercent: yesPercent, + noPercent: noPercent, + votingStartTime: msg.startHeight + ? `Height ${msg.startHeight}` + : proposalData?.time + ? new Date(proposalData.time / 1000).toISOString() + : new Date().toISOString(), + votingEndTime: msg.endHeight + ? `Height ${msg.endHeight}` + : new Date( + Date.now() + 7 * 24 * 60 * 60 * 1000, + ).toISOString(), + yesVotes: value?.approve ? 1 : 0, + noVotes: value?.approve === false ? 1 : 0, + abstainVotes: 0, + totalVotes: 1, + type: proposalData?.type, + msg: msg, + approve: value?.approve, + createdHeight: proposalData?.createdHeight, + fee: proposalData?.fee, + memo: proposalData?.memo, + time: proposalData?.time, + }; + }, + ); + + return proposals.find( + (p) => p.id === proposalId || p.hash === proposalId, + ); + } + + return undefined; + }, + }, + ); +}; + +export const useVotingPower = (address: string) => { + return useDS<{ + votingPower: number; + stakedAmount: number; + percentage: number; + }>( + "validator", + { account: { address } }, + { + enabled: !!address, + staleTimeMs: 10000, + select: (validator) => { + if (!validator || !validator.stakedAmount) { + return { + votingPower: 0, + stakedAmount: 0, + percentage: 0, + }; + } + + return { + votingPower: validator.stakedAmount, + stakedAmount: validator.stakedAmount, + percentage: 0, // This would need total staked to calculate + }; + }, + }, + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useHistoryCalculation.ts b/cmd/rpc/web/wallet-new/src/hooks/useHistoryCalculation.ts new file mode 100644 index 000000000..a7f04adc2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useHistoryCalculation.ts @@ -0,0 +1,53 @@ +import { useDS } from "@/core/useDs" +import { useConfig } from '@/app/providers/ConfigProvider' + +export interface HistoryResult { + current: number; + previous24h: number; + change24h: number; + changePercentage: number; + progressPercentage: number; +} + +/** + * Hook to get consistent block height calculations for 24h history + * This ensures all charts and difference calculations use the same logic + */ +export function useHistoryCalculation() { + const { chain } = useConfig() + const { data: currentHeight = 0 } = useDS('height', {}, { staleTimeMs: 30_000 }) + + // Calculate height 24h ago using consistent logic + const secondsPerBlock = Number(chain?.params?.avgBlockTimeSec) > 0 + ? Number(chain?.params?.avgBlockTimeSec) + : 20 // Default to 20 seconds if not available + + const blocksPerDay = Math.round((24 * 60 * 60) / secondsPerBlock) + const height24hAgo = Math.max(0, currentHeight - blocksPerDay) + + /** + * Calculate history metrics from current and previous values + */ + const calculateHistory = (currentTotal: number, previousTotal: number): HistoryResult => { + const change24h = currentTotal - previousTotal + const changePercentage = previousTotal > 0 ? (change24h / previousTotal) * 100 : 0 + const progressPercentage = Math.min(Math.abs(changePercentage), 100) + + return { + current: currentTotal, + previous24h: previousTotal, + change24h, + changePercentage, + progressPercentage + } + } + + return { + currentHeight, + height24hAgo, + blocksPerDay, + secondsPerBlock, + calculateHistory, + isReady: currentHeight > 0 + } +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useManifest.ts b/cmd/rpc/web/wallet-new/src/hooks/useManifest.ts new file mode 100644 index 000000000..0077e6569 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useManifest.ts @@ -0,0 +1,69 @@ +import { useState, useEffect, useCallback } from 'react'; +import type { Action, Manifest } from "@/manifest/types"; + +export const useManifest = () => { + const [manifest, setManifest] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const ac = new AbortController(); + + const loadManifest = async () => { + try { + setLoading(true); + setError(null); + + // Use BASE_URL to construct the path, removing trailing slash if present to avoid double slashes + const baseUrl = import.meta.env.BASE_URL.endsWith('/') + ? import.meta.env.BASE_URL.slice(0, -1) + : import.meta.env.BASE_URL; + const res = await fetch(`${baseUrl}/plugin/canopy/manifest.json`, { signal: ac.signal }); + if (!res.ok) { + throw new Error(`Failed to load manifest: ${res.status} ${res.statusText}`); + } + + const data: Manifest = await res.json(); + setManifest(data); + } catch (err: any) { + if (err?.name !== 'AbortError') { + console.error('Error loading manifest:', err); + setError(err instanceof Error ? err.message : 'Failed to load manifest'); + } + } finally { + setLoading(false); + } + }; + + loadManifest(); + return () => ac.abort(); + }, []); + + const getActionById = useCallback((id: string): Action | undefined => { + if (!manifest) return undefined; + return manifest.actions.find(a => a.id === id); + }, [manifest]); + + const getActionsByKind = useCallback((kind: 'tx' | 'query'): Action[] => { + if (!manifest) return []; + return manifest.actions.filter(a => a.kind === kind); + }, [manifest]); + + const getVisibleActions = useCallback((): Action[] => { + if (!manifest) return []; + const sorted = [...manifest.actions] + .filter(a => !a.hidden) + .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || (a.order ?? 0) - (b.order ?? 0)); + const max = manifest.ui?.quickActions?.max; + return typeof max === 'number' ? sorted.slice(0, max) : sorted; + }, [manifest]); + + return { + manifest, + loading, + error, + getActionById, + getActionsByKind, + getVisibleActions, + }; +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useMultipleValidatorRewardsHistory.ts b/cmd/rpc/web/wallet-new/src/hooks/useMultipleValidatorRewardsHistory.ts new file mode 100644 index 000000000..07784cbb3 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useMultipleValidatorRewardsHistory.ts @@ -0,0 +1,105 @@ +import { useQuery } from '@tanstack/react-query'; +import { useHistoryCalculation, HistoryResult } from './useHistoryCalculation'; +import {useDSFetcher} from "@/core/dsFetch"; + +interface RewardEvent { + eventType: string; + msg: { + amount: number; + }; + height: number; + reference: string; + chainId: number; + address: string; +} + +interface EventsResponse { + pageNumber: number; + perPage: number; + results: RewardEvent[]; + type: string; + count: number; + totalPages: number; + totalCount: number; +} + +/** + * Hook to calculate rewards for multiple validators + * Fetches reward events and calculates total rewards earned in the last 24h + */ +export function useMultipleValidatorRewardsHistory(addresses: string[]) { + const dsFetch = useDSFetcher(); + const { currentHeight, height24hAgo, isReady } = useHistoryCalculation(); + + + return useQuery({ + queryKey: ['multipleValidatorRewardsHistory', addresses, currentHeight], + enabled: addresses.length > 0 && isReady, + staleTime: 30_000, + + queryFn: async (): Promise> => { + const results: Record = {}; + + // Fetch rewards for all validators in parallel + const validatorPromises = addresses.map(async (address) => { + try { + // Fetch all reward events for this validator + const eventsResponse = await dsFetch('events.byAddress', { + address, + height: 0, + page: 1, + perPage: 10000 // Large number to get all rewards + }); + + + // Handle both array format and object format + let allEvents: RewardEvent[] = []; + if (Array.isArray(eventsResponse)) { + allEvents = eventsResponse; + } else if (eventsResponse?.results) { + allEvents = eventsResponse.results; + } + + const rewardEvents = allEvents.filter(event => event.eventType === 'reward'); + + + // Calculate total rewards (all time) + const totalRewards = rewardEvents.reduce((sum, event) => sum + (event.msg?.amount || 0), 0); + + // Calculate rewards from the last 24h + const rewards24h = rewardEvents + .filter(event => + event.height > height24hAgo && + event.height <= currentHeight + ) + .reduce((sum, event) => sum + (event.msg?.amount || 0), 0); + + results[address] = { + current: rewards24h, + previous24h: 0, + change24h: rewards24h, + changePercentage: 0, + progressPercentage: 100, + rewards24h: rewards24h, + totalRewards: totalRewards + }; + } catch (error) { + console.error(`Error fetching rewards for ${address}:`, error); + results[address] = { + current: 0, + previous24h: 0, + change24h: 0, + changePercentage: 0, + progressPercentage: 0, + rewards24h: 0, + totalRewards: 0 + }; + } + }); + + await Promise.all(validatorPromises); + + return results; + } + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useNodes.ts b/cmd/rpc/web/wallet-new/src/hooks/useNodes.ts new file mode 100644 index 000000000..97a09247e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useNodes.ts @@ -0,0 +1,147 @@ +import { useQuery } from "@tanstack/react-query"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useConfig } from "@/app/providers/ConfigProvider"; + +export interface NodeInfo { + id: string; + name: string; + address: string; + isActive: boolean; + netAddress?: string; +} + +export interface NodeData { + height: any; + consensus: any; + peers: any; + resources: any; + logs: string; + validatorSet: any; +} + +/** + * Hook to get the current node info using DS pattern + * Uses the frontend's base URL configuration instead of discovering multiple nodes + */ +export const useAvailableNodes = () => { + const config = useConfig(); + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ["availableNodes"], + queryFn: async (): Promise => { + try { + // Fetch consensus info and validator set using DS pattern + const [consensusData, validatorSetData] = await Promise.all([ + dsFetch("admin.consensusInfo"), + dsFetch("validatorSet", { height: 0, committeeId: 1 }), + ]); + + // Try to find the validator by matching publicKey, or use the first validator if not found + let validator = validatorSetData?.validatorSet?.find( + (v: any) => v.publicKey === consensusData?.publicKey, + ); + + // If no matching validator found by publicKey, use the first available validator + if (!validator && validatorSetData?.validatorSet?.length > 0) { + validator = validatorSetData.validatorSet[0]; + } + + const netAddress = validator?.netAddress || "tcp://localhost"; + + // Extract the node name from netAddress (e.g., "tcp://localhost" -> "localhost") + let nodeName = netAddress.replace("tcp://", ""); + + // Only apply transformations if it's not a simple hostname like "localhost" + if (nodeName !== "localhost" && nodeName.includes("-")) { + nodeName = nodeName + .replace(/-/g, " ") + .replace(/\b\w/g, (l: string) => l.toUpperCase()); + } + + // Fallback name if extraction fails + if (!nodeName || nodeName === "current-node") { + nodeName = "Current Node"; + } + + return [ + { + id: "current_node", + name: nodeName, + address: consensusData?.address || "", + isActive: true, + netAddress: netAddress, + }, + ]; + } catch (error) { + console.log("Current node not available:", error); + + // Return a default node info even if there's an error + return [ + { + id: "current_node", + name: "localhost", + address: "", + isActive: false, + netAddress: "tcp://localhost", + }, + ]; + } + }, + refetchInterval: 10000, + staleTime: 5000, + retry: 1, + }); +}; + +/** + * Hook to fetch all node data for the current node using DS pattern + */ +export const useNodeData = (nodeId: string) => { + const config = useConfig(); + const dsFetch = useDSFetcher(); + const { data: availableNodes = [] } = useAvailableNodes(); + const selectedNode = + availableNodes.find((n) => n.id === nodeId) || availableNodes[0]; + + return useQuery({ + queryKey: ["nodeData", nodeId], + enabled: !!nodeId && !!selectedNode, + queryFn: async (): Promise => { + if (!selectedNode) throw new Error("Node not found"); + + try { + // Fetch all required data using DS pattern + const [ + heightData, + consensusData, + peerData, + resourceData, + logsData, + validatorSetData, + ] = await Promise.all([ + dsFetch("height"), + dsFetch("admin.consensusInfo"), + dsFetch("admin.peerInfo"), + dsFetch("admin.resourceUsage"), + dsFetch("admin.log"), + dsFetch("validatorSet", { height: 0, committeeId: 1 }), + ]); + + return { + height: heightData, + consensus: consensusData, + peers: peerData, + resources: resourceData, + logs: logsData, + validatorSet: validatorSetData, + }; + } catch (error) { + console.error(`Error fetching node data for ${nodeId}:`, error); + throw error; + } + }, + refetchInterval: 20000, + staleTime: 5000, + }); +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useStakedBalanceHistory.ts b/cmd/rpc/web/wallet-new/src/hooks/useStakedBalanceHistory.ts new file mode 100644 index 000000000..9c24831dc --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useStakedBalanceHistory.ts @@ -0,0 +1,47 @@ +import { useQuery } from '@tanstack/react-query' +import { useDSFetcher } from '@/core/dsFetch' +import { useHistoryCalculation, HistoryResult } from './useHistoryCalculation' +import {useAccounts} from "@/app/providers/AccountsProvider"; + +export function useStakedBalanceHistory() { + const { accounts, loading: accountsLoading } = useAccounts() + const addresses = accounts.map(a => a.address).filter(Boolean) + const dsFetch = useDSFetcher() + const { currentHeight, height24hAgo, calculateHistory, isReady } = useHistoryCalculation() + + return useQuery({ + queryKey: ['stakedBalanceHistory', addresses, currentHeight], + enabled: !accountsLoading && addresses.length > 0 && isReady, + staleTime: 30_000, + retry: 2, + retryDelay: 2000, + + queryFn: async (): Promise => { + if (addresses.length === 0) { + return { current: 0, previous24h: 0, change24h: 0, changePercentage: 0, progressPercentage: 0 } + } + + // Fetch current and previous staked amounts in parallel + const currentPromises = addresses.map(address => + dsFetch('validatorByHeight', { address, height: currentHeight }) + .then(v => v?.stakedAmount || 0) + .catch(() => 0) + ) + const previousPromises = addresses.map(address => + dsFetch('validatorByHeight', { address, height: height24hAgo }) + .then(v => v?.stakedAmount || 0) + .catch(() => 0) + ) + + const [currentStakes, previousStakes] = await Promise.all([ + Promise.all(currentPromises), + Promise.all(previousPromises), + ]) + + const currentTotal = currentStakes.reduce((sum, v) => sum + (v || 0), 0) + const previousTotal = previousStakes.reduce((sum, v) => sum + (v || 0), 0) + + return calculateHistory(currentTotal, previousTotal) + } + }) +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useStakingData.ts b/cmd/rpc/web/wallet-new/src/hooks/useStakingData.ts new file mode 100644 index 000000000..e78b68198 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useStakingData.ts @@ -0,0 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; +import { useValidators } from './useValidators'; +import { useMultipleValidatorRewardsHistory } from './useMultipleValidatorRewardsHistory'; +import { useDS } from '@/core/useDs'; +import {useAccounts} from "@/app/providers/AccountsProvider"; + +interface StakingInfo { + totalStaked: number; + totalRewards: number; + totalRewards24h: number; + stakingHistory: Array<{ + height: number; + staked: number; + rewards: number; + }>; + chartData: Array<{ + x: number; + y: number; + }>; +} + +export function useStakingData() { + const { accounts, loading: accountsLoading } = useAccounts(); + const { data: validators = [], isLoading: validatorsLoading } = useValidators(); + const { data: currentHeight = 0 } = useDS('height', {}, { staleTimeMs: 30_000 }); + const validatorAddresses = validators.map((v: any) => v.address); + const { data: rewardsHistory = {}, isLoading: rewardsLoading } = useMultipleValidatorRewardsHistory(validatorAddresses); + + return useQuery({ + queryKey: ['stakingData', accounts.map(acc => acc.address), validatorAddresses, rewardsHistory, currentHeight], + enabled: !accountsLoading && !validatorsLoading && accounts.length > 0, + queryFn: async (): Promise => { + if (accounts.length === 0 || validators.length === 0) { + return { totalStaked: 0, totalRewards: 0, totalRewards24h: 0, stakingHistory: [], chartData: [] }; + } + + const totalStaked = validators.reduce((sum: number, validator: any) => sum + (validator.stakedAmount || 0), 0); + let totalRewards24h = 0; + let totalRewards = 0; + + validators.forEach((validator: any) => { + const rewardData = rewardsHistory[validator.address]; + if (rewardData) { + totalRewards24h += rewardData.rewards24h || 0; + totalRewards += rewardData.totalRewards || 0; + } + }); + + const stakingHistory = []; + const chartData = []; + const dataPoints = 7; + + for (let i = 0; i < dataPoints; i++) { + const dayOffset = dataPoints - i - 1; + const height = currentHeight - (dayOffset * 4320); + const estimatedStaked = totalStaked - (totalRewards24h * dayOffset); + stakingHistory.push({ height, staked: Math.max(0, estimatedStaked), rewards: totalRewards24h * (i + 1) }); + chartData.push({ x: i, y: Math.max(0, estimatedStaked) }); + } + + return { totalStaked, totalRewards, totalRewards24h, stakingHistory, chartData }; + }, + staleTime: 30000, + retry: 2, + retryDelay: 2000, + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useTotalStage.ts b/cmd/rpc/web/wallet-new/src/hooks/useTotalStage.ts new file mode 100644 index 000000000..24f7ff9d2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useTotalStage.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useDSFetcher } from '@/core/dsFetch'; + +interface AccountBalance { + address: string; + amount: number; +} + +export function useTotalStage() { + const { accounts, loading: accountsLoading } = useAccounts(); + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ['totalStage', accounts.map(acc => acc.address)], + enabled: !accountsLoading && accounts.length > 0, + queryFn: async () => { + if (accounts.length === 0) return 0; + + const balancePromises = accounts.map(account => + dsFetch('account', { account: {address: account.address}, height: 0 }) + .then(data => data?.amount || 0) + .catch(err => { console.error(`Error fetching balance for ${account.address}:`, err); return 0; }) + ); + + const balances = await Promise.all(balancePromises); + return balances.reduce((sum, balance) => sum + balance, 0); + }, + staleTime: 10000, + retry: 2, + retryDelay: 1000, + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useTransactions.ts b/cmd/rpc/web/wallet-new/src/hooks/useTransactions.ts new file mode 100644 index 000000000..279e627e8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useTransactions.ts @@ -0,0 +1,110 @@ +import { useQuery } from "@tanstack/react-query"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useDSFetcher } from "@/core/dsFetch"; + +interface Transaction { + hash: string; + height: number; + time: number; + transaction: { + type: string; + from?: string; + to?: string; + amount?: number; + }; + fee: number; + memo?: string; + status?: string; +} + +interface TransactionResponse { + results: Transaction[]; + total: number; + pageNumber: number; + perPage: number; +} + +export function useTransactions() { + const { accounts, loading: accountsLoading } = useAccounts(); + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ["transactions", accounts.map((acc: any) => acc.address)], + enabled: !accountsLoading && accounts.length > 0, + queryFn: async () => { + if (accounts.length === 0) return []; + + try { + // Fetch transactions for all accounts + const allTransactions: Transaction[] = []; + + for (const account of accounts) { + const [sentTxsData, receivedTxsData, failedTxsData] = + await Promise.all([ + dsFetch("txs.sent", { + account: { address: account.address }, + page: 1, + perPage: 20, + }).catch((error) => { + console.error( + `Error fetching sent transactions for address ${account.address}:`, + error, + ); + return { results: [] }; + }), + dsFetch("txs.received", { + account: { address: account.address }, + page: 1, + perPage: 20, + }).catch((error) => { + console.error( + `Error fetching received transactions for address ${account.address}:`, + error, + ); + return { results: [] }; + }), + dsFetch("txs.failed", { + account: { address: account.address }, + page: 1, + perPage: 20, + }).catch((error) => { + console.error( + `Error fetching failed transactions for address ${account.address}:`, + error, + ); + return { results: [] }; + }), + ]); + + const sentTxs = sentTxsData.results || []; + const receivedTxs = receivedTxsData.results || []; + const failedTxs = failedTxsData.results || []; + + // Add status to transactions + sentTxs.forEach((tx: Transaction) => (tx.status = "included")); + receivedTxs.forEach((tx: Transaction) => (tx.status = "included")); + failedTxs.forEach((tx: Transaction) => (tx.status = "failed")); + + allTransactions.push(...sentTxs, ...receivedTxs, ...failedTxs); + } + + // Sort by time (most recent first) and remove duplicates + const uniqueTransactions = allTransactions + .filter( + (tx, index, self) => + index === self.findIndex((t) => t.hash === tx.hash), + ) + .sort((a, b) => b.time - a.time) + .slice(0, 10); // Get latest 10 transactions + + return uniqueTransactions; + } catch (error) { + console.error("Error fetching transactions:", error); + return []; + } + }, + staleTime: 10000, + retry: 2, + retryDelay: 1000, + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewards.ts b/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewards.ts new file mode 100644 index 000000000..fe0b836cc --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewards.ts @@ -0,0 +1,93 @@ +import { useDS } from "@/core/useDs"; +import { useMemo } from "react"; + +interface RewardEvent { + type: string; + msg: { + amount: number; + }; + height: number; + time: string; + ref: string; + chainId: string; + indexedAddress: string; +} + +interface RewardsData { + totalRewards: number; + rewardEvents: RewardEvent[]; + last24hRewards: number; + last7dRewards: number; + averageRewardPerBlock: number; +} + +export const useValidatorRewards = (address?: string) => { + const { + data: events = [], + isLoading, + error, + } = useDS( + "events.byAddress", + { address: address || "", page: 1, perPage: 1000 }, + { + enabled: !!address, + select: (data) => { + // Filter only reward events + if (Array.isArray(data)) { + return data.filter((event: any) => event.type === "reward"); + } + return []; + }, + }, + ); + + const rewardsData = useMemo(() => { + if (!events || events.length === 0) { + return { + totalRewards: 0, + rewardEvents: [], + last24hRewards: 0, + last7dRewards: 0, + averageRewardPerBlock: 0, + }; + } + + const now = Date.now(); + const oneDayAgo = now - 24 * 60 * 60 * 1000; + const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000; + + let totalRewards = 0; + let last24hRewards = 0; + let last7dRewards = 0; + + events.forEach((event: any) => { + const amount = event.msg?.amount || 0; + totalRewards += amount; + + const eventTime = new Date(event.time).getTime(); + if (eventTime >= oneDayAgo) { + last24hRewards += amount; + } + if (eventTime >= sevenDaysAgo) { + last7dRewards += amount; + } + }); + + const averageRewardPerBlock = + events.length > 0 ? totalRewards / events.length : 0; + + return { + totalRewards, + rewardEvents: events, + last24hRewards, + last7dRewards, + averageRewardPerBlock, + }; + }, [events]); + + return { + ...rewardsData, + isLoading, + error, + }; +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewardsHistory.ts b/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewardsHistory.ts new file mode 100644 index 000000000..700b11a68 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewardsHistory.ts @@ -0,0 +1,68 @@ +import { useQuery } from '@tanstack/react-query'; +import { useHistoryCalculation, HistoryResult } from './useHistoryCalculation'; +import {useDSFetcher} from "@/core/dsFetch"; + +interface RewardEvent { + eventType: string; + msg: { + amount: number; + }; + height: number; + reference: string; + chainId: number; + address: string; +} + +interface EventsResponse { + pageNumber: number; + perPage: number; + results: RewardEvent[]; + type: string; + count: number; + totalPages: number; + totalCount: number; +} + +/** + * Hook to calculate validator rewards using block height comparison + * Fetches reward events and calculates total rewards earned in the last 24h + */ +export function useValidatorRewardsHistory(address?: string) { + const dsFetch = useDSFetcher(); + const { currentHeight, height24hAgo, calculateHistory, isReady } = useHistoryCalculation(); + + return useQuery({ + queryKey: ['validatorRewardsHistory', address, currentHeight], + enabled: !!address && isReady, + staleTime: 30_000, + + queryFn: async (): Promise => { + // Fetch all reward events + const events = await dsFetch('events.byAddress', { + address, + height: 0, + page: 1, + perPage: 10000 // Large number to get all rewards + }); + + // Filter rewards from the last 24h (between height24hAgo and currentHeight) + const rewardsLast24h = events + .filter(event => + event.eventType === 'reward' && + event.height > height24hAgo && + event.height <= currentHeight + ) + .reduce((sum, event) => sum + (event.msg?.amount || 0), 0); + + // Return the total as both current and change24h + // This will display the actual rewards earned in the last 24h + return { + current: rewardsLast24h, + previous24h: 0, + change24h: rewardsLast24h, + changePercentage: 0, + progressPercentage: 100 + }; + } + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useValidatorSet.ts b/cmd/rpc/web/wallet-new/src/hooks/useValidatorSet.ts new file mode 100644 index 000000000..ff25c9c3c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useValidatorSet.ts @@ -0,0 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; +import { useDSFetcher } from "@/core/dsFetch"; + +interface ValidatorSetMember { + publicKey: string; + votingPower: number; + netAddress: string; +} + +interface ValidatorSetResponse { + validatorSet: ValidatorSetMember[]; +} + +/** + * Hook to fetch validator set data for a specific committee using DS pattern + * @param committeeId - The committee ID to fetch validator set for + * @param enabled - Whether the query should run + */ +export function useValidatorSet(committeeId: number, enabled: boolean = true) { + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ['validatorSet', committeeId], + enabled: enabled && committeeId !== undefined, + staleTime: 30_000, + queryFn: async (): Promise => { + return dsFetch('validatorSet', { + height: 0, + committeeId: committeeId + }); + } + }); +} + +/** + * Hook to fetch validator sets for multiple committees using DS pattern + * @param committeeIds - Array of committee IDs + */ +export function useMultipleValidatorSets(committeeIds: number[]) { + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ['multipleValidatorSets', committeeIds], + enabled: committeeIds.length > 0, + staleTime: 30_000, + queryFn: async (): Promise> => { + const results: Record = {}; + + // Fetch all validator sets in parallel + const promises = committeeIds.map(async (committeeId) => { + try { + const data = await dsFetch('validatorSet', { + height: 0, + committeeId: committeeId + }); + results[committeeId] = data; + } catch (error) { + console.error(`Error fetching validator set for committee ${committeeId}:`, error); + results[committeeId] = { validatorSet: [] }; + } + }); + + await Promise.all(promises); + return results; + } + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useValidators.ts b/cmd/rpc/web/wallet-new/src/hooks/useValidators.ts new file mode 100644 index 000000000..45319ff5a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useValidators.ts @@ -0,0 +1,76 @@ +import { useQuery } from "@tanstack/react-query"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useAccounts } from "@/app/providers/AccountsProvider"; + +interface Validator { + address: string; + publicKey: string; + stakedAmount: number; + unstakingAmount: number; + unstakingHeight: number; + pausedHeight: number; + unstaking: boolean; + paused: boolean; + delegate: boolean; + blocksProduced: number; + rewards24h: number; + stakeWeight: number; + weightChange: number; + nickname?: string; +} + +export function useValidators() { + const { accounts, loading: accountsLoading } = useAccounts(); + const dsFetch = useDSFetcher(); + + return useQuery({ + queryKey: ["validators", accounts.map((acc) => acc.address)], + enabled: !accountsLoading && accounts.length > 0, + queryFn: async (): Promise => { + try { + // Get all validators from the network using DS pattern + const allValidatorsResponse = await dsFetch("validators"); + const allValidators = allValidatorsResponse || []; + + // Filter validators that belong to our accounts + const accountAddresses = accounts.map((acc) => acc.address); + const ourValidators = allValidators.filter((validator: any) => + accountAddresses.includes(validator.address), + ); + + // Map to our interface + const validators: Validator[] = ourValidators.map((validator: any) => { + const account = accounts.find( + (acc) => acc.address === validator.address, + ); + return { + address: validator.address, + publicKey: validator.publicKey || "", + stakedAmount: validator.stakedAmount || 0, + unstakingAmount: validator.unstakingAmount || 0, + unstakingHeight: validator.unstakingHeight || 0, + pausedHeight: validator.maxPausedHeight || 0, + unstaking: validator.unstakingHeight > 0, + paused: validator.maxPausedHeight > 0, + delegate: validator.delegate || false, + blocksProduced: 0, // This would need to be calculated separately + rewards24h: 0, // This would need to be calculated separately + stakeWeight: 0, // This would need to be calculated separately + weightChange: 0, // This would need to be calculated separately + nickname: account?.nickname, + // Include all raw validator data to preserve committees, netAddress, etc. + ...validator, + }; + }); + + return validators; + } catch (error) { + console.error("Error fetching validators:", error); + return []; + } + }, + staleTime: 10000, + retry: 2, + retryDelay: 1000, + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useWallets.ts b/cmd/rpc/web/wallet-new/src/hooks/useWallets.ts new file mode 100644 index 000000000..b1fffa4be --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useWallets.ts @@ -0,0 +1,38 @@ +// src/hooks/useWallets.ts +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { QK } from '@/core/queryKeys'; +// import { makeRpc } from '@/core/rpc'; + +export type Wallet = { id: string; name: string; address: string; isActive?: boolean }; + +async function fetchWallets(): Promise { + // A: desde contexto + const { wallets } = (window as any).__configCtx ?? {}; + return (wallets ?? []) as Wallet[]; + + // B: desde Admin RPC + // const rpc = makeRpc('admin'); + // const res = await rpc.get<{ wallets: Wallet[] }>('/admin/wallets'); + // return res.wallets; +} + +export function useWallets() { + const qc = useQueryClient(); + + const query = useQuery({ + queryKey: QK.WALLETS, + queryFn: fetchWallets, + // Use the global refetch configuration every 20s + // staleTime and refetchOnWindowFocus are inherited from the global configuration + }); + + const activeWallet = query.data?.find(w => w.isActive); + + return { + data: query.data, + isLoading: query.isLoading, + error: query.error as Error | null, + activeWallet, + + }; +} diff --git a/cmd/rpc/web/wallet-new/src/index.css b/cmd/rpc/web/wallet-new/src/index.css new file mode 100644 index 000000000..603ba1797 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/index.css @@ -0,0 +1,73 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --primary: #22d3a6; + --primary-foreground: #1a1b23; +} + +html, +body, +#root { + font-family: "Inter", sans-serif; + overflow-x: hidden; + max-width: 100vw; + height: 100%; + margin: 0; + padding: 0; +} + +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +/* Smooth scrolling */ +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Custom scrollbar for sidebar and main content */ +.overflow-y-auto::-webkit-scrollbar { + width: 6px; +} + +.overflow-y-auto::-webkit-scrollbar-track { + background: transparent; +} + +.overflow-y-auto::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.overflow-y-auto::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + + +/* width */ +::-webkit-scrollbar { + width: 2px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: var(--primary-foreground); + + +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #ffffff; + border-radius: 5px; +} + diff --git a/cmd/rpc/web/wallet-new/src/main.tsx b/cmd/rpc/web/wallet-new/src/main.tsx new file mode 100644 index 000000000..e4e44ee6a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/main.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import App from './app/App' +import './index.css' +import "@radix-ui/themes/styles.css"; + + +const qc = new QueryClient({ + defaultOptions: { + queries: { + refetchInterval: 20000, // 20 seconds + refetchIntervalInBackground: true, // Continue to refetch in background + staleTime: 10000, // Data is considered stale after 10 seconds + refetchOnWindowFocus: true, // Update when the window regains focus + }, + }, +}) +createRoot(document.getElementById('root')!).render( + + + + + +) diff --git a/cmd/rpc/web/wallet-new/src/manifest/loader.ts b/cmd/rpc/web/wallet-new/src/manifest/loader.ts new file mode 100644 index 000000000..adc5b0140 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/manifest/loader.ts @@ -0,0 +1,57 @@ +import { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import type { Manifest } from './types' + +const DEFAULT_CHAIN = (import.meta.env.VITE_DEFAULT_CHAIN as string) || 'canopy' +const MODE = ((import.meta.env.VITE_CONFIG_MODE as string) || 'embedded') as 'embedded' | 'runtime' +const RUNTIME_URL = import.meta.env.VITE_PLUGIN_URL as string | undefined + +export function getPluginBase(chain = DEFAULT_CHAIN) { + if (MODE === 'runtime' && RUNTIME_URL) return `${RUNTIME_URL.replace(/\/$/, '')}/${chain}` + + // Use configured base path from Vite + // This will be /wallet/ in production and / in development + const baseUrl = import.meta.env.BASE_URL.endsWith('/') + ? import.meta.env.BASE_URL.slice(0, -1) + : import.meta.env.BASE_URL + + return `${baseUrl}/plugin/${chain}` +} + +async function fetchJson(url: string): Promise { + const res = await fetch(url) + if (!res.ok) throw new Error(`Failed ${res.status} ${url}`) + return res.json() as Promise +} + +export function useEmbeddedConfig(chain = DEFAULT_CHAIN) { + const base = useMemo(() => getPluginBase(chain), [chain]) + + const chainQ = useQuery({ + queryKey: ['chain', base], + queryFn: () => fetchJson(`${base}/chain.json`), + // Use the global refetch configuration every 20s + // The configuration data may change, so it's good to update it + }) + + const manifestQ = useQuery({ + queryKey: ['manifest', base], + enabled: !!chainQ.data, + queryFn: () => fetchJson(`${base}/manifest.json`), + // Use the global refetch configuration every 20s + // The manifest can change dynamically + }) + + // tiny bridge for places where global ctx is handy (e.g., validators) + if (typeof window !== 'undefined') { + ; (window as any).__configCtx = { chain: chainQ.data, manifest: manifestQ.data } + } + + return { + base, + chain: chainQ.data, + manifest: manifestQ.data, + isLoading: chainQ.isLoading || manifestQ.isLoading, + error: chainQ.error ?? manifestQ.error + } +} diff --git a/cmd/rpc/web/wallet-new/src/manifest/params.ts b/cmd/rpc/web/wallet-new/src/manifest/params.ts new file mode 100644 index 000000000..807a1d58c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/manifest/params.ts @@ -0,0 +1,41 @@ +import { useQueries } from '@tanstack/react-query' +import { template } from '@/core/templater' + +export function useNodeParams(chain?: any) { + const sources = chain?.params?.sources ?? [] + const queries = useQueries({ + queries: sources.map((s: { id: any; base: string; path: any; method: string; headers: any; encoding: string; body: any }) => ({ + queryKey: ['params', s.id, chain?.rpc], + enabled: !!chain, + queryFn: async () => { + const host = s.base === 'admin' ? chain!.rpc.admin! : chain!.rpc.base + const url = `${host}${s.path}` + const method = s.method ?? 'GET' + const headers = { ...(s.headers ?? {}) } + let body: string | undefined + const encoding = s.encoding ?? 'json' + if (method === 'POST') { + if (encoding === 'text') { + const raw = typeof s.body === 'string' ? s.body : JSON.stringify(s.body ?? {}) + body = template(raw, { chain }) + if (!headers['content-type']) headers['content-type'] = 'text/plain;charset=UTF-8' + } else { + const obj = template(s.body ?? {}, { chain }) + body = JSON.stringify(obj) + if (!headers['content-type']) headers['content-type'] = 'application/json' + } + } + const res = await fetch(url, { method, headers, body }) + const json = await res.json().catch(() => ({})) + if (!res.ok) throw Object.assign(new Error('params error'), { json }) + return json + }, + staleTime: chain?.params?.refresh?.staleTimeMs ?? 30_000 + })) + }) + + const loading = queries.some((q) => q.isLoading) + const error = queries.find((q) => q.error)?.error + const data = Object.fromEntries(queries.map((q, i) => [sources[i]?.id, q.data ?? {}])) + return { data, loading, error } +} diff --git a/cmd/rpc/web/wallet-new/src/manifest/types.ts b/cmd/rpc/web/wallet-new/src/manifest/types.ts new file mode 100644 index 000000000..5533f5faf --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/manifest/types.ts @@ -0,0 +1,332 @@ +/* =========================== + * Manifest & UI Core Types + * =========================== */ + +import React from "react"; + +export type Manifest = { + version: string; + ui?: { + quickActions?: { max?: number }; + tx: { + typeMap: Record; + typeIconMap: Record; + fundsWay: Record; + }; + }; + actions: Action[]; +}; + +export type PayloadValue = + | string + | { + value: string; + coerce?: "string" | "number" | "boolean"; + }; + +export type Action = { + id: string; + title?: string; // opcional si usas label + icon?: string; + kind: "tx" | "view" | "utility"; + tags?: string[]; + relatedActions?: string[]; + priority?: number; + order?: number; + requiresFeature?: string; + hidden?: boolean; + + ui?: { + variant?: "modal" | "page"; + icon?: string; + slots?: { modal?: { style: React.CSSProperties; className?: string } }; + }; + + // Wizard steps support + steps?: Array<{ + title?: string; + form?: { + fields: Field[]; + layout?: { + grid?: { cols?: number; gap?: number }; + aside?: { show?: boolean; width?: number }; + }; + }; + aside?: { + widget?: string; + }; + }>; + + // dynamic form + form?: { + fields: Field[]; + layout?: { + grid?: { cols?: number; gap?: number }; + aside?: { show?: boolean; width?: number }; + }; + info?: { + title: string; + items: { label: string; value: string; icons: string }[]; + }; + summary?: { + title: string; + items: { label: string; value: string; icons: string }[]; + }; + confirmation: { + btn: { + icon: string; + label: string; + }; + }; + }; + payload?: Record; + + // RPC configuration + rpc?: { + base: "rpc" | "admin"; + path: string; + method: string; + payload?: any; + }; + + // Paso de confirmación (opcional y simple) + confirm?: { + title?: string; + summary?: Array<{ label: string; value: string }>; + ctaLabel?: string; + danger?: boolean; + showPayload?: boolean; + payloadSource?: "rpc.payload" | "custom"; + payloadTemplate?: any; // si usas plantilla custom de confirmación + }; + + // Success configuration + success?: { + message?: string; + links?: Array<{ + label: string; + href: string; + }>; + }; + + auth?: { type: "sessionPassword" | "none" }; + + // Envío (tx o llamada) + submit?: Submit; +}; + +/* =========================== + * Fields + * =========================== */ + +export type FieldBase = { + id: string; + name: string; + label?: string; + help?: string; + placeholder?: string; + readOnly?: boolean; + required?: boolean; + disabled?: boolean; + value?: string; + // features: copy / paste / set (Max) + features?: FieldOp[]; + ds?: Record; +}; + +export type AddressField = FieldBase & { + type: "address"; +}; + +export type AmountField = FieldBase & { + type: "amount"; + min?: number; + max?: number; +}; + +export type TextField = FieldBase & { + type: "text" | "textarea"; +}; + +export type SwitchField = FieldBase & { + type: "switch"; +}; + +export type OptionCardField = FieldBase & { + type: "optionCard"; +}; + +export type DynamicHtml = FieldBase & { + type: "dynamicHtml"; + html: string; +}; + +export type OptionField = FieldBase & { + type: "option"; + inLine?: boolean; +}; + +export type TableSelectColumn = { + key: string; + title: string; + expr?: string; + position?: "right" | "left" | "center"; +}; + +export type TableRowAction = { + title?: string; + label?: string; + icon?: string; + showIf?: string; + emit?: { op: "set" | "copy"; field?: string; value?: string }; + position?: "right" | "left" | "center"; +}; + +export type TableSelectField = FieldBase & { + type: "tableSelect"; + id: string; + name: string; + label?: string; + help?: string; + required?: boolean; + readOnly?: boolean; + multiple?: boolean; + rowKey?: string; + columns: TableSelectColumn[]; + rows?: any[]; + source?: { uses: string; selector?: string }; // p.ej. {uses:'ds', selector:'committees'} + rowAction?: TableRowAction; +}; + +export type SelectField = FieldBase & { + type: "select"; + // Could be a json string or a list of options + options?: String | Array<{ label: string; value: string }>; +}; + +export type AdvancedSelectField = FieldBase & { + type: "advancedSelect"; + allowCreate?: boolean; + allowFreeInput?: boolean; + options?: Array<{ label: string; value: string }>; +}; + +export type Field = + | AddressField + | AmountField + | SwitchField + | OptionCardField + | OptionField + | TextField + | SelectField + | TableSelectField + | AdvancedSelectField + | DynamicHtml; + +/* =========================== + * Field Features (Ops) + * =========================== */ + +export type FieldOp = + | { id: string; op: "copy"; from: string } // copia al clipboard el valor resuelto + | { id: string; op: "paste" } // pega desde clipboard al field + | { id: string; op: "set"; field: string; value: string }; // setea un valor (p.ej. Max) + +/* =========================== + * UI Ops / Events + * =========================== */ + +export type UIOp = + | { op: "fetch"; source: SourceKey } // dispara un refetch/carga de DS al abrir + | { op: "notify"; message: string }; // opcional: mostrar toast/notificación + +/* =========================== + * Submit (HTTP) + * =========================== */ + +export type Submit = { + base: "rpc" | "admin"; + path: string; // p.ej. '/v1/admin/tx-send' + method?: "GET" | "POST"; + headers?: Record; + encoding?: "json" | "text"; + body?: any; // plantilla a resolver o valor literal +}; + +/* =========================== + * Sources y Selectors + * =========================== */ + +export type SourceRef = { + // de dónde sale el dato que vas a interpolar + uses: string; + // ruta dentro de la fuente (p.ej. 'fee.sendFee', 'amount', 'address') + selector?: string; +}; + +// claves comunes de tu DS actual; permite string libre para crecer sin tocar tipos +export type SourceKey = + | "account" + | "params" + | "fees" + | "height" + | "validators" + | "activity" + | "txs.sent" + | "txs.received" + | "gov.proposals" + | string; + +/* =========================== + * Fees (opcional, lo mínimo) + * =========================== */ + +export type FeeBuckets = { + [bucket: string]: { multiplier: number; default?: boolean }; +}; + +export type FeeProviderQuery = { + type: "query"; + base: "rpc" | "admin"; + path: string; + method?: "GET" | "POST"; + headers?: Record; + encoding?: "json" | "text"; + selector?: string; // p.ej. 'fee' dentro del response + cache?: { staleTimeMs?: number; refetchIntervalMs?: number }; +}; + +export type FeeProviderSimulate = { + type: "simulate"; + base: "rpc" | "admin"; + path: string; + method?: "GET" | "POST"; + headers?: Record; + encoding?: "json" | "text"; + body?: any; + gasAdjustment?: number; + gasPrice?: + | { type: "static"; value: string } + | { + type: "query"; + base: "rpc" | "admin"; + path: string; + selector?: string; + }; +}; + +export type FeeProvider = FeeProviderQuery | FeeProviderSimulate; + +/* =========================== + * Templater Context (doc) + * =========================== + * Tu resolvedor debe recibir, al menos, este shape: + * { + * chain: { displayName: string; fees?: any; ... }, + * form: Record, + * session: { password?: string; ... }, + * fees: { effective?: string|number; amount?: string|number }, + * account: { address: string; nickname?: string }, + * ds: Record // p.ej. ds.account.amount + * } + */ diff --git a/cmd/rpc/web/wallet-new/src/state/session.ts b/cmd/rpc/web/wallet-new/src/state/session.ts new file mode 100644 index 000000000..a306b99e8 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/state/session.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand' + +type SessionState = { + unlockedUntil: number + password?: string + address?: string + unlock: (address: string, password: string, ttlSec: number) => void + lock: () => void + isUnlocked: () => boolean +} + +export const useSession = create((set, get) => ({ + unlockedUntil: 0, + password: undefined, + address: undefined, + unlock: (address, password, ttlSec) => + set({ address, password, unlockedUntil: Date.now() + ttlSec * 1000 }), + lock: () => set({ password: undefined, unlockedUntil: 0 }), + isUnlocked: () => Date.now() < get().unlockedUntil && !!get().password, +})) + +export function attachIdleRenew(ttlSec: number) { + const renew = () => { + const s = useSession.getState() + if (s.password) useSession.setState({ unlockedUntil: Date.now() + ttlSec * 1000 }) + } + ;['click','keydown','mousemove','touchstart'].forEach(e => window.addEventListener(e, renew)) +} diff --git a/cmd/rpc/web/wallet-new/src/toast/DefaultToastItem.tsx b/cmd/rpc/web/wallet-new/src/toast/DefaultToastItem.tsx new file mode 100644 index 000000000..5163872eb --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/DefaultToastItem.tsx @@ -0,0 +1,116 @@ +// toast/DefaultToastItem.tsx +import React from "react"; +import { ToastRenderData } from "./types"; +import { X, CheckCircle2, XCircle, AlertTriangle, Info, Bell } from "lucide-react"; +import { motion } from "framer-motion"; + +const VARIANT_STYLES: Record, { + container: string; + icon: React.ReactNode; + iconBg: string; +}> = { + success: { + container: "bg-gradient-to-r from-bg-secondary to-bg-tertiary border-l-4 border-l-primary shadow-lg shadow-primary/20", + icon: , + iconBg: "bg-primary/20" + }, + error: { + container: "bg-gradient-to-r from-bg-secondary to-bg-tertiary border-l-4 border-l-red-500 shadow-lg shadow-red-500/20", + icon: , + iconBg: "bg-red-500/20" + }, + warning: { + container: "bg-gradient-to-r from-bg-secondary to-bg-tertiary border-l-4 border-l-orange-500 shadow-lg shadow-orange-500/20", + icon: , + iconBg: "bg-orange-500/20" + }, + info: { + container: "bg-gradient-to-r from-bg-secondary to-bg-tertiary border-l-4 border-l-blue-500 shadow-lg shadow-blue-500/20", + icon: , + iconBg: "bg-blue-500/20" + }, + neutral: { + container: "bg-gradient-to-r from-bg-secondary to-bg-tertiary border-l-4 border-l-gray-500 shadow-lg shadow-gray-500/10", + icon: , + iconBg: "bg-gray-500/20" + }, +}; + +export const DefaultToastItem: React.FC<{ + data: Required; + onClose: () => void; +}> = ({ data, onClose }) => { + const styles = VARIANT_STYLES[data.variant ?? "neutral"]; + + return ( + +
+ {/* Icon */} + + {data.icon || styles.icon} + + + {/* Content */} +
+ {data.title && ( +
+ {data.title} +
+ )} + {data.description && ( +
+ {data.description} +
+ )} + + {/* Actions */} + {!!data.actions?.length && ( +
+ {data.actions.map((a, i) => + a.type === "link" ? ( + + {a.label} + + ) : ( + + ) + )} +
+ )} +
+ + {/* Close Button */} + +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/toast/ToastContext.tsx b/cmd/rpc/web/wallet-new/src/toast/ToastContext.tsx new file mode 100644 index 000000000..5ec77e8e4 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/ToastContext.tsx @@ -0,0 +1,132 @@ +// toast/ToastContext.tsx +"use client"; +import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; +import { ToastApi, ToastTemplateOptions, ToastFromResultOptions } from "./types"; +import { renderTemplate } from "./utils"; +import { motion, AnimatePresence } from "framer-motion"; +import {DefaultToastItem} from "@/toast/DefaultToastItem"; + +type ToastState = { + queue: Array>; +}; + +type ProviderProps = { + children: React.ReactNode; + maxVisible?: number; + position?: "top-right" | "top-left" | "bottom-right" | "bottom-left"; + defaultDurationMs?: number; + renderItem?: (t: Required) => React.ReactNode; +}; + +const ToastContext = createContext(null); + +let _id = 0; +const genId = () => `t_${Date.now()}_${_id++}`; + +export const ToastProvider: React.FC = ({ + children, + maxVisible = 4, + position = "top-right", + defaultDurationMs = 300000, + renderItem, + }) => { + const [queue, setQueue] = useState([]); + const timers = useRef>({}); + + const scheduleAutoDismiss = useCallback((id: string, ms?: number, sticky?: boolean) => { + if (sticky) return; + const dur = typeof ms === "number" ? ms : defaultDurationMs; + timers.current[id] = setTimeout(() => { + setQueue((q) => q.filter((x) => x.id !== id)); + delete timers.current[id]; + }, dur); + }, [defaultDurationMs]); + + const add = useCallback((opts: ToastTemplateOptions, variant?: import("./types").ToastVariant) => { + const id = genId(); + const data = { + id, + title: opts.title != null ? renderTemplate(opts.title, opts.ctx) : undefined, + description: opts.description != null ? renderTemplate(opts.description, opts.ctx) : undefined, + icon: opts.icon, + actions: opts.actions, + variant: variant ?? opts.variant ?? "neutral", + durationMs: opts.durationMs, + sticky: opts.sticky ?? false, + } as Required; + setQueue((q) => { + const next = [data, ...q]; + return next.slice(0, maxVisible); + }); + scheduleAutoDismiss(id, data.durationMs, data.sticky); + return id; + }, [maxVisible, scheduleAutoDismiss]); + + const dismiss = useCallback((id: string) => { + if (timers.current[id]) { + clearTimeout(timers.current[id]); + delete timers.current[id]; + } + setQueue((q) => q.filter((x) => x.id !== id)); + }, []); + + const clear = useCallback(() => { + Object.values(timers.current).forEach(clearTimeout); + timers.current = {}; + setQueue([]); + }, []); + + const fromResult = useCallback(({ result, ctx, map, fallback }: ToastFromResultOptions) => { + const mapped = map?.(result as R, ctx); + if (!mapped && !fallback) return null; + return add(mapped ?? fallback!, mapped?.variant); + }, [add]); + + const api = useMemo(() => ({ + toast: (t) => add(t, t.variant), + success: (t) => add({ ...t, variant: "success" }), + error: (t) => add({ ...t, variant: "error" }), + info: (t) => add({ ...t, variant: "info" }), + warning: (t) => add({ ...t, variant: "warning" }), + neutral: (t) => add({ ...t, variant: "neutral" }), + fromResult, + dismiss, + clear, + }), [add, dismiss, clear, fromResult]); + + const posClasses = { + "top-right": "top-4 right-4", + "top-left": "top-4 left-4", + "bottom-right": "bottom-4 right-4", + "bottom-left": "bottom-4 left-4", + }[position]; + + return ( + + {children} + {/* Container */} +
+ + {queue.map((t) => + + {renderItem ? renderItem(t) : dismiss(t.id)} />} + + )} + +
+
+ ); +}; + +export const useToast = () => { + const ctx = useContext(ToastContext); + if (!ctx) throw new Error("useToast must be used within "); + return ctx; +}; diff --git a/cmd/rpc/web/wallet-new/src/toast/manifestRuntime.ts b/cmd/rpc/web/wallet-new/src/toast/manifestRuntime.ts new file mode 100644 index 000000000..669ea1be4 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/manifestRuntime.ts @@ -0,0 +1,57 @@ +// toast/manifestRuntime.ts +import { template } from "@/core/templater"; +import { ToastTemplateOptions } from "@/toast/types"; + +const maybeTpl = (v: any, data: any) => + typeof v === "string" ? template(v, data) : v; + +export type NotificationNode = Partial & { + actions?: Array< + | { type: "link"; label: string; href: string; newTab?: boolean } + | { type: "button"; label: string; onClickId?: string } // opcional: callback id + >; +}; + +export function resolveToastFromManifest( + action: any, + key: "onInit" | "onBeforeSubmit" | "onSuccess" | "onError" | "onFinally", + ctx: any, + result?: any +): ToastTemplateOptions | null { + const node: NotificationNode | undefined = action?.notifications?.[key]; + if (!node) return null; + + const data = { ...ctx, result }; + const rendered: ToastTemplateOptions = { + variant: node.variant, + title: maybeTpl(node.title, data), + description: maybeTpl(node.description, data), + icon: node.icon, + sticky: node.sticky, + durationMs: node.durationMs, + actions: node.actions?.map((a) => + a.type === "link" + ? { ...a, href: maybeTpl(a.href, data), label: maybeTpl(a.label, data) } + : { ...a, label: maybeTpl(a.label, data) } + ), + ctx: data + }; + return rendered; +} + +export function resolveRedirectFromManifest( + action: any, + ctx: any, + result: any +): { to: string; delayMs?: number; replace?: boolean } | null { + const r = action?.redirect; + if (!r) return null; + const should = + r.when === "always" || + (r.when === "success" && (result?.ok ?? true)) || + (r.when === "error" && !(result?.ok ?? true)); + if (!should) return null; + + const to = template(r.to, { ...ctx, result }); + return { to, delayMs: r.delayMs ?? 0, replace: !!r.replace }; +} diff --git a/cmd/rpc/web/wallet-new/src/toast/mappers.tsx b/cmd/rpc/web/wallet-new/src/toast/mappers.tsx new file mode 100644 index 000000000..b5052957e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/mappers.tsx @@ -0,0 +1,146 @@ +// toast/mappers.tsx +import React from "react"; +import { ToastTemplateOptions } from "./types"; +import { Pause, Play } from "lucide-react"; + +export const genericResultMap = ( + r: R, + ctx: any +): ToastTemplateOptions => { + if (r.ok) { + return { + variant: "success", + title: "Done", + description: typeof r.data?.message === "string" + ? r.data.message + : "The operation completed successfully.", + ctx, + }; + } + // error pathway + const code = r.status ?? r.error?.code ?? "ERR"; + const msg = + r.error?.message ?? + r.error?.reason ?? + r.data?.message ?? + "We couldn't complete your request."; + return { + variant: "error", + title: `Something went wrong (${code})`, + description: msg, + ctx, + sticky: true, + }; +}; + +// Mapper for pause validator action +export const pauseValidatorMap = ( + r: R, + ctx: any +): ToastTemplateOptions => { + // Handle string response (transaction hash) + if (typeof r === 'string') { + const validatorAddr = ctx?.form?.validatorAddress || "Validator"; + const shortAddr = validatorAddr.length > 12 + ? `${validatorAddr.slice(0, 6)}...${validatorAddr.slice(-4)}` + : validatorAddr; + + return { + variant: "success", + title: "Validator Paused Successfully", + description: `Validator ${shortAddr} has been paused. The validator will stop producing blocks until resumed. Transaction: ${r.slice(0, 8)}...${r.slice(-6)}`, + icon: , + ctx, + durationMs: 5000, + }; + } + + // Handle object response + if (typeof r === 'object' && r.ok) { + const validatorAddr = ctx?.form?.validatorAddress || "Validator"; + const shortAddr = validatorAddr.length > 12 + ? `${validatorAddr.slice(0, 6)}...${validatorAddr.slice(-4)}` + : validatorAddr; + + return { + variant: "success", + title: "Validator Paused Successfully", + description: `Validator ${shortAddr} has been paused. The validator will stop producing blocks until resumed.`, + icon: , + ctx, + durationMs: 5000, + }; + } + + const code = (r as any).status ?? (r as any).error?.code ?? "ERR"; + const msg = + (r as any).error?.message ?? + (r as any).error?.reason ?? + (r as any).data?.message ?? + "Failed to pause validator. Please check your connection and try again."; + + return { + variant: "error", + title: "Pause Failed", + description: `${msg} (${code})`, + icon: , + ctx, + sticky: true, + }; +}; + +// Mapper for unpause validator action +export const unpauseValidatorMap = ( + r: R, + ctx: any +): ToastTemplateOptions => { + // Handle string response (transaction hash) + if (typeof r === 'string') { + const validatorAddr = ctx?.form?.validatorAddress || "Validator"; + const shortAddr = validatorAddr.length > 12 + ? `${validatorAddr.slice(0, 6)}...${validatorAddr.slice(-4)}` + : validatorAddr; + + return { + variant: "success", + title: "Validator Resumed Successfully", + description: `Validator ${shortAddr} is now active and will resume producing blocks. Transaction: ${r.slice(0, 8)}...${r.slice(-6)}`, + icon: , + ctx, + durationMs: 5000, + }; + } + + // Handle object response + if (typeof r === 'object' && r.ok) { + const validatorAddr = ctx?.form?.validatorAddress || "Validator"; + const shortAddr = validatorAddr.length > 12 + ? `${validatorAddr.slice(0, 6)}...${validatorAddr.slice(-4)}` + : validatorAddr; + + return { + variant: "success", + title: "Validator Resumed Successfully", + description: `Validator ${shortAddr} is now active and will resume producing blocks.`, + icon: , + ctx, + durationMs: 5000, + }; + } + + const code = (r as any).status ?? (r as any).error?.code ?? "ERR"; + const msg = + (r as any).error?.message ?? + (r as any).error?.reason ?? + (r as any).data?.message ?? + "Failed to resume validator. Please check your connection and try again."; + + return { + variant: "error", + title: "Resume Failed", + description: `${msg} (${code})`, + icon: , + ctx, + sticky: true, + }; +}; diff --git a/cmd/rpc/web/wallet-new/src/toast/types.ts b/cmd/rpc/web/wallet-new/src/toast/types.ts new file mode 100644 index 000000000..cf71334ea --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/types.ts @@ -0,0 +1,50 @@ +export type ToastVariant = "success" | "error" | "warning" | "info" | "neutral"; + +export type ToastAction = + | { type: "link"; label: string; href: string; newTab?: boolean } + | { type: "button"; label: string; onClick: () => void }; + +export type ToastRenderData = { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + icon?: React.ReactNode; + actions?: ToastAction[]; + variant?: ToastVariant; + durationMs?: number; // auto-dismiss + sticky?: boolean; // no auto-dismiss +}; + +export type ToastTemplateInput = + | string // "Hello {{user.name}}" + | ((ctx: any) => string) // (ctx) => `Hello ${ctx.user.name}` + | React.ReactNode; // + +export type ToastTemplateOptions = Omit< + ToastRenderData, + "title" | "description" | "id" +> & { + id?: string; + title?: ToastTemplateInput; + description?: ToastTemplateInput; + ctx?: any; // Action Runner ctx +}; + +export type ToastFromResultOptions = { + result: R; + ctx?: any; + map?: (r: R, ctx: any) => ToastTemplateOptions | null | undefined; + fallback?: ToastTemplateOptions; +}; + +export type ToastApi = { + toast: (t: ToastTemplateOptions) => string; + success: (t: ToastTemplateOptions) => string; + error: (t: ToastTemplateOptions) => string; + info: (t: ToastTemplateOptions) => string; + warning: (t: ToastTemplateOptions) => string; + neutral: (t: ToastTemplateOptions) => string; + fromResult: (o: ToastFromResultOptions) => string | null; + dismiss: (id: string) => void; + clear: () => void; +}; diff --git a/cmd/rpc/web/wallet-new/src/toast/utils.ts b/cmd/rpc/web/wallet-new/src/toast/utils.ts new file mode 100644 index 000000000..7a142f54a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/utils.ts @@ -0,0 +1,17 @@ +// toast/utils.ts +import {ToastTemplateInput} from "@/toast/types"; + +export const getAt = (o: any, p?: string) => + !p ? o : p.split(".").reduce((a, k) => (a ? a[k] : undefined), o); + +const interpolate = (tpl: string, ctx: any) => + tpl.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, path) => { + const v = getAt(ctx, path.trim()); + return v == null ? "" : String(v); + }); + +export const renderTemplate = (input: ToastTemplateInput, ctx?: any): React.ReactNode => { + if (typeof input === "function") return (input as any)(ctx); + if (typeof input === "string") return ctx ? interpolate(input, ctx) : input; + return input; // ReactNode passthrough +}; diff --git a/cmd/rpc/web/wallet-new/src/ui/cx.ts b/cmd/rpc/web/wallet-new/src/ui/cx.ts new file mode 100644 index 000000000..4dfe8aca3 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/ui/cx.ts @@ -0,0 +1,3 @@ +import { twMerge } from 'tailwind-merge' +import clsx from 'clsx' +export const cx = (...args: any[]) => twMerge(clsx(args)) diff --git a/cmd/rpc/web/wallet-new/tailwind.config.js b/cmd/rpc/web/wallet-new/tailwind.config.js new file mode 100644 index 000000000..b91200731 --- /dev/null +++ b/cmd/rpc/web/wallet-new/tailwind.config.js @@ -0,0 +1,128 @@ +module.exports = { + content: [ + "./src/**/*.{html,js,ts,jsx,tsx}", + "app/**/*.{ts,tsx}", + "components/**/*.{ts,tsx}", + ], + theme: { + extend: { + colors: { + // Canopy Wallet Brand Colors + canopy: { + 50: '#f0fdf9', + 100: '#ccfbef', + 200: '#99f6e0', + 300: '#5eead4', + 400: '#2dd4bf', + 500: '#14b8a6', + 600: '#0d9488', + 700: '#0f766e', + 800: '#115e59', + 900: '#134e4a', + 950: '#042f2e', + }, + // Background Colors + bg: { + primary: '#1a1b23', + secondary: '#22232e', + tertiary: '#2b2c38', + accent: '#2a2b35', + }, + // Text Colors + text: { + primary: '#ffffff', + secondary: '#e5e7eb', + muted: '#9ca3af', + accent: '#6fe3b4', + }, + // Status Colors + status: { + success: '#10b981', + warning: '#f59e0b', + error: '#ef4444', + info: '#3b82f6', + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "#6fe3b4", + foreground: "#1a1b23", + 50: "#f0fdf9", + 100: "#ccfbef", + 200: "#99f6e0", + 300: "#5eead4", + 400: "#2dd4bf", + 500: "#6fe3b4", + 600: "#0d9488", + 700: "#0f766e", + 800: "#115e59", + 900: "#134e4a", + }, + secondary: { + DEFAULT: "#22232e", + foreground: "#ffffff", + }, + destructive: { + DEFAULT: "#ef4444", + foreground: "#ffffff", + }, + muted: { + DEFAULT: "#2b2c38", + foreground: "#9ca3af", + }, + accent: { + DEFAULT: "#6fe3b4", + foreground: "#1a1b23", + }, + popover: { + DEFAULT: "#22232e", + foreground: "#ffffff", + }, + card: { + DEFAULT: "#22232e", + foreground: "#ffffff", + }, + }, + spacing: { + '18': '4.5rem', + '88': '22rem', + }, + fontSize: { + 'xs': ['0.75rem', { lineHeight: '1rem' }], + 'sm': ['0.875rem', { lineHeight: '1.25rem' }], + 'base': ['1rem', { lineHeight: '1.5rem' }], + 'lg': ['1.125rem', { lineHeight: '1.75rem' }], + 'xl': ['1.25rem', { lineHeight: '1.75rem' }], + '2xl': ['1.5rem', { lineHeight: '2rem' }], + '3xl': ['1.875rem', { lineHeight: '2.25rem' }], + }, + boxShadow: { + 'wallet': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + 'wallet-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + }, + fontFamily: { + inter: ["Inter", "sans-serif"], + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + container: { center: true, padding: "2rem", screens: { "2xl": "1400px" } }, + }, + plugins: [], + darkMode: ["class"], +}; diff --git a/cmd/rpc/web/wallet-new/tsconfig.json b/cmd/rpc/web/wallet-new/tsconfig.json new file mode 100644 index 000000000..2b63e9713 --- /dev/null +++ b/cmd/rpc/web/wallet-new/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "types": [ + "vite/client" + ], + "baseUrl": "src", + "paths": { "@/*": ["*"] } + + }, + "include": [ + "src" + ] +} diff --git a/cmd/rpc/web/wallet-new/vite.config.ts b/cmd/rpc/web/wallet-new/vite.config.ts new file mode 100644 index 000000000..98bf1f0c9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/vite.config.ts @@ -0,0 +1,47 @@ +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vite.dev/config/ +export default defineConfig(({ mode }) => { + // Load env file based on `mode` in the current working directory. + const env = loadEnv(mode, ".", ""); + + // Determine base path based on environment + // Priority: VITE_BASE_PATH env var > production default > development default + const getBasePath = () => { + // If explicitly set via environment variable, use it + if (env.VITE_BASE_PATH) { + return env.VITE_BASE_PATH; + } + // In development, use / for local dev + if (mode === "development") { + return "/"; + } + // In production, use /wallet/ because the app is served behind a reverse proxy + // at http://node1.localhost/wallet/ + // This ensures: + // 1. Assets are requested as /wallet/assets/... (Traefik strips /wallet, Go server gets /assets/...) + // 2. React Router basename is /wallet (matches browser URL) + return "/wallet/"; + }; + + return { + base: getBasePath(), + resolve: { + alias: { + "@": "/src", + }, + }, + plugins: [react()], + build: { + outDir: "out", + assetsDir: "assets", + }, + define: { + // Ensure environment variables are available at build time + "import.meta.env.VITE_NODE_ENV": JSON.stringify( + env.VITE_NODE_ENV || "development", + ), + }, + }; +}); diff --git a/cmd/rpc/web/wallet/package.json b/cmd/rpc/web/wallet/package.json index 3d0fe4ff4..f610ac974 100644 --- a/cmd/rpc/web/wallet/package.json +++ b/cmd/rpc/web/wallet/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 50000", + "dev": "next dev -p 3000", "build": "next build", "start": "next start -p 50000", "lint": "next lint",