diff --git a/.docker/Dockerfile b/.docker/Dockerfile index cae842eb1..0a92043ea 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -2,9 +2,21 @@ FROM golang:1.24-alpine AS builder RUN apk update && apk add --no-cache make bash nodejs npm +# Wallet-specific build configuration +# Default: / (for simple-stack deployment with direct port access) +ARG VITE_WALLET_BASE_PATH=/ +# RPC proxy targets for chain.json generation +ARG VITE_WALLET_RPC_PROXY_TARGET=http://localhost:50002 +ARG VITE_WALLET_ADMIN_RPC_PROXY_TARGET=http://localhost:50003 + WORKDIR /go/src/github.com/canopy-network/canopy COPY . /go/src/github.com/canopy-network/canopy +# Export build configuration to environment +ENV VITE_WALLET_BASE_PATH=${VITE_WALLET_BASE_PATH} +ENV VITE_WALLET_RPC_PROXY_TARGET=${VITE_WALLET_RPC_PROXY_TARGET} +ENV VITE_WALLET_ADMIN_RPC_PROXY_TARGET=${VITE_WALLET_ADMIN_RPC_PROXY_TARGET} + RUN make build/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..0f99f518d 100644 --- a/.docker/compose.yaml +++ b/.docker/compose.yaml @@ -4,38 +4,48 @@ services: build: context: .. dockerfile: .docker/Dockerfile + args: + VITE_WALLET_BASE_PATH: / + VITE_EXPLORER_BASE_PATH: / + VITE_WALLET_RPC_PROXY_TARGET: http://localhost:50002 + VITE_WALLET_ADMIN_RPC_PROXY_TARGET: http://localhost:50003 ports: - 50000:50000 # Wallet - 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 build: context: .. dockerfile: .docker/Dockerfile + args: + VITE_WALLET_BASE_PATH: / + VITE_EXPLORER_BASE_PATH: / + VITE_WALLET_RPC_PROXY_TARGET: http://localhost:40002 + VITE_WALLET_ADMIN_RPC_PROXY_TARGET: http://localhost:40003 ports: - 40000:40000 # Wallet - 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 +82,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..babac6a10 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM golang:1.24-alpine AS builder + +RUN apk update && apk add --no-cache make bash nodejs npm + +ARG BIN_PATH +# Wallet-specific build configuration +# Default: /wallet/ (for monitoring-stack deployment with Traefik reverse proxy) +# Override with: docker build --build-arg VITE_WALLET_BASE_PATH=/ +ARG VITE_WALLET_BASE_PATH=/wallet/ +# RPC proxy targets for chain.json generation +# For monitoring-stack, these should be Traefik URLs +ARG VITE_WALLET_RPC_PROXY_TARGET=/wallet/rpc +ARG VITE_WALLET_ADMIN_RPC_PROXY_TARGET=/wallet/adminrpc + +WORKDIR /go/src/github.com/canopy-network/canopy +COPY . /go/src/github.com/canopy-network/canopy + +# Export build configuration to environment +# These are available during npm build for wallet and explorer +ENV VITE_WALLET_BASE_PATH=${VITE_WALLET_BASE_PATH} +ENV VITE_WALLET_RPC_PROXY_TARGET=${VITE_WALLET_RPC_PROXY_TARGET} +ENV VITE_WALLET_ADMIN_RPC_PROXY_TARGET=${VITE_WALLET_ADMIN_RPC_PROXY_TARGET} + +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 7e04113e2..faff491b7 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ GO_BIN_DIR := ~/go/bin CLI_DIR := ./cmd/main/... AUTO_UPDATE_DIR := ./cmd/auto-update/... -WALLET_DIR := ./cmd/rpc/web/wallet +WALLET_DIR := ./cmd/rpc/web/wallet-new EXPLORER_DIR := ./cmd/rpc/web/explorer DOCKER_DIR := ./.docker/compose.yaml @@ -37,6 +37,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 7f47afd94..ee39fcb20 100644 --- a/cmd/rpc/server.go +++ b/cmd/rpc/server.go @@ -36,7 +36,7 @@ const ( ContentType = "Content-MessageType" ApplicationJSON = "application/json; charset=utf-8" - walletStaticDir = "web/wallet/out" + walletStaticDir = "web/wallet-new/out" explorerStaticDir = "web/explorer/out" ) @@ -338,10 +338,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) @@ -355,20 +355,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 { @@ -383,11 +376,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/.agents/skills/config-first-architect/references/form-engine-patterns.md b/cmd/rpc/web/wallet-new/.agents/skills/config-first-architect/references/form-engine-patterns.md new file mode 100644 index 000000000..1670966f0 --- /dev/null +++ b/cmd/rpc/web/wallet-new/.agents/skills/config-first-architect/references/form-engine-patterns.md @@ -0,0 +1,169 @@ +# Dynamic Form Engine Design Patterns + +## Core Concept + +A form engine takes a **manifest** (JSON/YAML) and produces a functional form. +The engine handles: rendering, validation, conditional logic, state, submission. +Components know nothing about business rules. + +--- + +## Field Schema Contract + +```typescript +interface FieldSchema { + id: string + type: 'text' | 'number' | 'select' | 'date' | 'boolean' | 'file' | 'group' | 'repeat' + label: string + placeholder?: string + defaultValue?: unknown + required?: boolean | ConditionalExpression + disabled?: boolean | ConditionalExpression + hidden?: boolean | ConditionalExpression + validation?: ValidationRule[] + options?: OptionSource // for select/radio/checkbox + dependsOn?: string[] // fields this field reacts to + meta?: Record // renderer hints (layout, icons, etc.) +} + +interface ConditionalExpression { + when: string // field id + operator: 'eq' | 'neq' | 'gt' | 'lt' | 'in' | 'notIn' | 'exists' + value: unknown +} + +interface ValidationRule { + type: 'required' | 'min' | 'max' | 'pattern' | 'custom' | 'async' + value?: unknown + message: string + ref?: string // for 'custom': points to registered validator key +} +``` + +--- + +## Form Manifest Contract + +```typescript +interface FormManifest { + version: string // semver, e.g. "1.0.0" + id: string + title?: string + fields: FieldSchema[] + layout?: LayoutDescriptor + actions: ActionDescriptor[] + submission: SubmissionDescriptor +} + +interface SubmissionDescriptor { + endpoint: string // reference to config endpoint key, not raw URL + method: 'POST' | 'PUT' | 'PATCH' + payloadTransform?: string // optional transform key registered in engine + onSuccess: string // action key: 'redirect', 'notify', 'reset' + onError: string // action key +} +``` + +--- + +## Engine Implementation Pattern + +```typescript +// The engine is a pure function pipeline +class FormEngine { + constructor( + private config: SystemConfig, + private validators: ValidatorRegistry, + private actions: ActionRegistry, + ) {} + + parse(raw: unknown): FormManifest { + // 1. Validate against JSON Schema / Zod + // 2. Normalize defaults + // 3. Resolve endpoint references from config + return parsed + } + + resolve(manifest: FormManifest, context: FormContext): ResolvedForm { + // 1. Evaluate all conditional expressions with current field values + // 2. Resolve dynamic options (from API or config) + // 3. Apply permission guards + return resolvedForm + } + + validate(values: Record, manifest: FormManifest): ValidationResult { + // 1. Run field-level validators + // 2. Run cross-field validators + // 3. Return structured errors + return { valid: boolean, errors: FieldErrors } + } + + async submit(values: Record, manifest: FormManifest): Promise { + // 1. Final validation pass + // 2. Apply payload transform if defined + // 3. Resolve endpoint from config + // 4. Execute HTTP action + // 5. Run success/error action + } +} +``` + +--- + +## Conditional Logic Evaluation + +Never use `eval()`. Use a safe expression evaluator: + +```typescript +function evaluateCondition( + expr: ConditionalExpression, + values: Record +): boolean { + const fieldValue = values[expr.when] + switch (expr.operator) { + case 'eq': return fieldValue === expr.value + case 'neq': return fieldValue !== expr.value + case 'in': return Array.isArray(expr.value) && expr.value.includes(fieldValue) + case 'exists': return fieldValue !== undefined && fieldValue !== null && fieldValue !== '' + // ... + } +} +``` + +--- + +## Common Patterns + +### Field Dependency Graph +Build a DAG from `dependsOn` fields to determine re-evaluation order. +When field A changes, only re-resolve fields that depend on A. + +### Option Sources +```typescript +type OptionSource = + | { type: 'static'; items: Option[] } + | { type: 'config'; key: string } // from system config + | { type: 'api'; endpoint: string; transform: string } // endpoint = config key + | { type: 'context'; path: string } // from form context/state +``` + +### Payload Transforms +Register named transforms in the engine, reference by key in manifest: +```typescript +engine.registerTransform('snakeCase', (values) => toSnakeCase(values)) +engine.registerTransform('dateFormat', (values) => formatDates(values)) +``` + +### Repeatable Groups +Fields of type `repeat` render N instances of a sub-schema. +Store as array in form state. Each instance is an independent field group. + +--- + +## Anti-Patterns to Avoid + +❌ `if (formId === 'checkout') { ... }` — hardcoded form-specific logic +❌ Fetching endpoints directly in components — use config keys +❌ Validation logic in components — belongs in engine +❌ `eval()` or `new Function()` for conditionals — security risk +❌ Mixing form state with app state — keep isolated diff --git a/cmd/rpc/web/wallet-new/.agents/skills/config-first-architect/references/manifest-examples.md b/cmd/rpc/web/wallet-new/.agents/skills/config-first-architect/references/manifest-examples.md new file mode 100644 index 000000000..cf94f7d1e --- /dev/null +++ b/cmd/rpc/web/wallet-new/.agents/skills/config-first-architect/references/manifest-examples.md @@ -0,0 +1,202 @@ +# Annotated Manifest Examples + +Real-world examples across different domains showing config-first patterns in practice. + +--- + +## Example 1: Dynamic Form Manifest (Checkout Flow) + +```json +{ + "version": "1.0.0", + "id": "checkout-shipping", + "title": "Shipping Information", + "fields": [ + { + "id": "country", + "type": "select", + "label": "Country", + "required": true, + "options": { + "type": "config", + "key": "supportedCountries" // resolved from system config at runtime + } + }, + { + "id": "state", + "type": "select", + "label": "State / Province", + "required": { "when": "country", "operator": "in", "value": ["US", "CA"] }, + "hidden": { "when": "country", "operator": "notIn", "value": ["US", "CA"] }, + "options": { + "type": "api", + "endpoint": "statesEndpoint", // key resolved from config, not raw URL + "transform": "countryStates" // registered transform in engine + }, + "dependsOn": ["country"] + }, + { + "id": "postalCode", + "type": "text", + "label": "Postal Code", + "validation": [ + { + "type": "pattern", + "value": "^[0-9]{5}(-[0-9]{4})?$", + "message": "Enter a valid US postal code", + "when": { "field": "country", "operator": "eq", "value": "US" } + } + ] + } + ], + "submission": { + "endpoint": "checkoutApiEndpoint", + "method": "POST", + "payloadTransform": "checkoutShippingPayload", + "onSuccess": "navigateToPayment", + "onError": "showInlineError" + } +} +``` + +**Key patterns shown:** +- Options sourced from config (not hardcoded arrays) +- Conditional required/hidden driven by other field values +- Validation rules with conditions +- Submission pointing to config keys, not raw URLs + +--- + +## Example 2: Multi-Step Flow Manifest + +```json +{ + "version": "1.0.0", + "id": "onboarding-flow", + "title": "User Onboarding", + "steps": [ + { + "id": "profile", + "title": "Your Profile", + "form": "onboarding-profile-form", // references a form manifest + "guards": [], + "transitions": { + "next": "preferences", + "skip": { "target": "finish", "condition": { "featureFlag": "allowSkipOnboarding" } } + } + }, + { + "id": "preferences", + "title": "Your Preferences", + "form": "onboarding-preferences-form", + "guards": [ + { "permission": "canSetPreferences", "fallback": "finish" } + ], + "transitions": { + "next": "finish", + "back": "profile" + } + }, + { + "id": "finish", + "type": "confirmation", + "title": "All done!", + "actions": [ + { "type": "dispatch", "key": "completeOnboarding" } + ] + } + ] +} +``` + +**Key patterns shown:** +- Step transitions are data, not code +- Feature flags control flow variations +- Permission guards with fallback targets +- Forms are referenced by ID, not embedded + +--- + +## Example 3: System Configuration + +```json +{ + "version": "1.0.0", + "environment": "production", + "features": { + "allowSkipOnboarding": false, + "experimentalDashboard": true, + "maxUploadSizeMb": 10 + }, + "endpoints": { + "checkoutApiEndpoint": "https://api.example.com/v2/checkout", + "statesEndpoint": "https://api.example.com/v1/geo/states", + "authEndpoint": "https://auth.example.com/token" + }, + "supportedCountries": ["US", "CA", "MX", "GB", "DE"], + "permissions": { + "canSetPreferences": ["admin", "user"], + "canSkipOnboarding": ["admin"] + }, + "theme": { + "primaryColor": "#2563EB", + "borderRadius": "0.5rem", + "fontFamily": "Inter, sans-serif" + } +} +``` + +**Key patterns shown:** +- Feature flags as first-class citizens +- All endpoints centralized (single place to change) +- Permissions defined declaratively +- Theme tokens in config (not in CSS or components) + +--- + +## Example 4: Plugin / Extension Manifest + +```json +{ + "version": "1.0.0", + "id": "crm-integration", + "type": "integration-plugin", + "triggers": ["form.submit", "user.created"], + "actions": [ + { + "on": "form.submit", + "filter": { "formId": "contact-form" }, + "execute": { + "type": "http", + "endpoint": "crmWebhookEndpoint", + "method": "POST", + "payloadTransform": "crmContactPayload" + } + } + ], + "requiredConfig": ["crmWebhookEndpoint"], + "optionalConfig": ["crmTagPrefix"] +} +``` + +**Key patterns shown:** +- Plugin behavior described entirely in manifest +- Event-based triggers (decoupled) +- Required config declared (engine can validate at boot) +- No code in the manifest — pure declarative intent + +--- + +## Reading These Examples + +Notice what's **absent** in every manifest: +- No `if/else` statements +- No function references +- No hardcoded URLs or values +- No UI concerns (colors, classes, layout details) + +And what's always **present**: +- `version` field +- References to config keys (not raw values) +- Declarative intent ("what", never "how") +- IDs that enable referencing from other manifests diff --git a/cmd/rpc/web/wallet-new/.env.example b/cmd/rpc/web/wallet-new/.env.example new file mode 100644 index 000000000..71dc36845 --- /dev/null +++ b/cmd/rpc/web/wallet-new/.env.example @@ -0,0 +1,22 @@ +# Wallet Base Path Configuration +# This sets the base URL path for the wallet in production builds +# +# Examples: +# - For deployment at https://example.com/wallet/ use: VITE_WALLET_BASE_PATH=/wallet/ +# - For deployment at https://wallet.example.com/ use: VITE_WALLET_BASE_PATH=/ +# - For deployment at root domain use: VITE_WALLET_BASE_PATH=/ +# +# Default: /wallet/ +VITE_WALLET_BASE_PATH=/wallet/ + +# RPC Proxy Targets (Development Server Only) +# Used by Vite dev server proxy configuration +VITE_WALLET_RPC_PROXY_TARGET=http://localhost:50002 +VITE_WALLET_ADMIN_RPC_PROXY_TARGET=http://localhost:50003 +# Root chain RPC target - used for cross-chain order queries (available orders, orders to fulfill) +VITE_ROOT_WALLET_RPC_PROXY_TARGET=http://localhost:50002 + +# 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..c1038ee47 --- /dev/null +++ b/cmd/rpc/web/wallet-new/.gitignore @@ -0,0 +1,12 @@ +.agents/* +.claude/* +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..dd0c21d8a --- /dev/null +++ b/cmd/rpc/web/wallet-new/README.md @@ -0,0 +1,451 @@ +# 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 + +### Declarative Architecture Docs +- **Declarative Actions + Chain Guide**: `docs/declarative-actions-and-chain.md` +- Covers the config-first runtime flow, purpose of `chain.json` and `manifest.json`, and implementation guidelines. + +## 🖥️ 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/docs/declarative-actions-and-chain.md b/cmd/rpc/web/wallet-new/docs/declarative-actions-and-chain.md new file mode 100644 index 000000000..ed6d3601c --- /dev/null +++ b/cmd/rpc/web/wallet-new/docs/declarative-actions-and-chain.md @@ -0,0 +1,242 @@ +# Declarative Actions and Chain Configuration + +This wallet follows a **config-first architecture**: behavior is defined in configuration files, and the runtime executes that configuration. + +The two core files are: + +- `public/plugin/canopy/chain.json` +- `public/plugin/canopy/manifest.json` + +## Why This Exists + +The goal is to avoid hardcoded blockchain behavior in UI components. + +With this model, you can: + +- Change RPC endpoints without rewriting app code. +- Add or modify wallet actions (send, stake, governance, etc.) without creating new React forms manually. +- Keep business rules in a single, auditable configuration layer. +- Reuse the same runtime engine across different chains or environments. + +## `chain.json`: Network and Data Contract + +`chain.json` defines chain-level and API-level behavior: + +- RPC base URLs (`rpc`, `admin`) +- denomination metadata (`base`, `symbol`, `decimals`) +- explorers and links +- data sources (`ds`) used by hooks and dynamic forms +- fee providers and session settings + +Think of `chain.json` as the **integration contract** with the blockchain. + +## `manifest.json`: Declarative UI + Action Contract + +`manifest.json` defines what the app can do and how forms behave: + +- `actions[]`: each user operation (send, stake, edit stake, governance vote, etc.) +- dynamic fields, validation, and conditional visibility +- wizard steps and confirmation summaries +- payload mapping and coercion +- submit endpoint (`base`, `path`, `method`) +- success/error notifications + +Think of `manifest.json` as the **interaction contract** between user intent and transaction payloads. + +## End-to-End Runtime Flow + +1. User opens an action. +2. Runtime loads action definition from `manifest.json`. +3. Runtime resolves DS dependencies from `chain.json` (`ds.*` calls). +4. Form is rendered dynamically from field config. +5. Values are validated/coerced according to manifest rules. +6. Payload is built declaratively. +7. Request is submitted to the configured endpoint. +8. Notifications and UI state are resolved from action config. + +## Runtime Layers in Practice + +In this wallet, declarative actions are executed by a runtime pipeline: + +- `ActionModalProvider` / `ActionsModal`: opens and hosts action UI. +- `ActionRunner`: orchestrates DS fetch, form state, payload build, submit, and notifications. +- `FormRenderer` + field registry: resolves and renders each field type. +- `usePopulateController`: controls initialization/autopopulate phases and DS readiness. + +This separation is intentional: + +- UI shell remains generic. +- business behavior lives in config. +- dynamic data and validation are resolved centrally. + +## Example: Stake / Edit Stake + +The `stake` action can route to different endpoints based on current state: + +- New validator -> `/v1/admin/tx-stake` +- Existing validator -> `/v1/admin/tx-edit-stake` + +This is defined in manifest submit config, not in component code. + +The same action can also adapt: + +- field requirements +- labels/help text +- payload normalization + +based on whether `ds.validator` exists. + +## Field Types (Manifest-Driven) + +Field rendering is mapped through a field registry. Current types include: + +- `text`, `textarea` +- `amount`, `number`, `address` +- `select`, `advancedSelect` +- `switch`, `option`, `optionCard` +- `tableSelect` +- `dynamicHtml` +- layout/structure: `section`, `divider`, `spacer`, `heading`, `collapsibleGroup` + +Why this matters: + +- new flows can be created by reusing existing field types in manifest. +- runtime does not need action-specific React forms. +- unsupported types fail safely and are visible during integration. + +## Modal Data Population and Prefill Model + +When opening a dynamic action modal, data can come from multiple sources: + +1. action defaults (`field.value`) +2. DS results (`ds.*`) +3. runtime/session context (`account`, `chain`, `fees`, `session`) +4. explicit `prefilledData` passed when opening the action (for edit flows) + +### Population phases + +Population is not a single step. It follows phases: + +- `waiting`: critical DS is not ready, form can show loading/skeleton behavior. +- `initializing`: first safe population pass from templates/defaults. +- `ready`: form becomes interactive; only explicit `autoPopulate: "always"` keeps updating from DS. + +### Precedence and safety + +- `prefilledData` is treated as authoritative for those fields during init. +- default templates do not overwrite prefilled fields. +- after initialization, DS updates do not blindly overwrite user input unless field explicitly opts in with `autoPopulate: "always"`. + +This avoids race conditions and accidental input loss when DS refreshes. + +## Responsive Behavior (Declarative + Runtime) + +Responsive form layout is controlled through field span config and runtime helpers. + +### Grid model + +- Forms render in a 12-column grid. +- Each field can define `span` (or `ui.grid.colSpan`) with breakpoints: + - `base`, `sm`, `md`, `lg`, `xl` + +Example: + +```json +{ + "id": "fee", + "name": "fee", + "type": "amount", + "span": { "base": 12, "md": 6 } +} +``` + +Runtime behavior: + +- mobile-first: defaults to full width on small screens. +- breakpoints progressively apply wider multi-column layouts. +- tabs and modal content regions are scroll-safe to prevent overflow. + +### Modal responsiveness + +Dynamic action modals are designed to: + +- keep viewport constraints (`dvh`-based max height). +- maintain internal scrolling in content region. +- preserve close controls and headers. +- avoid sidebar/topbar overlap through controlled overlay stacking. + +## Data Source (DS) Interaction Pattern + +DS definitions live in `chain.json`. Action DS blocks reference them declaratively. + +Typical usage: + +- action declares DS dependencies (`account`, `validator`, `keystore`, etc.). +- DS options control staleness/refetch behavior (`staleTimeMs`, `refetchIntervalMs`, `watch`, `critical`). +- templates consume DS via `{{ ds.someKey.someValue }}`. + +This provides consistent data-fetch semantics for all actions. + +## Template and Coercion Model + +Templates are resolved at runtime using context objects: + +- `form` +- `ds` +- `account` +- `chain` +- `fees` +- `params` +- `session` + +Payload fields can define coercion: + +- `string` +- `number` +- `boolean` +- raw objects where needed + +For blockchain payloads, this is critical for safe numeric conversions +(e.g., display denom -> micro denom) and type correctness. + +## Design Rules for New Actions + +When adding a new action, keep these rules: + +- Put business behavior in manifest, not React components. +- Keep payload transformations explicit (`coerce`, conversion helpers, guards). +- Fetch external state via DS, never inline endpoint strings in UI components. +- Prefer conditional config (`showIf`, dynamic `required`, templated values) over branching UI code. +- Keep confirmation summaries and notifications in config so users can review intent before submit. + +Additional rules for dynamic modals/forms: + +- Use `prefilledData` for edit workflows instead of hardcoding edit components. +- Use `critical` DS keys when form correctness depends on external data. +- Use `autoPopulate: "once"` for default suggestions and `"always"` only when continuous sync is required. +- Keep long help text and labels concise to preserve mobile readability. + +## Benefits and Tradeoffs + +Benefits: + +- Faster iteration for product/business rules. +- Lower risk of UI/backend drift. +- Easier portability to new chains. + +Tradeoffs: + +- More responsibility on config quality. +- Requires strict validation and review of manifest changes. +- Debugging often means inspecting resolved templates and DS outputs. + +## Recommended Review Checklist + +Before shipping manifest or chain changes: + +- Validate JSON syntax. +- Verify DS endpoints, methods, and selectors. +- Confirm numeric conversions (micro/base denom) are correct. +- Confirm `submit.path` and `payload` align with RPC contract. +- Test both success and failure toasts/messages. +- Test responsive behavior for long labels/help/validation messages. 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..2ec6bb078 --- /dev/null +++ b/cmd/rpc/web/wallet-new/package-lock.json @@ -0,0 +1,5693 @@ +{ + "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-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@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/react-tabs": "^1.1.13", + "@radix-ui/react-visually-hidden": "^1.2.4", + "@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" + } + }, + "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-accessible-icon/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/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-form/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-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.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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-label/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "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-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-navigation-menu/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/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-select/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/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-toast/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/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-tooltip/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/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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.4.tgz", + "integrity": "sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "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/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.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/radix-ui/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/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/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..5b87e7957 --- /dev/null +++ b/cmd/rpc/web/wallet-new/package.json @@ -0,0 +1,52 @@ +{ + "name": "canopy-wallet", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "prebuild": "node scripts/generate-chain-config.js", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@number-flow/react": "^0.5.10", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@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/react-tabs": "^1.1.13", + "@radix-ui/react-visually-hidden": "^1.2.4", + "@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..880ca4c90 --- /dev/null +++ b/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json @@ -0,0 +1,624 @@ +{ + "version": "1", + "chainId": "1", + "displayName": "Canopy", + "denom": { + "base": "ucnpy", + "symbol": "CNPY", + "decimals": 6 + }, + "rpc": { + "base": "http://localhost:50002", + "admin": "http://localhost:50003", + "root": "http://localhost:50002" + }, + "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 + } + } + } + }, + "orders": { + "bySeller": { + "source": { + "base": "rpc", + "path": "/v1/query/orders", + "method": "POST" + }, + "body": { + "height": 0, + "sellersSendAddress": "{{account.address}}", + "pageNumber": "{{page}}", + "perPage": "{{perPage}}" + }, + "coerce": { + "body": { + "height": "int", + "pageNumber": "int", + "perPage": "int" + } + }, + "selector": "" + }, + "byCommittee": { + "source": { + "base": "rpc", + "path": "/v1/query/orders", + "method": "POST" + }, + "body": { + "height": 0, + "committee": "{{committee}}", + "pageNumber": "{{page}}", + "perPage": "{{perPage}}" + }, + "coerce": { + "ctx": { + "committee": "int" + }, + "body": { + "height": "int", + "committee": "int", + "pageNumber": "int", + "perPage": "int" + } + }, + "selector": "" + }, + "byBuyer": { + "source": { + "base": "rpc", + "path": "/v1/query/orders", + "method": "POST" + }, + "body": { + "height": 0, + "buyerSendAddress": "{{account.address}}", + "committee": "{{committee}}", + "pageNumber": "{{page}}", + "perPage": "{{perPage}}" + }, + "coerce": { + "ctx": { + "committee": "int" + }, + "body": { + "height": "int", + "committee": "int", + "pageNumber": "int", + "perPage": "int" + } + }, + "selector": "" + }, + "single": { + "source": { + "base": "rpc", + "path": "/v1/query/order", + "method": "POST" + }, + "body": { + "height": 0, + "orderId": "{{orderId}}", + "committee": "{{committee}}" + }, + "coerce": { + "ctx": { + "committee": "int" + }, + "body": { + "height": "int", + "committee": "int" + } + }, + "selector": "" + } + }, + "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": { + "poll": { + "source": { + "base": "rpc", + "path": "/v1/gov/poll", + "method": "GET" + }, + "selector": "" + }, + "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, + "default": true + } + } + }, + "features": [ + "staking", + "gov" + ], + "session": { + "unlockTimeoutSec": 900, + "rePromptSensitive": false, + "persistAcrossTabs": false + } +} \ No newline at end of file diff --git a/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json.template b/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json.template new file mode 100644 index 000000000..d6fc02309 --- /dev/null +++ b/cmd/rpc/web/wallet-new/public/plugin/canopy/chain.json.template @@ -0,0 +1,325 @@ +{ + "version": "1", + "chainId": "1", + "displayName": "Canopy", + "denom": { + "base": "ucnpy", + "symbol": "CNPY", + "decimals": 6 + }, + "rpc": { + "base": "/rpc", + "admin": "/adminrpc", + "root": "/rootrpc" + }, + "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 } + } + } + }, + "orders": { + "bySeller": { + "source": { "base": "rpc", "path": "/v1/query/orders", "method": "POST" }, + "body": { + "height": 0, + "sellersSendAddress": "{{account.address}}", + "pageNumber": "{{page}}", + "perPage": "{{perPage}}" + }, + "coerce": { + "body": { + "height": "int", + "pageNumber": "int", + "perPage": "int" + } + }, + "selector": "" + }, + "byCommittee": { + "source": { "base": "rpc", "path": "/v1/query/orders", "method": "POST" }, + "body": { + "height": 0, + "committee": "{{committee}}", + "pageNumber": "{{page}}", + "perPage": "{{perPage}}" + }, + "coerce": { + "ctx": { "committee": "int" }, + "body": { + "height": "int", + "committee": "int", + "pageNumber": "int", + "perPage": "int" + } + }, + "selector": "" + }, + "byBuyer": { + "source": { "base": "rpc", "path": "/v1/query/orders", "method": "POST" }, + "body": { + "height": 0, + "buyerSendAddress": "{{account.address}}", + "committee": "{{committee}}", + "pageNumber": "{{page}}", + "perPage": "{{perPage}}" + }, + "coerce": { + "ctx": { "committee": "int" }, + "body": { + "height": "int", + "committee": "int", + "pageNumber": "int", + "perPage": "int" + } + }, + "selector": "" + }, + "single": { + "source": { "base": "rpc", "path": "/v1/query/order", "method": "POST" }, + "body": { + "height": 0, + "orderId": "{{orderId}}", + "committee": "{{committee}}" + }, + "coerce": { + "ctx": { "committee": "int" }, + "body": { + "height": "int", + "committee": "int" + } + }, + "selector": "" + } + }, + + "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": { + "poll": { + "source": { "base": "rpc", "path": "/v1/gov/poll", "method": "GET" }, + "selector": "" + }, + "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..60fe75f93 --- /dev/null +++ b/cmd/rpc/web/wallet-new/public/plugin/canopy/manifest.json @@ -0,0 +1,5489 @@ +{ + "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", + "changeParam": "Change Parameter", + "daoTransfer": "DAO Transfer", + "deleteVote": "Remove Vote", + "votePoll": "Vote Poll", + "createPoll": "Create Poll", + "createOrder": "Create Order", + "editOrder": "Reprice Order", + "deleteOrder": "Void Order", + "lockOrder": "Lock Order", + "closeOrder": "Close Order", + "dexLimitOrder": "DEX Limit Order", + "dexLiquidityDeposit": "DEX Liquidity Deposit", + "dexLiquidityWithdraw": "DEX Liquidity Withdraw" + }, + "typeIconMap": { + "editStake": "Lock", + "send": "Send", + "stake": "Lock", + "certificateResults": "ShieldCheck", + "unstake": "Unlock", + "unpause": "Play", + "pause": "Pause", + "receive": "Download", + "vote": "Vote", + "createProposal": "FileText", + "changeParam": "Settings", + "daoTransfer": "Coins", + "deleteVote": "XCircle", + "votePoll": "CheckSquare", + "createPoll": "BarChart3", + "createOrder": "PlusCircle", + "editOrder": "Pencil", + "deleteOrder": "Trash2", + "lockOrder": "Lock", + "closeOrder": "CheckCircle2", + "dexLimitOrder": "ArrowLeftRight", + "dexLiquidityDeposit": "Droplets", + "dexLiquidityWithdraw": "CircleDashed" + }, + "fundsWay": { + "editStake": "out", + "send": "out", + "stake": "out", + "unstake": "in", + "receive": "in", + "vote": "neutral", + "createProposal": "out", + "changeParam": "neutral", + "daoTransfer": "neutral", + "deleteVote": "neutral", + "votePoll": "neutral", + "createPoll": "neutral", + "createOrder": "out", + "editOrder": "neutral", + "deleteOrder": "neutral", + "lockOrder": "neutral", + "closeOrder": "neutral", + "dexLimitOrder": "neutral", + "dexLiquidityDeposit": "out", + "dexLiquidityWithdraw": "in" + } + } + }, + "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, + "span": { "base": 12 } + }, + { + "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" + } + ], + "span": { "base": 12 } + }, + { + "type": "divider", + "variant": "gradient", + "spacing": "md", + "span": { "base": 12 } + }, + { + "id": "asset", + "name": "asset", + "type": "text", + "label": "Asset", + "value": "{{chain.displayName}} (Balance: {{formatToCoin<{{ds.account.amount}}>}})", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "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}}> }}" + } + ], + "span": { "base": 12 } + } + ], + "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" + ], + "critical": ["keystore"] + } + }, + "ui": { + "slots": { + "modal": { + "className": "" + } + } + }, + "tags": [ + "quick" + ], + "form": { + "wizard": { + "steps": [ + { + "id": "setup", + "title": "Setup" + }, + { + "id": "committees", + "title": "Committees" + } + ] + }, + "fields": [ + { + "type": "heading", + "text": "Addresses", + "level": 3, + "color": "secondary", + "span": { "base": 12 }, + "step": "setup" + }, + { + "id": "operator", + "span": { + "base": 12 + }, + "name": "operator", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Staking (Operator) Address", + "placeholder": "Select Staking Address", + "required": true, + "value": "{{ params.operator || '' }}", + "autoPopulate": "once", + "validation": { + "messages": { + "required": "Operator address is required" + } + }, + "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" + }, + { + "type": "section", + "title": "Validator Information", + "description": "Staked: {{formatToCoin<{{ds.validator.stakedAmount ?? 0}}>}} {{chain.denom.symbol}} • Committees: {{ds.validator.committees ?? 'N/A'}} • Type: {{ ds.validator.delegate ? 'Delegation' : 'Validation' }}", + "icon": "Info", + "variant": "default", + "showIf": "{{ form.operator && ds.validator }}", + "step": "setup", + "span": { + "base": 12 + } + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 }, + "step": "setup" + }, + { + "id": "output", + "span": { + "base": 12 + }, + "name": "output", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Rewards Address", + "placeholder": "Select Rewards Address", + "required": true, + "validation": { + "messages": { + "required": "Rewards address is required" + } + }, + "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", + "autoPopulate": "once", + "inLine": true, + "borders": false, + "options": [ + { + "label": "Operator", + "value": "operator", + "help": "Staked Address" + }, + { + "label": "Reward", + "value": "reward", + "help": "Output Address" + } + ], + "step": "setup" + }, + { + "type": "section", + "title": "Signer Account Balance: {{formatToCoin<{{ds.account.amount ?? 0}}>}} {{chain.denom.symbol}}", + "description": "Available balance for transaction fees and additional stake", + "icon": "Wallet", + "variant": "info", + "showIf": "{{ form.signerResponsible && form.operator && form.output }}", + "step": "setup", + "span": { + "base": 12 + } + }, + { + "type": "divider", + "label": "Stake Amount", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 }, + "step": "setup" + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "placeholder": "0.00", + "label": "{{ ds.validator ? 'New Stake Amount (optional)' : 'Amount' }}", + "value": "{{ ds.validator ? fromMicroDenom<{{ds.validator.stakedAmount}}> : 0 }}", + "autoPopulate": "once", + "required": "{{ !ds.validator }}", + "min": 0, + "max": "{{ fromMicroDenom<{{(ds.account?.amount ?? 0) + (ds.validator?.stakedAmount ?? 0)}}> }}", + "help": "{{ ds.validator ? 'For edit stake: if you enter an amount lower/equal to current stake, blockchain keeps current stake and only updates settings (committees/output/compound).' : 'Amount to lock for a new validator/delegation. Blockchain enforces minimum stake.' }}", + "validation": { + "min": 0, + "messages": { + "required": "Amount is required for new stakes", + "min": "Amount must be greater than or equal to {{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" + }, + { + "type": "section", + "title": "Current Stake: {{numberToLocaleString<{{fromMicroDenom<{{ds.validator.stakedAmount}}>}}>}} CNPY", + "description": "Edit stake follows blockchain behavior: amount <= current stake will not decrease locked stake; it only applies configuration updates.", + "icon": "TrendingUp", + "variant": "success", + "showIf": "{{ ds.validator }}", + "step": "setup", + "span": { + "base": 12 + } + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 }, + "step": "setup", + "showIf": "{{ ds.validator }}" + }, + { + "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" + }, + { + "type": "section", + "title": "About Committees", + "description": "Select the committees you want to delegate to. You can select up to 15 committees per validator. Your stake will be distributed across all selected committees.", + "icon": "Info", + "variant": "info", + "span": { "base": 12 }, + "step": "committees" + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 }, + "step": "committees" + }, + { + "id": "selectCommittees", + "name": "selectCommittees", + "type": "tableSelect", + "label": "Select Committees", + "required": true, + "help": "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": "{{ params.selectCommittees || 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": "showAdvancedCommittees", + "name": "showAdvancedCommittees", + "type": "collapsibleGroup", + "title": "Advanced Options", + "description": "Manual committee configuration", + "icon": "Settings", + "variant": "default", + "defaultExpanded": false, + "step": "committees", + "span": { "base": 12 } + }, + { + "id": "manualCommittees", + "name": "manualCommittees", + "type": "text", + "label": "Committees IDs (manual override)", + "placeholder": "1,2,3", + "help": "Enter comma separated committee IDs to manually override selection", + "value": "{{ Array.isArray(form.selectCommittees) ? form.selectCommittees.join(',') : (ds.validator?.committees ? (Array.isArray(ds.validator.committees) ? ds.validator.committees.join(',') : ds.validator.committees) : '') }}", + "readOnly": false, + "showIf": "{{ form.showAdvancedCommittees }}", + "step": "committees" + }, + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "showIf": "{{ form.isDelegate === false || form.isDelegate === 'false' }}", + "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" + }, + { + "type": "divider", + "label": "Transaction Fee", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 }, + "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}}> }}", + "span": { "base": 12 }, + "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 === false || form?.isDelegate === 'false') ? 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 === false || form?.isDelegate === 'false') ? form.validatorAddress : ''}}", + "coerce": "string" + }, + "amount": { + "value": "{{(() => { const entered = Number(form.amount ?? 0); const current = Number(fromMicroDenom(ds.validator?.stakedAmount ?? 0)); const effective = ds.validator ? Math.max(entered, current) : entered; return toMicroDenom(effective); })()}}", + "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": [ + { + "type": "section", + "title": "Voting Information", + "description": "Your vote will be recorded on-chain and cannot be changed after submission. Ensure you review the proposal details before voting.", + "icon": "Info", + "variant": "info", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "id": "proposalId", + "name": "proposalId", + "type": "text", + "label": "Proposal ID", + "required": true, + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Proposal ID is required" + } + } + }, + { + "type": "divider", + "label": "Your Decision", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 } + }, + { + "id": "vote", + "name": "vote", + "type": "option", + "label": "Your Vote", + "required": true, + "span": { "base": 12 }, + "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" + } + ] + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "voterAddress", + "name": "voterAddress", + "type": "text", + "label": "Voter Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + } + ], + "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": [ + { + "type": "section", + "title": "Proposal Requirements", + "description": "Proposals require a minimum deposit and will be open for community voting. Deposits are returned if the proposal passes or is rejected normally.", + "icon": "Info", + "variant": "info", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "type": "heading", + "text": "Proposal Details", + "level": 3, + "color": "secondary", + "span": { "base": 12 } + }, + { + "id": "title", + "name": "title", + "type": "text", + "label": "Proposal Title", + "required": true, + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Title is required" + } + } + }, + { + "id": "description", + "name": "description", + "type": "textarea", + "label": "Description", + "required": true, + "rows": 5, + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Description is required" + } + } + }, + { + "type": "divider", + "label": "Submission Details", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 } + }, + { + "id": "proposerAddress", + "name": "proposerAddress", + "type": "text", + "label": "Proposer Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "deposit", + "name": "deposit", + "type": "amount", + "label": "Deposit Amount", + "required": true, + "min": 0, + "span": { "base": 12 }, + "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": [ + { + "type": "section", + "title": "Pause Duration Limit", + "description": "Maximum pause duration: 4,380 blocks (~24.3 hours). If not unpaused within this period, the validator will be automatically unstaked.", + "icon": "AlertTriangle", + "variant": "warning", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "readOnly": true, + "help": "The address of the validator to pause", + "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", + "span": { "base": 12 } + }, + { + "type": "divider", + "spacing": "md", + "span": { "base": 12 } + }, + { + "id": "memo", + "name": "memo", + "type": "text", + "label": "Memo (Optional)", + "required": false, + "placeholder": "Add a note about this pause action", + "span": { "base": 12 } + }, + { + "id": "txFee", + "name": "txFee", + "type": "amount", + "label": "Transaction Fee", + "value": "{{ fromMicroDenom<{{fees.raw.pauseFee}}> }}", + "autoPopulate": "always", + "required": true, + "min": "{{ fromMicroDenom<{{fees.raw.pauseFee}}> }}", + "span": { "base": 12 }, + "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": [ + { + "type": "section", + "title": "Resume Validator Operations", + "description": "This action will resume your validator's participation in consensus and block production. Make sure your node is fully synced before unpausing.", + "icon": "Info", + "variant": "success", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "readOnly": true, + "help": "The address of the validator to unpause", + "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", + "span": { "base": 12 } + }, + { + "type": "divider", + "spacing": "md", + "span": { "base": 12 } + }, + { + "id": "memo", + "name": "memo", + "type": "text", + "label": "Memo (Optional)", + "required": false, + "placeholder": "Add a note about this unpause action", + "span": { "base": 12 } + }, + { + "id": "txFee", + "name": "txFee", + "type": "amount", + "label": "Transaction Fee", + "value": "{{ fromMicroDenom<{{fees.raw.unpauseFee}}> }}", + "autoPopulate": "always", + "required": true, + "min": "{{ fromMicroDenom<{{fees.raw.unpauseFee}}> }}", + "span": { "base": 12 }, + "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": "createPoll", + "title": "Create Poll", + "icon": "BarChart3", + "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": [ + { + "type": "section", + "title": "About Polls", + "description": "Community polls allow validators and delegators to gauge sentiment on non-binding proposals. Results are publicly visible but do not execute on-chain actions.", + "icon": "Info", + "variant": "info", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "type": "heading", + "text": "Poll Question", + "level": 3, + "color": "secondary", + "span": { "base": 12 } + }, + { + "id": "question", + "name": "question", + "type": "text", + "label": "Poll Question", + "required": true, + "placeholder": "What would you like to ask?", + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Question is required" + } + } + }, + { + "id": "description", + "name": "description", + "type": "textarea", + "label": "Description (Optional)", + "rows": 3, + "placeholder": "Provide additional context for the poll...", + "span": { "base": 12 } + }, + { + "type": "divider", + "label": "Configuration", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 } + }, + { + "id": "duration", + "name": "duration", + "type": "option", + "label": "Poll Duration", + "required": true, + "value": "24", + "span": { "base": 12 }, + "options": [ + { + "label": "24 Hours", + "value": "24", + "help": "Poll ends in 1 day" + }, + { + "label": "48 Hours", + "value": "48", + "help": "Poll ends in 2 days" + }, + { + "label": "72 Hours", + "value": "72", + "help": "Poll ends in 3 days" + }, + { + "label": "1 Week", + "value": "168", + "help": "Poll ends in 7 days" + } + ] + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "creatorAddress", + "name": "creatorAddress", + "type": "text", + "label": "Creator Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Poll Creation", + "summary": [ + { + "label": "Question", + "value": "{{form.question}}" + }, + { + "label": "Description", + "value": "{{form.description || 'No description'}}" + }, + { + "label": "Duration", + "value": "{{form.duration}} hours" + }, + { + "label": "Creator", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "BarChart3", + "label": "Create Poll" + } + } + }, + "payload": { + "question": { + "value": "{{form.question}}", + "coerce": "string" + }, + "description": { + "value": "{{form.description || ''}}", + "coerce": "string" + }, + "duration": { + "value": "{{form.duration}}", + "coerce": "number" + }, + "creatorAddress": { + "value": "{{account.address}}", + "coerce": "string" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/gov-create-poll", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Poll Created!", + "description": "Your poll has been created successfully", + "actions": [] + } + } + }, + { + "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": [ + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "validatorAddress", + "name": "validatorAddress", + "type": "text", + "label": "Validator Address", + "required": true, + "readOnly": true, + "help": "The address of the validator to unstake", + "span": { "base": 12 } + }, + { + "type": "section", + "title": "Validator Information", + "description": "Current Stake: {{formatToCoin<{{ds.validator.stakedAmount ?? 0}}>}} {{chain.denom.symbol}} • Committees: {{ds.validator.committees ?? 'N/A'}} • Type: {{ ds.validator.delegate ? 'Delegation' : 'Validation' }}", + "icon": "Info", + "variant": "default", + "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", + "span": { "base": 12 } + }, + { + "id": "memo", + "name": "memo", + "type": "text", + "label": "Memo (Optional)", + "required": false, + "placeholder": "Add a note about this unstake action", + "span": { "base": 12 } + }, + { + "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}}", + "span": { "base": 12 } + } + ], + "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": "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-unstake", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Unstake Successful!", + "description": "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 + } + } + }, + { + "id": "changeParam", + "title": "Change Parameter Proposal", + "icon": "Settings", + "ds": { + "height": {}, + "params": {}, + "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": [ + { + "type": "section", + "title": "Proposal Requirements", + "description": "Parameter change proposals require +2/3 validator approval to pass. Ensure start/end heights allow sufficient voting time.", + "icon": "Info", + "variant": "info", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "id": "parameterSpace", + "name": "parameterSpace", + "type": "option", + "label": "Parameter Space", + "required": true, + "value": "fee", + "options": [ + { + "label": "Fee Parameters", + "value": "fee", + "help": "Transaction fee settings" + }, + { + "label": "Consensus Parameters", + "value": "cons", + "help": "Consensus mechanism settings" + }, + { + "label": "Validator Parameters", + "value": "val", + "help": "Validator-related settings" + }, + { + "label": "Governance Parameters", + "value": "gov", + "help": "Governance mechanism settings" + }, + { + "label": "Economics Parameters", + "value": "eco", + "help": "Economic model settings" + } + ], + "validation": { + "messages": { + "required": "Parameter space is required" + } + }, + "span": { "base": 12 } + }, + { + "id": "parameterKey", + "name": "parameterKey", + "type": "text", + "label": "Parameter Key", + "required": true, + "placeholder": "Enter parameter key (e.g., sendFee, maxValidators)", + "help": "Available parameters will depend on the selected space", + "validation": { + "messages": { + "required": "Parameter key is required" + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "id": "parameterValue", + "name": "parameterValue", + "type": "text", + "label": "New Parameter Value", + "required": true, + "placeholder": "Enter new value", + "help": "The new value for this parameter", + "validation": { + "messages": { + "required": "Parameter value is required" + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "type": "divider", + "label": "Voting Period", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 } + }, + { + "id": "startHeight", + "name": "startHeight", + "type": "number", + "label": "Start Height", + "required": true, + "value": "{{resolveHeight(ds.height)}}", + "autoPopulate": "always", + "readOnly": true, + "placeholder": "Block height to start voting", + "help": "Block height when voting begins", + "validation": { + "messages": { + "required": "Start height is required" + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "id": "endHeight", + "name": "endHeight", + "type": "number", + "label": "End Height", + "required": true, + "value": "{{resolveHeight(ds.height) + 1}}", + "autoPopulate": "once", + "min": "{{Number(form.startHeight || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startHeight || resolveHeight(ds.height)) + 10000}}", + "placeholder": "Block height to end voting", + "help": "Block height when voting ends. Must be > start height and within 10,000 blocks.", + "validation": { + "min": "{{Number(form.startHeight || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startHeight || resolveHeight(ds.height)) + 10000}}", + "messages": { + "required": "End height is required", + "min": "End height must be at least {{min}} (start height + 1).", + "max": "End height must be at most {{max}} (start height + 10,000)." + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "signerAddress", + "name": "signerAddress", + "type": "text", + "label": "Proposer (Validator Address)", + "value": "{{account.address}}", + "required": true, + "readOnly": true, + "help": "Must be a validator address to create proposals", + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Parameter Change Proposal", + "summary": [ + { + "label": "Parameter Space", + "value": "{{form.parameterSpace}}" + }, + { + "label": "Parameter Key", + "value": "{{form.parameterKey}}" + }, + { + "label": "New Value", + "value": "{{form.parameterValue}}" + }, + { + "label": "Voting Period", + "value": "Block {{form.startHeight}} to {{form.endHeight}}" + }, + { + "label": "Proposer (Validator)", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "Settings", + "label": "Submit Proposal" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "parameterSpace": { + "value": "{{form.parameterSpace}}", + "coerce": "string" + }, + "parameterKey": { + "value": "{{form.parameterKey}}", + "coerce": "string" + }, + "parameterValue": { + "value": "{{form.parameterValue}}", + "coerce": "string" + }, + "startHeight": { + "value": "{{resolveHeight(ds.height)}}", + "coerce": "number" + }, + "endHeight": { + "value": "{{(() => { const s = Number(form.startHeight || resolveHeight(ds.height)); const eRaw = Number(form.endHeight); const e = eRaw > 0 ? eRaw : s + 1; return Math.min(Math.max(e, s + 1), s + 10000); })()}}", + "coerce": "number" + }, + "signer": { + "value": "{{account.address}}", + "coerce": "string" + }, + "memo": { + "value": "", + "coerce": "string" + }, + "fee": { + "value": "{{fees.raw.changeParamFee ?? 0}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-change-param", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Parameter Change Proposal Submitted!", + "description": "Your proposal has been submitted for validator voting", + "actions": [] + } + } + }, + { + "id": "daoTransfer", + "title": "DAO Treasury Transfer Proposal", + "icon": "Coins", + "ds": { + "height": {}, + "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": [ + { + "type": "section", + "title": "Treasury Proposal Requirements", + "description": "DAO treasury transfers require +2/3 validator approval to pass. Funds will be automatically transferred if proposal is approved.", + "icon": "Info", + "variant": "primary", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "type": "heading", + "text": "Transfer Details", + "level": 3, + "color": "secondary", + "span": { "base": 12 } + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Treasury Amount", + "required": true, + "min": 0.000001, + "placeholder": "Amount to transfer from DAO treasury", + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Amount is required", + "min": "Amount must be greater than 0" + } + }, + "help": "Amount of {{chain.denom.symbol}} to request from DAO treasury" + }, + { + "id": "recipientAddress", + "name": "recipientAddress", + "type": "text", + "label": "Recipient Address", + "required": true, + "placeholder": "Address to receive treasury funds", + "help": "The address that will receive the funds if proposal passes", + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Recipient address is required" + } + }, + "features": [ + { + "id": "pasteBtn", + "op": "paste" + } + ] + }, + { + "type": "divider", + "label": "Voting Period", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 } + }, + { + "id": "startHeight", + "name": "startHeight", + "type": "number", + "label": "Start Height", + "required": true, + "value": "{{resolveHeight(ds.height)}}", + "autoPopulate": "always", + "readOnly": true, + "placeholder": "Block height to start voting", + "help": "Block height when voting begins", + "span": { "base": 12, "md": 6 }, + "validation": { + "messages": { + "required": "Start height is required" + } + } + }, + { + "id": "endHeight", + "name": "endHeight", + "type": "number", + "label": "End Height", + "required": true, + "value": "{{resolveHeight(ds.height) + 1}}", + "autoPopulate": "once", + "min": "{{Number(form.startHeight || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startHeight || resolveHeight(ds.height)) + 10000}}", + "placeholder": "Block height to end voting", + "help": "Block height when voting ends. Must be > start height and within 10,000 blocks.", + "span": { "base": 12, "md": 6 }, + "validation": { + "min": "{{Number(form.startHeight || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startHeight || resolveHeight(ds.height)) + 10000}}", + "messages": { + "required": "End height is required", + "min": "End height must be at least {{min}} (start height + 1).", + "max": "End height must be at most {{max}} (start height + 10,000)." + } + } + }, + { + "type": "divider", + "spacing": "md", + "span": { "base": 12 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo / Justification", + "required": false, + "rows": 3, + "placeholder": "Provide justification for this treasury request...", + "help": "Explanation of why this transfer is needed", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "signerAddress", + "name": "signerAddress", + "type": "text", + "label": "Proposer Address (Validator)", + "value": "{{account.address}}", + "required": true, + "readOnly": true, + "help": "Must be a validator address to create proposals", + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm DAO Treasury Proposal", + "summary": [ + { + "label": "Amount", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Recipient", + "value": "{{shortAddress<{{form.recipientAddress}}>}}" + }, + { + "label": "Voting Period", + "value": "Block {{form.startHeight}} to {{form.endHeight}}" + }, + { + "label": "Memo", + "value": "{{form.memo || 'No memo'}}" + }, + { + "label": "Proposer (Validator)", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "Coins", + "label": "Submit Proposal" + } + } + }, + "payload": { + "address": { + "value": "{{form.recipientAddress}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "startHeight": { + "value": "{{resolveHeight(ds.height)}}", + "coerce": "number" + }, + "endHeight": { + "value": "{{(() => { const s = Number(form.startHeight || resolveHeight(ds.height)); const eRaw = Number(form.endHeight); const e = eRaw > 0 ? eRaw : s + 1; return Math.min(Math.max(e, s + 1), s + 10000); })()}}", + "coerce": "number" + }, + "signer": { + "value": "{{account.address}}", + "coerce": "string" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{fees.raw.daoTransferFee ?? 0}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-dao-transfer", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "DAO Transfer Proposal Submitted!", + "description": "Your treasury request has been submitted for validator voting", + "actions": [] + } + } + }, + { + "id": "deleteVote", + "title": "Remove Vote from Proposal", + "icon": "XCircle", + "ds": { + "proposals": { + "id": "gov.proposals" + }, + "__options": { + "staleTimeMs": 30000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[40rem]" + } + } + }, + "form": { + "fields": [ + { + "type": "section", + "title": "Vote Removal Notice", + "description": "This will permanently remove your vote from the specified proposal. You can vote again later if the voting period is still active.", + "icon": "AlertTriangle", + "variant": "warning", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "id": "proposalHash", + "name": "proposalHash", + "type": "text", + "label": "Proposal Hash", + "required": true, + "placeholder": "Enter proposal hash", + "help": "The hash of the proposal to remove your vote from", + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Proposal hash is required" + } + }, + "features": [ + { + "id": "pasteBtn", + "op": "paste" + } + ] + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "voterAddress", + "name": "voterAddress", + "type": "text", + "label": "Voter Address (Validator)", + "value": "{{account.address}}", + "readOnly": true, + "help": "Your validator address", + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Vote Removal", + "summary": [ + { + "label": "Proposal Hash", + "value": "{{form.proposalHash}}" + }, + { + "label": "Voter Address", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "XCircle", + "label": "Remove Vote" + } + } + }, + "payload": { + "proposal": { + "value": "{{form.proposalHash}}", + "coerce": "string" + } + }, + "submit": { + "base": "rpc", + "path": "/v1/gov/del-vote", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Vote Removed!", + "description": "Your vote has been successfully removed from the proposal", + "actions": [] + } + } + }, + { + "id": "votePoll", + "title": "Vote on Community Poll", + "icon": "CheckSquare", + "ds": { + "height": {}, + "polls": { + "id": "gov.poll" + }, + "__options": { + "staleTimeMs": 30000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[40rem]" + } + } + }, + "form": { + "fields": [ + { + "type": "section", + "title": "About Community Polls", + "description": "Community polls are non-binding votes to gauge community sentiment. Your vote will be publicly recorded but will not trigger any on-chain actions.", + "icon": "Info", + "variant": "info", + "span": { "base": 12 } + }, + { + "type": "spacer", + "height": "md", + "span": { "base": 12 } + }, + { + "type": "heading", + "text": "Poll Identification", + "level": 3, + "color": "secondary", + "span": { "base": 12 } + }, + { + "id": "pollQuestion", + "name": "pollQuestion", + "type": "text", + "label": "Poll Question", + "required": true, + "placeholder": "Enter the poll question", + "help": "The exact question from the poll you want to vote on", + "span": { "base": 12 }, + "validation": { + "messages": { + "required": "Poll question is required" + } + } + }, + { + "id": "pollEndBlock", + "name": "pollEndBlock", + "type": "number", + "label": "Poll End Block", + "required": true, + "min": "{{resolveHeight(ds.height) + 1}}", + "placeholder": "Block height when poll ends", + "help": "The end block height of the poll", + "span": { "base": 12, "md": 6 }, + "validation": { + "min": "{{resolveHeight(ds.height) + 1}}", + "messages": { + "required": "Poll end block is required", + "min": "Poll end block must be at least {{min}} (current height + 1)." + } + } + }, + { + "id": "pollURL", + "name": "pollURL", + "type": "text", + "label": "Poll URL (Optional)", + "required": false, + "placeholder": "https://discord.com/...", + "help": "Optional URL for poll discussion", + "span": { "base": 12, "md": 6 } + }, + { + "type": "divider", + "label": "Your Vote", + "variant": "gradient", + "spacing": "lg", + "span": { "base": 12 } + }, + { + "id": "voteApprove", + "name": "voteApprove", + "type": "optionCard", + "label": "Your Vote", + "required": true, + "value": true, + "span": { "base": 12 }, + "options": [ + { + "label": "Approve", + "value": true, + "help": "Vote in favor", + "toolTip": "Vote YES on this poll" + }, + { + "label": "Reject", + "value": false, + "help": "Vote against", + "toolTip": "Vote NO on this poll" + } + ] + }, + { + "type": "spacer", + "height": "sm", + "span": { "base": 12 } + }, + { + "id": "voterAddress", + "name": "voterAddress", + "type": "text", + "label": "Voter Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Poll Vote", + "summary": [ + { + "label": "Poll Question", + "value": "{{form.pollQuestion}}" + }, + { + "label": "Your Vote", + "value": "{{ form.voteApprove ? 'Approve' : 'Reject' }}" + }, + { + "label": "End Block", + "value": "{{form.pollEndBlock}}" + }, + { + "label": "Voter Address", + "value": "{{shortAddress<{{account.address}}>}}" + } + ], + "btn": { + "icon": "CheckSquare", + "label": "Submit Vote" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "pollJSON": { + "value": "{{JSON.stringify({ proposal: form.pollQuestion, endBlock: Math.max(parseInt(form.pollEndBlock || '0', 10), resolveHeight(ds.height) + 1), URL: form.pollURL || '' })}}", + "coerce": "string" + }, + "pollApprove": { + "value": "{{form.voteApprove}}", + "coerce": "boolean" + }, + "fee": { + "value": "{{fees.raw.votePollFee ?? 0}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-vote-poll", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Poll Vote Submitted!", + "description": "Your vote on the community poll has been recorded", + "actions": [] + } + } + }, + { + "id": "govStartPoll", + "title": "Start New Poll", + "icon": "BarChart3", + "ds": { + "height": {}, + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 4000, + "refetchIntervalMs": 4000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[42rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "proposal", + "name": "proposal", + "type": "text", + "label": "Proposal", + "required": true, + "placeholder": "Poll proposal or question", + "help": "Clearly describe the question or decision being voted on.", + "span": { "base": 12 } + }, + { + "id": "endBlock", + "name": "endBlock", + "type": "number", + "label": "End Block", + "required": true, + "value": "{{resolveHeight(ds.height) + 1}}", + "autoPopulate": "once", + "min": "{{resolveHeight(ds.height) + 1}}", + "validation": { + "min": "{{resolveHeight(ds.height) + 1}}", + "messages": { + "min": "End block must be at least {{min}} (current height + 1)." + } + }, + "placeholder": "Block height when poll ends", + "help": "Use a valid future block height to allow participation.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "URL", + "name": "URL", + "type": "text", + "label": "Discussion URL", + "required": false, + "placeholder": "https://...", + "help": "Optional link to Discord, forum, or proposal context.", + "span": { "base": 12, "md": 6 } + } + ], + "confirmation": { + "title": "Confirm New Poll", + "summary": [ + { + "label": "Proposal", + "value": "{{form.proposal}}" + }, + { + "label": "End Block", + "value": "{{form.endBlock}}" + }, + { + "label": "URL", + "value": "{{form.URL || 'No URL'}}" + } + ], + "btn": { + "icon": "BarChart3", + "label": "Start Poll" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "pollJSON": { + "proposal": "{{form.proposal}}", + "endBlock": "{{Math.max(Number(form.endBlock || 0), resolveHeight(ds.height) + 1)}}", + "URL": "{{form.URL || ''}}" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-start-poll", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Poll Submitted", + "description": "The new governance poll transaction was broadcast successfully." + } + } + }, + { + "id": "govVotePoll", + "title": "Vote Poll", + "icon": "CheckSquare", + "ds": { + "height": {}, + "gov.poll": {}, + "gov.proposals": {}, + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 4000, + "refetchIntervalMs": 4000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[42rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "proposalHash", + "name": "proposalHash", + "type": "select", + "label": "Proposal Hash", + "required": true, + "map": "{{Object.entries(ds['gov.poll'] || {}).map(([k, v]) => { const h = v?.proposalHash || k; return { label: String(h).slice(0, 12) + '...', value: h }; })}}", + "help": "Select the poll hash to auto-fill proposal details.", + "span": { "base": 12 } + }, + { + "id": "proposal", + "name": "proposal", + "type": "text", + "label": "Proposal", + "required": true, + "value": "{{(() => { const selected = form.proposalHash || form.proposal || ''; const proposals = ds['gov.proposals'] || {}; const direct = proposals[selected]?.proposal?.msg; if (direct) return selected; const byMsgHashKey = Object.keys(proposals).find(k => proposals[k]?.proposal?.msg?.proposalHash === selected); if (byMsgHashKey) return byMsgHashKey; return selected; })()}}", + "autoPopulate": "always", + "placeholder": "Poll proposal", + "help": "Must match the existing poll proposal text.", + "span": { "base": 12 } + }, + { + "id": "endBlock", + "name": "endBlock", + "type": "number", + "label": "End Block", + "required": true, + "value": "{{(() => { const selected = form.proposalHash || form.proposal || ''; const proposals = ds['gov.proposals'] || {}; const direct = proposals[selected]?.proposal?.msg?.endHeight; if (direct != null && direct !== '') return direct; const byMsgHashKey = Object.keys(proposals).find(k => proposals[k]?.proposal?.msg?.proposalHash === selected); if (byMsgHashKey) { const fallback = proposals[byMsgHashKey]?.proposal?.msg?.endHeight; if (fallback != null && fallback !== '') return fallback; } return form.endBlock ?? ''; })()}}", + "autoPopulate": "always", + "min": "{{resolveHeight(ds.height) + 1}}", + "validation": { + "min": "{{resolveHeight(ds.height) + 1}}", + "messages": { + "min": "End block must be at least {{min}} (current height + 1)." + } + }, + "placeholder": "Block height", + "help": "Must match the poll endBlock value.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "URL", + "name": "URL", + "type": "text", + "label": "Discussion URL", + "required": false, + "value": "{{(() => { const selected = form.proposalHash || ''; const polls = ds['gov.poll'] || {}; const byKey = polls[selected]?.proposalURL; if (byKey) return byKey; const byProposalHashKey = Object.keys(polls).find(k => polls[k]?.proposalHash === selected); if (byProposalHashKey) return polls[byProposalHashKey]?.proposalURL || ''; return form.URL ?? ''; })()}}", + "autoPopulate": "always", + "placeholder": "https://...", + "help": "Should match the poll URL when one exists.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "voteApprove", + "name": "voteApprove", + "type": "optionCard", + "label": "Vote", + "required": true, + "value": true, + "options": [ + { + "label": "Approve", + "value": true, + "help": "Vote in favor" + }, + { + "label": "Reject", + "value": false, + "help": "Vote against" + } + ], + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Poll Vote", + "summary": [ + { + "label": "Proposal Hash", + "value": "{{form.proposalHash}}" + }, + { + "label": "Proposal", + "value": "{{form.proposal}}" + }, + { + "label": "End Block", + "value": "{{form.endBlock}}" + }, + { + "label": "Vote", + "value": "{{form.voteApprove ? 'Approve' : 'Reject'}}" + } + ], + "btn": { + "icon": "CheckSquare", + "label": "Submit Vote" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "pollJSON": { + "proposal": "{{form.proposal}}", + "endBlock": "{{Math.max(Number(form.endBlock || 0), resolveHeight(ds.height) + 1)}}", + "URL": "{{form.URL || ''}}" + }, + "pollApprove": { + "value": "{{form.voteApprove}}", + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-vote-poll", + "method": "POST" + } + }, + { + "id": "govGenerateParamChange", + "title": "Create Parameter Change Proposal", + "icon": "Settings", + "ds": { + "height": {}, + "params": {}, + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 4000, + "refetchIntervalMs": 4000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[44rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Proposer Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "paramSpace", + "name": "paramSpace", + "type": "select", + "label": "Parameter Space", + "required": true, + "map": "{{Object.keys(ds.params || {}).map(k => ({ label: k, value: k }))}}", + "span": { "base": 12, "md": 6 } + }, + { + "id": "paramKey", + "name": "paramKey", + "type": "select", + "label": "Parameter Key", + "required": true, + "map": "{{Object.keys(ds.params?.[form.paramSpace] || {}).map(k => ({ label: k, value: k }))}}", + "span": { "base": 12, "md": 6 } + }, + { + "id": "paramValue", + "name": "paramValue", + "type": "text", + "label": "New Value", + "required": true, + "value": "{{ds.params?.[form.paramSpace]?.[form.paramKey] ?? ''}}", + "autoPopulate": "once", + "span": { "base": 12 } + }, + { + "id": "startBlock", + "name": "startBlock", + "type": "number", + "label": "Start Block", + "required": true, + "value": "{{resolveHeight(ds.height)}}", + "autoPopulate": "always", + "readOnly": true, + "help": "Always uses current chain height.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "endBlock", + "name": "endBlock", + "type": "number", + "label": "End Block", + "required": true, + "value": "{{resolveHeight(ds.height) + 1}}", + "autoPopulate": "once", + "min": "{{Number(form.startBlock || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startBlock || resolveHeight(ds.height)) + 10000}}", + "validation": { + "min": "{{Number(form.startBlock || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startBlock || resolveHeight(ds.height)) + 10000}}", + "messages": { + "min": "End block must be at least {{min}} (start block + 1).", + "max": "End block must be at most {{max}} (start block + 10,000)." + } + }, + "help": "Must be greater than start block and within 10,000 blocks.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo", + "placeholder": "Description/reason for change", + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "required": true, + "value": "{{fromMicroDenom<{{fees.raw.changeParameterFee ?? 0}}>}}", + "autoPopulate": "once", + "span": { "base": 12, "md": 12 } + } + ], + "confirmation": { + "title": "Confirm Parameter Change Proposal", + "summary": [ + { + "label": "Parameter", + "value": "{{form.paramSpace}} / {{form.paramKey}}" + }, + { + "label": "Value", + "value": "{{form.paramValue}}" + }, + { + "label": "Voting Window", + "value": "{{form.startBlock}} -> {{form.endBlock}}" + } + ], + "btn": { + "icon": "Settings", + "label": "Create Proposal" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "amount": { + "value": 0, + "coerce": "number" + }, + "paramSpace": { + "value": "{{form.paramSpace}}", + "coerce": "string" + }, + "paramKey": { + "value": "{{form.paramKey}}", + "coerce": "string" + }, + "paramValue": { + "value": "{{form.paramValue}}", + "coerce": "string" + }, + "startBlock": { + "value": "{{resolveHeight(ds.height)}}", + "coerce": "number" + }, + "endBlock": { + "value": "{{(() => { const s = Number(form.startBlock || resolveHeight(ds.height)); const eRaw = Number(form.endBlock); const e = eRaw > 0 ? eRaw : s + 1; return Math.min(Math.max(e, s + 1), s + 10000); })()}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-change-param", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Parameter Change Proposal Submitted", + "description": "Your proposal transaction was broadcast successfully." + } + } + }, + { + "id": "govGenerateDaoTransfer", + "title": "Create Treasury Subsidy Proposal", + "icon": "Coins", + "ds": { + "height": {}, + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "__options": { + "staleTimeMs": 4000, + "refetchIntervalMs": 4000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[44rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Proposer Address", + "value": "{{account.address}}", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Amount (CNPY)", + "required": true, + "span": { "base": 12, "md": 6 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "required": true, + "value": "{{fromMicroDenom<{{fees.raw.sendFee ?? 0}}>}}", + "min": "{{ fromMicroDenom<{{fees.raw.sendFee}}> }}", + "validation": { + "messages": { + "required": "Transaction fee is required", + "min": "Transaction fee cannot be less than the network minimum: {{min}} {{chain.denom.symbol}}" + } + }, + "autoPopulate": "once", + "span": { "base": 12, "md": 6 } + }, + { + "id": "startBlock", + "name": "startBlock", + "type": "number", + "label": "Start Block", + "required": true, + "value": "{{resolveHeight(ds.height)}}", + "autoPopulate": "always", + "readOnly": true, + "help": "Always uses current chain height.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "endBlock", + "name": "endBlock", + "type": "number", + "label": "End Block", + "required": true, + "value": "{{resolveHeight(ds.height) + 1}}", + "autoPopulate": "once", + "min": "{{Number(form.startBlock || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startBlock || resolveHeight(ds.height)) + 10000}}", + "validation": { + "min": "{{Number(form.startBlock || resolveHeight(ds.height)) + 1}}", + "max": "{{Number(form.startBlock || resolveHeight(ds.height)) + 10000}}", + "messages": { + "min": "End block must be at least {{min}} (start block + 1).", + "max": "End block must be at most {{max}} (start block + 10,000)." + } + }, + "help": "Must be greater than start block and within 10,000 blocks.", + "span": { "base": 12, "md": 6 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo", + "placeholder": "Description/reason for transfer", + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Treasury Subsidy Proposal", + "summary": [ + { + "label": "Amount", + "value": "{{form.amount}} {{chain.denom.symbol}}" + }, + { + "label": "Voting Window", + "value": "{{form.startBlock}} -> {{form.endBlock}}" + }, + { + "label": "Memo", + "value": "{{form.memo || 'No memo'}}" + } + ], + "btn": { + "icon": "Coins", + "label": "Create Proposal" + } + } + }, + "payload": { + "address": { + "value": "{{account.address}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "paramSpace": { + "value": "", + "coerce": "string" + }, + "paramKey": { + "value": "", + "coerce": "string" + }, + "paramValue": { + "value": "", + "coerce": "string" + }, + "startBlock": { + "value": "{{resolveHeight(ds.height)}}", + "coerce": "number" + }, + "endBlock": { + "value": "{{(() => { const s = Number(form.startBlock || resolveHeight(ds.height)); const eRaw = Number(form.endBlock); const e = eRaw > 0 ? eRaw : s + 1; return Math.min(Math.max(e, s + 1), s + 10000); })()}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-dao-transfer", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Treasury Subsidy Proposal Submitted", + "description": "Your treasury proposal transaction was broadcast successfully." + } + } + }, + { + "id": "govSubmitProposalTx", + "title": "Manual Raw TX Broadcast", + "icon": "Send", + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[38rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "proposalTx", + "name": "proposalTx", + "type": "textarea", + "label": "Raw Proposal JSON", + "required": true, + "placeholder": "{\"type\":\"...\"}", + "help": "Advanced mode: paste a fully signed raw transaction JSON to broadcast via /v1/tx.", + "rows": 12, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Manual Broadcast", + "summary": [ + { + "label": "Payload", + "value": "Raw JSON transaction will be sent to /v1/tx" + } + ], + "btn": { + "icon": "Send", + "label": "Submit" + } + } + }, + "payload": { + "__raw": { + "value": "{{JSON.parse(form.proposalTx)}}" + } + }, + "submit": { + "base": "rpc", + "path": "/v1/tx", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Raw Transaction Broadcast", + "description": "The raw transaction was sent to the network endpoint." + } + } + }, + { + "id": "govAddProposalVote", + "title": "Vote Proposal", + "icon": "Vote", + "ds": { + "gov.proposals": {}, + "__options": { + "staleTimeMs": 4000, + "refetchIntervalMs": 4000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[38rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "proposalId", + "name": "proposalId", + "type": "select", + "label": "Proposal", + "required": true, + "map": "{{Object.keys(ds['gov.proposals'] || {}).map(k => ({ label: k, value: k }))}}", + "span": { "base": 12 } + }, + { + "id": "approve", + "name": "approve", + "type": "optionCard", + "label": "Vote", + "required": true, + "value": true, + "options": [ + { + "label": "Approve", + "value": true + }, + { + "label": "Reject", + "value": false + } + ], + "span": { "base": 12 } + }, + { + "id": "proposalMsg", + "name": "proposalMsg", + "type": "textarea", + "label": "Proposal JSON (Auto-filled)", + "value": "{{JSON.stringify(ds['gov.proposals']?.[form.proposalId]?.proposal?.msg || {}, null, 2)}}", + "autoPopulate": "always", + "readOnly": true, + "rows": 10, + "span": { "base": 12 } + }, + { + "id": "proposalType", + "name": "proposalType", + "type": "text", + "label": "Proposal Type", + "value": "{{ds['gov.proposals']?.[form.proposalId]?.proposal?.type || 'unknown'}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12, "md": 6 } + }, + { + "id": "currentVoteStatus", + "name": "currentVoteStatus", + "type": "text", + "label": "Current Vote Status", + "value": "{{(() => { const v = ds['gov.proposals']?.[form.proposalId]?.approve; if (v === true) return 'Approved'; if (v === false) return 'Rejected'; return 'No vote'; })()}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12, "md": 6 } + }, + { + "id": "changeSummary", + "name": "changeSummary", + "type": "textarea", + "label": "Proposed Change (Explicit)", + "value": "{{(() => { const p = ds['gov.proposals']?.[form.proposalId]?.proposal || {}; const m = p.msg || {}; if (m.parameterSpace && m.parameterKey) return `Parameter change: ${m.parameterSpace}.${m.parameterKey} -> ${m.parameterValue}`; if (p.type === 'daoTransfer') return `Treasury subsidy: amount=${m.amount ?? p.amount ?? 'N/A'}; memo=${p.memo ?? ''}`; return `Type=${p.type || 'unknown'}; memo=${p.memo || 'N/A'}`; })()}}", + "autoPopulate": "always", + "readOnly": true, + "rows": 3, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Proposal Vote", + "summary": [ + { + "label": "Proposal", + "value": "{{form.proposalId}}" + }, + { + "label": "Vote", + "value": "{{form.approve ? 'Approve' : 'Reject'}}" + }, + { + "label": "Change", + "value": "{{form.changeSummary}}" + } + ], + "btn": { + "icon": "Vote", + "label": "Submit Vote" + } + } + }, + "payload": { + "approve": { + "value": "{{form.approve}}", + "coerce": "boolean" + }, + "proposal": { + "value": "{{ds['gov.proposals']?.[form.proposalId]?.proposal?.msg || {}}}" + } + }, + "submit": { + "base": "admin", + "path": "/v1/gov/add-vote", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Proposal Vote Saved", + "description": "Your proposal vote preference has been updated." + } + } + }, + { + "id": "govDeleteProposalVote", + "title": "Delete Proposal Vote", + "icon": "XCircle", + "ds": { + "gov.proposals": {}, + "__options": { + "staleTimeMs": 4000, + "refetchIntervalMs": 4000, + "refetchOnMount": true, + "refetchOnWindowFocus": false + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[38rem]" + } + } + }, + "form": { + "fields": [ + { + "id": "proposalId", + "name": "proposalId", + "type": "select", + "label": "Proposal", + "required": true, + "map": "{{Object.keys(ds['gov.proposals'] || {}).map(k => ({ label: k, value: k }))}}", + "span": { "base": 12 } + }, + { + "id": "proposalMsg", + "name": "proposalMsg", + "type": "textarea", + "label": "Proposal JSON (Auto-filled)", + "value": "{{JSON.stringify(ds['gov.proposals']?.[form.proposalId]?.proposal?.msg || {}, null, 2)}}", + "autoPopulate": "always", + "readOnly": true, + "rows": 10, + "span": { "base": 12 } + }, + { + "id": "proposalType", + "name": "proposalType", + "type": "text", + "label": "Proposal Type", + "value": "{{ds['gov.proposals']?.[form.proposalId]?.proposal?.type || 'unknown'}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12, "md": 6 } + }, + { + "id": "currentVoteStatus", + "name": "currentVoteStatus", + "type": "text", + "label": "Current Vote Status", + "value": "{{(() => { const v = ds['gov.proposals']?.[form.proposalId]?.approve; if (v === true) return 'Approved'; if (v === false) return 'Rejected'; return 'No vote'; })()}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12, "md": 6 } + }, + { + "id": "changeSummary", + "name": "changeSummary", + "type": "textarea", + "label": "Proposed Change (Explicit)", + "value": "{{(() => { const p = ds['gov.proposals']?.[form.proposalId]?.proposal || {}; const m = p.msg || {}; if (m.parameterSpace && m.parameterKey) return `Parameter change: ${m.parameterSpace}.${m.parameterKey} -> ${m.parameterValue}`; if (p.type === 'daoTransfer') return `Treasury subsidy: amount=${m.amount ?? p.amount ?? 'N/A'}; memo=${p.memo ?? ''}`; return `Type=${p.type || 'unknown'}; memo=${p.memo || 'N/A'}`; })()}}", + "autoPopulate": "always", + "readOnly": true, + "rows": 3, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Delete Vote", + "summary": [ + { + "label": "Proposal", + "value": "{{form.proposalId}}" + }, + { + "label": "Change", + "value": "{{form.changeSummary}}" + } + ], + "btn": { + "icon": "XCircle", + "label": "Delete Vote" + } + } + }, + "payload": { + "approve": { + "value": null + }, + "proposal": { + "value": "{{ds['gov.proposals']?.[form.proposalId]?.proposal?.msg || {}}}" + } + }, + "submit": { + "base": "admin", + "path": "/v1/gov/del-vote", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Proposal Vote Removed", + "description": "Your proposal vote preference was removed." + } + } + }, + { + "id": "orderCreate", + "title": "Create Order", + "icon": "PlusCircle", + "ds": { + "admin.config": {}, + "account": { + "account": { + "address": "{{account.address}}" + } + }, + "keystore": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "admin.config", + "account", + "keystore" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[36rem] md:max-w-[56rem]" + } + } + }, + "form": { + "wizard": { + "steps": [ + { + "id": "context", + "title": "Order Context" + }, + { + "id": "pricing", + "title": "Pricing & Submit" + } + ] + }, + "info": { + "title": "Create Order Guide", + "items": [ + { + "label": "Execution", + "value": "Create runs on root chain", + "icon": "Info" + }, + { + "label": "For Sale", + "value": "{{numberToLocaleString<{{form.amount || 0}}>}} {{chain.denom.symbol}}", + "icon": "Coins" + }, + { + "label": "Requested", + "value": "{{numberToLocaleString<{{form.receiveAmount || 0}}>}}", + "icon": "ArrowLeftRight" + }, + { + "label": "Rule", + "value": "Only unlocked orders can be repriced or voided", + "icon": "AlertTriangle" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Seller Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "receiveAddress", + "name": "receiveAddress", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Receive Address (Counter-Asset)", + "required": true, + "value": "{{params.receiveAddress || account.address || ''}}", + "autoPopulate": "once", + "validation": { + "messages": { + "required": "Receive address is required" + } + }, + "map": "{{ Object.keys(ds.keystore?.addressMap || {}).map(k => ({ label: k.slice(0, 6) + '...' + String(k).slice(-6) + ' (' + ds.keystore.addressMap[k].keyNickname + ')', value: k })) }}", + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "committees", + "name": "committees", + "type": "tableSelect", + "label": "Committee", + "required": true, + "help": "Select the committee used for this order.", + "multiple": false, + "rowKey": "id", + "selectMode": "action", + "value": "{{ params.committees ?? ds['admin.config']?.chainId ?? '' }}", + "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": "Committee ID", + "expr": "{{row.id}}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ (form.committees ?? '') + '' === (row.id ?? '') + '' ? 'Selected' : 'Select' }}", + "emit": { + "op": "select" + } + }, + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Amount For Sale", + "required": true, + "min": 0.000001, + "max": "{{ fromMicroDenom<{{ds.account?.amount ?? 0}}> }}", + "validation": { + "messages": { + "required": "Amount for sale is required", + "min": "Amount must be greater than 0", + "max": "Insufficient balance. Max available: {{max}} {{chain.denom.symbol}}" + } + }, + "span": { "base": 12, "md": 6 }, + "step": "pricing" + }, + { + "id": "receiveAmount", + "name": "receiveAmount", + "type": "amount", + "label": "Requested Amount", + "required": true, + "min": 0.000001, + "validation": { + "messages": { + "required": "Requested amount is required", + "min": "Requested amount must be greater than 0" + } + }, + "span": { "base": 12, "md": 6 }, + "step": "pricing" + }, + { + "id": "data", + "name": "data", + "type": "text", + "label": "Data (optional)", + "placeholder": "Sub-asset contract address (hex)", + "value": "{{params.data || ''}}", + "autoPopulate": "once", + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo (optional)", + "placeholder": "Optional message", + "value": "{{params.memo || ''}}", + "autoPopulate": "once", + "span": { "base": 12 }, + "step": "pricing" + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12, "md": 6 }, + "step": "pricing" + } + ], + "confirmation": { + "title": "Confirm Create Order", + "summary": [ + { + "label": "Seller", + "value": "{{shortAddress<{{form.address}}>}}" + }, + { + "label": "Committee", + "value": "{{form.committees}}" + }, + { + "label": "For Sale", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Requested", + "value": "{{numberToLocaleString<{{form.receiveAmount}}>}}" + } + ], + "btn": { + "icon": "PlusCircle", + "label": "Create Order" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "receiveAddress": { + "value": "{{form.receiveAddress}}", + "coerce": "string" + }, + "committees": { + "value": "{{ form.committees ?? ds['admin.config']?.chainId ?? '' }}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "receiveAmount": { + "value": "{{toMicroDenom<{{form.receiveAmount}}>}}", + "coerce": "number" + }, + "data": { + "value": "{{form.data || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-create-order", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Order Created", + "description": "{{result}}" + } + } + }, + { + "id": "orderReprice", + "title": "Reprice Order", + "icon": "Pencil", + "ds": { + "admin.config": {}, + "keystore": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "admin.config", + "keystore" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[36rem] md:max-w-[56rem]" + } + } + }, + "form": { + "wizard": { + "steps": [ + { + "id": "context", + "title": "Order Context" + }, + { + "id": "pricing", + "title": "New Pricing" + } + ] + }, + "info": { + "title": "Reprice Rules", + "items": [ + { + "label": "Execution", + "value": "Reprice runs on root chain", + "icon": "Info" + }, + { + "label": "Editable", + "value": "Only unlocked orders can be edited", + "icon": "AlertTriangle" + }, + { + "label": "For Sale", + "value": "{{numberToLocaleString<{{form.amount || 0}}>}} {{chain.denom.symbol}}", + "icon": "Coins" + }, + { + "label": "Requested", + "value": "{{numberToLocaleString<{{form.receiveAmount || 0}}>}}", + "icon": "ArrowLeftRight" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Seller Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "orderId", + "name": "orderId", + "type": "text", + "label": "Order ID", + "required": true, + "value": "{{params.orderId || ''}}", + "autoPopulate": "once", + "readOnly": true, + "validation": { + "messages": { + "required": "Order ID is required" + } + }, + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "receiveAddress", + "name": "receiveAddress", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Receive Address (Counter-Asset)", + "required": true, + "value": "{{params.receiveAddress || ''}}", + "autoPopulate": "once", + "validation": { + "messages": { + "required": "Receive address is required" + } + }, + "map": "{{ Object.keys(ds.keystore?.addressMap || {}).map(k => ({ label: k.slice(0, 6) + '...' + String(k).slice(-6) + ' (' + ds.keystore.addressMap[k].keyNickname + ')', value: k })) }}", + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "committees", + "name": "committees", + "type": "tableSelect", + "label": "Committee", + "required": true, + "help": "Select the committee used for this order.", + "multiple": false, + "rowKey": "id", + "selectMode": "action", + "value": "{{ params.committees ?? ds['admin.config']?.chainId ?? '' }}", + "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": "Committee ID", + "expr": "{{row.id}}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ (form.committees ?? '') + '' === (row.id ?? '') + '' ? 'Selected' : 'Select' }}", + "emit": { + "op": "select" + } + }, + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Amount For Sale", + "required": true, + "min": 0.000001, + "value": "{{params.amount ?? ''}}", + "autoPopulate": "once", + "validation": { + "messages": { + "required": "Amount for sale is required", + "min": "Amount must be greater than 0" + } + }, + "span": { "base": 12, "md": 6 }, + "step": "pricing" + }, + { + "id": "receiveAmount", + "name": "receiveAmount", + "type": "amount", + "label": "Requested Amount", + "required": true, + "value": "{{params.receiveAmount ?? ''}}", + "autoPopulate": "once", + "min": 0.000001, + "validation": { + "messages": { + "required": "Requested amount is required", + "min": "Requested amount must be greater than 0" + } + }, + "span": { "base": 12, "md": 6 }, + "step": "pricing" + }, + { + "id": "data", + "name": "data", + "type": "text", + "label": "Data (optional)", + "placeholder": "Sub-asset contract address (hex)", + "value": "{{params.data || ''}}", + "autoPopulate": "once", + "span": { "base": 12 }, + "step": "context" + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo (optional)", + "placeholder": "Optional message", + "value": "{{params.memo || ''}}", + "autoPopulate": "once", + "span": { "base": 12 }, + "step": "pricing" + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12, "md": 6 }, + "step": "pricing" + } + ], + "confirmation": { + "title": "Confirm Reprice Order", + "summary": [ + { + "label": "Order ID", + "value": "{{shortAddress<{{form.orderId}}>}}" + }, + { + "label": "Committee", + "value": "{{form.committees}}" + }, + { + "label": "For Sale", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Requested", + "value": "{{numberToLocaleString<{{form.receiveAmount}}>}}" + } + ], + "btn": { + "icon": "Pencil", + "label": "Reprice Order" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "receiveAddress": { + "value": "{{form.receiveAddress}}", + "coerce": "string" + }, + "committees": { + "value": "{{ form.committees ?? ds['admin.config']?.chainId ?? '' }}", + "coerce": "string" + }, + "orderId": { + "value": "{{form.orderId}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "receiveAmount": { + "value": "{{toMicroDenom<{{form.receiveAmount}}>}}", + "coerce": "number" + }, + "data": { + "value": "{{form.data || ''}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-edit-order", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Order Repriced", + "description": "{{result}}" + } + } + }, + { + "id": "orderVoid", + "title": "Void Order", + "icon": "Trash2", + "ds": { + "admin.config": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "admin.config" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[38rem]" + } + } + }, + "form": { + "info": { + "title": "Void Order Rules", + "items": [ + { + "label": "Execution", + "value": "Void runs on root chain", + "icon": "Info" + }, + { + "label": "Restriction", + "value": "Locked orders cannot be voided", + "icon": "AlertTriangle" + }, + { + "label": "Order", + "value": "{{shortAddress<{{form.orderId}}>}}", + "icon": "Trash2" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Seller Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "orderId", + "name": "orderId", + "type": "text", + "label": "Order ID", + "required": true, + "value": "{{params.orderId || ''}}", + "autoPopulate": "once", + "readOnly": true, + "validation": { + "messages": { + "required": "Order ID is required" + } + }, + "span": { "base": 12 } + }, + { + "id": "committees", + "name": "committees", + "type": "tableSelect", + "label": "Committee", + "required": true, + "help": "Select the committee used for this order.", + "multiple": false, + "rowKey": "id", + "selectMode": "action", + "value": "{{ params.committees ?? ds['admin.config']?.chainId ?? '' }}", + "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": "Committee ID", + "expr": "{{row.id}}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ (form.committees ?? '') + '' === (row.id ?? '') + '' ? 'Selected' : 'Select' }}", + "emit": { + "op": "select" + } + }, + "span": { "base": 12 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo (optional)", + "placeholder": "Optional message", + "value": "{{params.memo || ''}}", + "autoPopulate": "once", + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12, "md": 6 } + } + ], + "confirmation": { + "title": "Confirm Void Order", + "summary": [ + { + "label": "Order ID", + "value": "{{shortAddress<{{form.orderId}}>}}" + }, + { + "label": "Committee", + "value": "{{form.committees}}" + } + ], + "btn": { + "icon": "Trash2", + "label": "Void Order" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "committees": { + "value": "{{ form.committees ?? ds['admin.config']?.chainId ?? '' }}", + "coerce": "string" + }, + "orderId": { + "value": "{{form.orderId}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-delete-order", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Order Voided", + "description": "{{result}}" + } + } + }, + { + "id": "orderLock", + "title": "Lock Order", + "icon": "Lock", + "ds": { + "keystore": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "keystore" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[28rem] md:max-w-[38rem]" + } + } + }, + "form": { + "info": { + "title": "Lock Order Notes", + "items": [ + { + "label": "Execution", + "value": "Lock runs on nested chain", + "icon": "Info" + }, + { + "label": "Receive Address", + "value": "Set root-chain address to receive CNPY", + "icon": "Lock" + }, + { + "label": "Order", + "value": "{{shortAddress<{{form.orderId}}>}}", + "icon": "ShoppingCart" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Buyer Address (Nested Chain)", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "receiveAddress", + "name": "receiveAddress", + "type": "advancedSelect", + "allowFreeInput": true, + "allowCreate": true, + "label": "Receive Address (Root Chain)", + "required": true, + "value": "{{params.receiveAddress || account.address || ''}}", + "autoPopulate": "once", + "validation": { + "messages": { + "required": "Receive address is required" + } + }, + "map": "{{ Object.keys(ds.keystore?.addressMap || {}).map(k => ({ label: k.slice(0, 6) + '...' + String(k).slice(-6) + ' (' + ds.keystore.addressMap[k].keyNickname + ')', value: k })) }}", + "span": { "base": 12 } + }, + { + "id": "orderId", + "name": "orderId", + "type": "text", + "label": "Order ID", + "required": true, + "value": "{{params.orderId || ''}}", + "autoPopulate": "once", + "readOnly": true, + "validation": { + "messages": { + "required": "Order ID is required" + } + }, + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Lock Order", + "summary": [ + { + "label": "Buyer", + "value": "{{shortAddress<{{form.address}}>}}" + }, + { + "label": "Order ID", + "value": "{{shortAddress<{{form.orderId}}>}}" + }, + { + "label": "Receive Address", + "value": "{{shortAddress<{{form.receiveAddress}}>}}" + } + ], + "btn": { + "icon": "Lock", + "label": "Lock Order" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "receiveAddress": { + "value": "{{form.receiveAddress}}", + "coerce": "string" + }, + "orderId": { + "value": "{{form.orderId}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-lock-order", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Order Locked", + "description": "{{result}}" + } + } + }, + { + "id": "orderClose", + "title": "Close Order", + "icon": "CheckCircle2", + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[30rem] md:max-w-[42rem]" + } + } + }, + "form": { + "info": { + "title": "Close Order Preconditions", + "items": [ + { + "label": "Execution", + "value": "Close runs on nested chain", + "icon": "Info" + }, + { + "label": "Restriction", + "value": "Address must match order buyer and be before deadline", + "icon": "AlertTriangle" + }, + { + "label": "Order", + "value": "{{shortAddress<{{form.orderId}}>}}", + "icon": "CheckCircle2" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Buyer Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "orderId", + "name": "orderId", + "type": "text", + "label": "Order ID", + "required": true, + "value": "{{params.orderId || ''}}", + "autoPopulate": "once", + "readOnly": true, + "validation": { + "messages": { + "required": "Order ID is required" + } + }, + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12 } + } + ], + "confirmation": { + "title": "Confirm Close Order", + "summary": [ + { + "label": "Buyer", + "value": "{{shortAddress<{{form.address}}>}}" + }, + { + "label": "Order ID", + "value": "{{shortAddress<{{form.orderId}}>}}" + } + ], + "btn": { + "icon": "CheckCircle2", + "label": "Close Order" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "orderId": { + "value": "{{form.orderId}}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-close-order", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "Order Closed", + "description": "{{result}}" + } + } + }, + { + "id": "dexLimitOrder", + "title": "DEX Limit Order", + "icon": "ArrowLeftRight", + "ds": { + "admin.config": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "admin.config" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[46rem]" + } + } + }, + "form": { + "info": { + "title": "DEX Limit Order Notes", + "items": [ + { + "label": "Amount", + "value": "{{numberToLocaleString<{{form.amount || 0}}>}} {{chain.denom.symbol}}", + "icon": "Coins" + }, + { + "label": "Minimum Receive", + "value": "{{numberToLocaleString<{{form.receiveAmount || 0}}>}}", + "icon": "ArrowLeftRight" + }, + { + "label": "Committee", + "value": "{{form.committees || ds['admin.config']?.chainId || '-'}}", + "icon": "Info" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "committees", + "name": "committees", + "type": "tableSelect", + "label": "Committee", + "required": true, + "help": "Select the target committee for the limit order.", + "multiple": false, + "rowKey": "id", + "selectMode": "action", + "value": "{{ params.committees ?? ds['admin.config']?.chainId ?? '' }}", + "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": "Committee ID", + "expr": "{{row.id}}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ (form.committees ?? '') + '' === (row.id ?? '') + '' ? 'Selected' : 'Select' }}", + "emit": { + "op": "select" + } + }, + "span": { "base": 12 } + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Amount", + "required": true, + "min": 0.000001, + "validation": { + "messages": { + "required": "Amount is required", + "min": "Amount must be greater than 0" + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "id": "receiveAmount", + "name": "receiveAmount", + "type": "amount", + "label": "Minimum Receive Amount", + "required": true, + "min": 0.000001, + "validation": { + "messages": { + "required": "Minimum receive amount is required", + "min": "Amount must be greater than 0" + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo (optional)", + "placeholder": "Optional message", + "value": "{{params.memo || ''}}", + "autoPopulate": "once", + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12, "md": 6 } + } + ], + "confirmation": { + "title": "Confirm DEX Limit Order", + "summary": [ + { + "label": "Committee", + "value": "{{form.committees}}" + }, + { + "label": "Amount", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + }, + { + "label": "Min Receive", + "value": "{{numberToLocaleString<{{form.receiveAmount}}>}}" + } + ], + "btn": { + "icon": "ArrowLeftRight", + "label": "Create DEX Order" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "receiveAmount": { + "value": "{{toMicroDenom<{{form.receiveAmount}}>}}", + "coerce": "number" + }, + "committees": { + "value": "{{ form.committees ?? ds['admin.config']?.chainId ?? '' }}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-dex-limit-order", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "DEX Limit Order Submitted", + "description": "{{result}}" + } + } + }, + { + "id": "dexLiquidityDeposit", + "title": "DEX Liquidity Deposit", + "icon": "Droplets", + "ds": { + "admin.config": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "admin.config" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[46rem]" + } + } + }, + "form": { + "info": { + "title": "DEX Deposit Notes", + "items": [ + { + "label": "Operation", + "value": "Deposit adds liquidity to the selected pool", + "icon": "Droplets" + }, + { + "label": "Amount", + "value": "{{numberToLocaleString<{{form.amount || 0}}>}} {{chain.denom.symbol}}", + "icon": "Coins" + }, + { + "label": "Committee", + "value": "{{form.committees || ds['admin.config']?.chainId || '-'}}", + "icon": "Info" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "committees", + "name": "committees", + "type": "tableSelect", + "label": "Committee", + "required": true, + "help": "Select the target committee for the liquidity deposit.", + "multiple": false, + "rowKey": "id", + "selectMode": "action", + "value": "{{ params.committees ?? ds['admin.config']?.chainId ?? '' }}", + "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": "Committee ID", + "expr": "{{row.id}}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ (form.committees ?? '') + '' === (row.id ?? '') + '' ? 'Selected' : 'Select' }}", + "emit": { + "op": "select" + } + }, + "span": { "base": 12 } + }, + { + "id": "amount", + "name": "amount", + "type": "amount", + "label": "Amount", + "required": true, + "min": 0.000001, + "validation": { + "messages": { + "required": "Amount is required", + "min": "Amount must be greater than 0" + } + }, + "span": { "base": 12, "md": 6 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo (optional)", + "placeholder": "Optional message", + "value": "{{params.memo || ''}}", + "autoPopulate": "once", + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12, "md": 6 } + } + ], + "confirmation": { + "title": "Confirm DEX Deposit", + "summary": [ + { + "label": "Committee", + "value": "{{form.committees}}" + }, + { + "label": "Amount", + "value": "{{numberToLocaleString<{{form.amount}}>}} {{chain.denom.symbol}}" + } + ], + "btn": { + "icon": "Droplets", + "label": "Deposit Liquidity" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "amount": { + "value": "{{toMicroDenom<{{form.amount}}>}}", + "coerce": "number" + }, + "committees": { + "value": "{{ form.committees ?? ds['admin.config']?.chainId ?? '' }}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-dex-liquidity-deposit", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "DEX Deposit Submitted", + "description": "{{result}}" + } + } + }, + { + "id": "dexLiquidityWithdraw", + "title": "DEX Liquidity Withdraw", + "icon": "CircleDashed", + "ds": { + "admin.config": {}, + "__options": { + "staleTimeMs": 6000, + "refetchIntervalMs": 6000, + "refetchOnMount": true, + "refetchOnWindowFocus": false, + "critical": [ + "admin.config" + ] + } + }, + "ui": { + "slots": { + "modal": { + "className": "sm:max-w-[32rem] md:max-w-[46rem]" + } + } + }, + "form": { + "info": { + "title": "DEX Withdraw Notes", + "items": [ + { + "label": "Operation", + "value": "Withdraw removes liquidity by percent (1-100)", + "icon": "CircleDashed" + }, + { + "label": "Percent", + "value": "{{form.percent || 0}}%", + "icon": "BarChart3" + }, + { + "label": "Committee", + "value": "{{form.committees || ds['admin.config']?.chainId || '-'}}", + "icon": "Info" + } + ] + }, + "fields": [ + { + "id": "address", + "name": "address", + "type": "text", + "label": "Address", + "value": "{{params.address || account.address}}", + "autoPopulate": "always", + "readOnly": true, + "span": { "base": 12 } + }, + { + "id": "committees", + "name": "committees", + "type": "tableSelect", + "label": "Committee", + "required": true, + "help": "Select the target committee for the liquidity withdraw.", + "multiple": false, + "rowKey": "id", + "selectMode": "action", + "value": "{{ params.committees ?? ds['admin.config']?.chainId ?? '' }}", + "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": "Committee ID", + "expr": "{{row.id}}", + "align": "left" + } + ], + "rowAction": { + "title": "Action", + "label": "{{ (form.committees ?? '') + '' === (row.id ?? '') + '' ? 'Selected' : 'Select' }}", + "emit": { + "op": "select" + } + }, + "span": { "base": 12 } + }, + { + "id": "percent", + "name": "percent", + "type": "range", + "label": "Withdraw Percentage", + "required": true, + "min": 1, + "max": 100, + "step": 1, + "suffix": "%", + "showInput": true, + "marks": [1, 25, 50, 75, 100], + "presets": [ + { "label": "25%", "value": 25 }, + { "label": "50%", "value": 50 }, + { "label": "75%", "value": 75 }, + { "label": "100%", "value": 100 } + ], + "help": "Use the slider or direct input. Allowed range: 1% to 100%.", + "value": "{{params.percent ?? 50}}", + "autoPopulate": "once", + "validation": { + "messages": { + "required": "Percent is required", + "min": "Percent must be at least {{min}}", + "max": "Percent cannot exceed {{max}}" + } + }, + "span": { "base": 12, "md": 12 } + }, + { + "id": "memo", + "name": "memo", + "type": "textarea", + "label": "Memo (optional)", + "placeholder": "Optional message", + "value": "{{params.memo || ''}}", + "autoPopulate": "once", + "span": { "base": 12 } + }, + { + "id": "fee", + "name": "fee", + "type": "amount", + "label": "Fee", + "value": "{{params.fee ?? 0}}", + "autoPopulate": "once", + "min": 0, + "span": { "base": 12, "md": 12 } + } + ], + "confirmation": { + "title": "Confirm DEX Withdraw", + "summary": [ + { + "label": "Committee", + "value": "{{form.committees}}" + }, + { + "label": "Percent", + "value": "{{form.percent}}%" + } + ], + "btn": { + "icon": "CircleDashed", + "label": "Withdraw Liquidity" + } + } + }, + "payload": { + "address": { + "value": "{{form.address || account.address}}", + "coerce": "string" + }, + "percent": { + "value": "{{form.percent}}", + "coerce": "number" + }, + "committees": { + "value": "{{ form.committees ?? ds['admin.config']?.chainId ?? '' }}", + "coerce": "string" + }, + "fee": { + "value": "{{toMicroDenom<{{form.fee || 0}}>}}", + "coerce": "number" + }, + "memo": { + "value": "{{form.memo || ''}}", + "coerce": "string" + }, + "submit": { + "value": true, + "coerce": "boolean" + }, + "password": { + "value": "{{session.password}}", + "coerce": "string" + } + }, + "submit": { + "base": "admin", + "path": "/v1/admin/tx-dex-liquidity-withdraw", + "method": "POST" + }, + "notifications": { + "onSuccess": { + "variant": "success", + "title": "DEX Withdraw Submitted", + "description": "{{result}}" + } + } + } + ] +} diff --git a/cmd/rpc/web/wallet-new/scripts/generate-chain-config.js b/cmd/rpc/web/wallet-new/scripts/generate-chain-config.js new file mode 100644 index 000000000..1e07644b7 --- /dev/null +++ b/cmd/rpc/web/wallet-new/scripts/generate-chain-config.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +/** + * Generate chain.json with RPC URLs from environment variables + * This script runs before the build to inject the correct RPC endpoints + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Read environment variables +const rpcTarget = process.env.VITE_WALLET_RPC_PROXY_TARGET || 'http://localhost:50002'; +const adminRpcTarget = process.env.VITE_WALLET_ADMIN_RPC_PROXY_TARGET || 'http://localhost:50003'; +const rootRpcTarget = process.env.VITE_ROOT_WALLET_RPC_PROXY_TARGET || 'http://localhost:50002'; + +// Path to chain.json template and output +const templatePath = path.join(__dirname, '../public/plugin/canopy/chain.json.template'); +const outputPath = path.join(__dirname, '../public/plugin/canopy/chain.json'); + +// Check if template exists, if not use the current chain.json as template +let chainConfig; +if (fs.existsSync(templatePath)) { + chainConfig = JSON.parse(fs.readFileSync(templatePath, 'utf-8')); +} else if (fs.existsSync(outputPath)) { + // Use existing chain.json as template + chainConfig = JSON.parse(fs.readFileSync(outputPath, 'utf-8')); +} else { + console.error('Error: chain.json not found'); + process.exit(1); +} + +// Update RPC URLs +chainConfig.rpc = { + base: rpcTarget, + admin: adminRpcTarget, + root: rootRpcTarget +}; + +// Write the updated config +fs.writeFileSync(outputPath, JSON.stringify(chainConfig, null, 2)); + +console.log(`✅ Generated chain.json with RPC targets:`); +console.log(` - base: ${rpcTarget}`); +console.log(` - admin: ${adminRpcTarget}`); +console.log(` - root: ${rootRpcTarget}`); 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..84075fd30 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ActionRunner.tsx @@ -0,0 +1,824 @@ +// 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 { + 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"; +import { usePopulateController } from "./usePopulateController"; + +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 || {}, + ); + const [txRes, setTxRes] = React.useState(null); + const [localDs, setLocalDs] = React.useState>({}); + // 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: globalParams, isLoading } = useConfig(); + const { selectedAccount } = useAccounts?.() ?? { selectedAccount: undefined }; + const session = useSession(); + + // Merge global params with prefilledData so templates can access both via {{ params.fieldName }} + const params = React.useMemo(() => ({ + ...globalParams, + ...prefilledData, + }), [globalParams, prefilledData]); + + const action = React.useMemo( + () => manifest?.actions.find((a) => a.id === actionId), + [manifest, actionId], + ); + + // Keep a normalized, non-debounced form for dynamic visibility/showIf and payload coherence. + const normalizedLiveForm = React.useMemo( + () => normalizeFormForAction(action as any, form), + [action, form], + ); + + // 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: normalizedLiveForm, + chain, + account: selectedAccount + ? { + address: selectedAccount.address, + nickname: selectedAccount.nickname, + pubKey: selectedAccount.publicKey, + } + : undefined, + params, + }), + [normalizedLiveForm, chain, selectedAccount, params], + ); + + const { ds: actionDs, isLoading: isDsLoading, fetchStatus: dsFetchStatus } = useActionDs( + actionDsConfig, + dsCtx, + actionId, + selectedAccount?.address, + ); + + // Extract critical DS keys from manifest (DS that must load before showing form) + const criticalDsKeys = React.useMemo(() => { + const dsOptions = actionDsConfig?.__options || {}; + const critical = dsOptions.critical; + if (Array.isArray(critical)) return critical; + // Default: keystore is always critical for address selects + return ["keystore"]; + }, [actionDsConfig]); + + // Detect if this is an edit operation (prefilledData contains operator/address) + const isEditMode = React.useMemo(() => { + return !!(prefilledData?.operator || prefilledData?.address); + }, [prefilledData]); + + // 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: normalizedLiveForm, + 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, + }), + [ + normalizedLiveForm, + 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, // optional + 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( + () => normalizedLiveForm, + [normalizedLiveForm], + ); + 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 httpRes = await fetch(host + submitPath, { + method: action!.submit?.method, + headers: action!.submit?.headers ?? { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + let res: any = null; + try { + res = await httpRes.json(); + } catch { + // Some endpoints may return plain text (e.g., tx hash) or empty body. + try { + const txt = await httpRes.text(); + res = txt || null; + } catch { + res = null; + } + } + + + setTxRes(res); + + // Success detection prioritizes HTTP status to avoid false negatives on valid payloads + // like {"approve": false, ...} or {"address":"..."}. + const hasExplicitError = + !!res?.error || + res?.ok === false || + res?.success === false || + (typeof res?.status === "number" && res.status >= 400); + + const isSuccess = + httpRes.ok && + (typeof res === "string" || + res == null || + (typeof res === "object" && !hasExplicitError)); + 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) { + // optional: show toast or shake the button + return; + } + if (hasSummary) { + setStage("confirm"); + } else { + void doExecute(); + } + }, [formHasErrors, hasSummary, doExecute]); + + const onConfirm = React.useCallback(() => { + if (formHasErrors) { + // optional: 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]); + + // Use PopulateController for phase-based form initialization + // This replaces the old auto-populate useEffect with a cleaner approach + const { phase: populatePhase, showLoading: showPopulateLoading } = usePopulateController({ + fields: allFields, // Use all fields, not just visible ones, for initial populate + form, + ds: mergedDs, + isDsLoading, + criticalDsKeys, + dsFetchStatus, // Pass fetch status to check if DS completed (success or error) + templateContext: templatingCtx, + onFormChange: (patch) => setForm(prev => ({ ...prev, ...patch })), + prefilledData, + isEditMode, + }); + + const handleErrorsChange = React.useCallback( + (errs: Record, hasErrors: boolean) => { + setErrorsMap(errs); + setFormHasErrors(hasErrors); + }, + [], + ); + + const hasStepErrors = React.useMemo(() => { + const evalCtx = { ...templatingCtx, form }; + const missingRequired = visibleFieldsForStep.some((f: any) => { + // Evaluate required - can be boolean or template string + let isRequired = false; + if (typeof f.required === "boolean") { + isRequired = f.required; + } else if (typeof f.required === "string") { + try { + isRequired = templateBool(f.required, evalCtx); + } catch { + isRequired = false; + } + } + return isRequired && (form[f.name] == null || form[f.name] === ""); + }); + const fieldErrors = visibleFieldsForStep.some( + (f: any) => !!errorsMap[f.name], + ); + return missingRequired || fieldErrors; + }, [visibleFieldsForStep, form, errorsMap, templatingCtx]); + + const isLastStep = !wizard || stepIdx >= steps.length - 1; + const isOrdersAction = React.useMemo(() => /^(order|dex)/i.test(actionId), [actionId]); + const stepProgress = + wizard && steps.length > 0 ? Math.round(((stepIdx + 1) / steps.length) * 100) : 0; + + 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} +
+
+
+
+
+
+ {steps.map((step: any, idx: number) => { + const isActive = idx === stepIdx; + const isCompleted = idx < stepIdx; + return ( + + {idx + 1} + {step?.title ?? `Step ${idx + 1}`} + + ); + })} +
+
+ )} + {/* Show skeleton loading while waiting for critical DS */} + {showPopulateLoading && ( +
+
+
+
+
+
Loading form data...
+
+
+ )} + {!showPopulateLoading && ( +
+ +
+ )} + + {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..b10758778 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ActionsModal.tsx @@ -0,0 +1,141 @@ +// ActionsModal.tsx +import React, { useEffect, useMemo, useState, Suspense } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { createPortal } from 'react-dom' +import { ModalTabs, Tab } from './ModalTabs' +import { Action as ManifestAction } from '@/manifest/types' +import { XIcon, Loader2 } from 'lucide-react' +import { cx } from '@/ui/cx' + +const ActionRunner = React.lazy(() => import('@/actions/ActionRunner')) + +const ActionRunnerFallback = () => ( +
+ + Loading action... +
+) + +interface ActionModalProps { + actions?: (ManifestAction & { prefilledData?: Record })[] + isOpen: boolean + onClose: () => void + prefilledData?: Record +} + +export const ActionsModal: React.FC = ({ + actions, + isOpen, + onClose, + prefilledData: propPrefilledData, +}) => { + 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]) + + const modalNode = ( + + {isOpen && ( + + e.stopPropagation()} + > + + +
+ +
+ + {selectedTab && ( + + }> + a.id === selectedTab.value)?.prefilledData + } + /> + + + )} +
+
+ )} +
+ ) + + if (typeof document === 'undefined') { + return null + } + + return ( + createPortal(modalNode, document.body) + ) +} 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..3f5b01a2a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ComboSelect.tsx @@ -0,0 +1,247 @@ +// ComboSelect.tsx — assigns a free value and shows it as a selected "extra option" +// (SAME DESIGN: same classes and tokens as your version) +"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; + + /** Allows assigning the typed text as the select value (without adding it to the list). */ + allowAssign?: boolean; + /** Enter confirms the text even if not in options (keyboard shortcut). */ + allowFreeInput?: boolean; + + // Style + 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); + + // Temporary "extra" option when a free value is assigned + const [tempOption, setTempOption] = React.useState(null); + + // If `value` comes from outside and doesn't exist in options, create/update tempOption so it shows as selected + 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]); + + // List to render = options + tempOption (if applicable). We don't mutate the 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; + // Create/update the temporary option and select it + const opt = {value: v, label: v}; + setTempOption(opt); + onChange(v, {assigned: true}); // <- only assigns; doesn't persist in global options + 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-card border border-border/70" + } + > + {/* Input */} +
+ setQuery(e.target.value)} + onKeyDown={onKeyDown} + placeholder={placeholder} + className="w-full bg-transparent outline-none placeholder:text-muted-foreground text-sm" + /> +
+ +
+ {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..48d411dc1 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/Confirm.tsx @@ -0,0 +1,64 @@ +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..e366f6122 --- /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..c7eeb2e4a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/FormRenderer.tsx @@ -0,0 +1,186 @@ +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"; +import { templateBool } from "@/core/templater"; + +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( + () => ({ + // Prefer parent-provided form context (already normalized by runtime), + // fallback to raw value for backwards compatibility. + form: ctx?.form ?? value, + 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, index: number) => ({ + ...f, + // Use id, name, or index for unique key - important for visual fields (section, heading, etc) that don't have name + __key: `${f.tab ?? "default"}:${f.group ?? ""}:${f.id ?? f.name ?? `field-${index}`}`, + })), + [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) => { + // Evaluate required - can be boolean or template string + let isRequired = false; + if (typeof f.required === "boolean") { + isRequired = f.required; + } else if (typeof f.required === "string") { + try { + isRequired = templateBool(f.required, templateContext); + } catch { + isRequired = false; + } + } + return isRequired && (value[f.name] == null || value[f.name] === ""); + }); + return anyMsg || requiredMissing; + }, [errors, fields, value, templateContext]); + + 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..d820e664c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/ModalTabs.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/Tabs"; + +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, + }) => { + const activeValue = activeTab?.value ?? tabs[0]?.value ?? ""; + + return ( +
+ { + const next = tabs.find((tab) => tab.value === value); + if (next) onTabChange?.(next); + }} + > + + {tabs.map((tab, index) => ( + + {tab.icon ? : null} + {tab.label} + + ))} + + +
+ ); +}; 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..3a01633a0 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/Option.tsx @@ -0,0 +1,40 @@ +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..5388d5d5e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/OptionCard.tsx @@ -0,0 +1,40 @@ +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..18e2f33fe --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/Result.tsx @@ -0,0 +1,15 @@ +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..8d9874b68 --- /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' // adjust path if needed + +/** Basic manifest types */ +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 or key -> image URL (if none, falls back to avatar) + alt?: string // optional expr for alt + initialsFrom?: string // expr/key to derive initials and color if no 'src' + size?: number // avatar/image size in px (default 28) + + /** HTML */ + html?: string // HTML templated (rendered with dangerouslySetInnerHTML) +} + +export type TableRowAction = { + title?: string // header title for the action column + label?: string // button label template + icon?: string // (reserved) for later use with a central icon set + showIf?: string // conditional template + disabledIf?: string // conditional template to disable the button + emit?: { + op: 'set' | 'copy' | 'select' // select: mark selection; set: set another field; copy: to clipboard + field?: string // required for '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[] // static data + source?: { uses: string; selector?: string } // dynamic data: e.g. {uses:'ds', selector:'committees'} + rowAction?: TableRowAction + /** how selection works */ + selectMode?: 'row' | 'action' | 'none' // 'row' (default): click on row; 'action': button only; 'none': disabled +} + +/** 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 based on total column count (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 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..c8295b4a2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/WizardRunner.tsx @@ -0,0 +1,221 @@ +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..439fa19f4 --- /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 h-11 sm:h-12 bg-background/60 border placeholder:text-muted-foreground/70 text-foreground rounded-xl px-3 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 transition-colors' + const paddingRight = hasFeatures ? 'pr-20' : '' + const border = error ? 'border-red-600' : 'border-border/70' + + 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..e6fc3fef9 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/AmountField.tsx @@ -0,0 +1,64 @@ +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. + // showDenom can be disabled per field from manifest (config-first behavior). + const explicitShowDenom = (field as any).showDenom + const denom = (field as any).denom ?? templateContext?.chain?.denom?.symbol ?? '' + const showDenom = explicitShowDenom === false ? false : !!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 h-11 sm:h-12 bg-background/60 border placeholder:text-muted-foreground/70 text-foreground rounded-xl px-3 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none' + const border = error ? 'border-red-600' : 'border-border/70' + + 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/CollapsibleGroupField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/CollapsibleGroupField.tsx new file mode 100644 index 000000000..314a199eb --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/CollapsibleGroupField.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { cx } from "@/ui/cx"; + +type CollapsibleGroupFieldProps = { + field: any; + value: any; + templateContext: any; + resolveTemplate: (s?: any) => any; + onChange: (value: any) => void; +}; + +/** + * CollapsibleGroup field type - Toggleable advanced options section + * + * This field stores its collapsed state in the form as a boolean. + * Use with showIf on child fields to control their visibility. + * + * Schema: + * { + * "type": "collapsibleGroup", + * "id": "showAdvancedCommittees", + * "name": "showAdvancedCommittees", + * "title": "Advanced Options", + * "description": "Click to show advanced configuration", + * "icon": "Settings", + * "variant": "default" | "primary", + * "defaultExpanded": false, + * "span": { "base": 12 } + * } + * + * Then in child fields: + * { + * "showIf": "{{ form.showAdvancedCommittees }}" + * } + */ +export const CollapsibleGroupField: React.FC = ({ + field, + value, + resolveTemplate, + onChange, +}) => { + const title = resolveTemplate(field.title) || "Advanced Options"; + const description = resolveTemplate(field.description); + const icon = field.icon || "Settings"; + const variant = field.variant || "default"; + + // Use form value for expanded state, default to field.defaultExpanded + const isExpanded = value === true || value === "true"; + + // Initialize with defaultExpanded if value is undefined + React.useEffect(() => { + if (value === undefined && field.defaultExpanded) { + onChange(true); + } + }, []); + + const handleToggle = () => { + onChange(!isExpanded); + }; + + const span = field.span?.base ?? 12; + + // Variant styling + const variantStyles: Record = { + default: { + bg: "bg-card/30", + border: "border-border/50", + text: "text-muted-foreground", + icon: "text-muted-foreground", + hover: "hover:bg-card/50 hover:border-border", + }, + primary: { + bg: "bg-primary/5", + border: "border-primary/20", + text: "text-primary/80", + icon: "text-primary/60", + hover: "hover:bg-primary/10 hover:border-primary/30", + }, + }; + + const styles = variantStyles[variant] || variantStyles.default; + + return ( + + + + ); +}; diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/DividerField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/DividerField.tsx new file mode 100644 index 000000000..73f4e867e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/DividerField.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { cx } from "@/ui/cx"; + +type DividerFieldProps = { + field: any; + resolveTemplate: (s?: any) => any; +}; + +/** + * Divider field type - Horizontal separator + * + * Schema: + * { + * "type": "divider", + * "label": "Optional label text", + * "variant": "solid" | "dashed" | "dotted" | "gradient", + * "spacing": "sm" | "md" | "lg", // Vertical spacing + * "span": { "base": 12 } + * } + */ +export const DividerField: React.FC = ({ + field, + resolveTemplate, +}) => { + const label = resolveTemplate(field.label); + const variant = field.variant || "solid"; + const spacing = field.spacing || "md"; + const span = field.span?.base ?? 12; + + const spacingStyles: Record = { + sm: "my-2", + md: "my-4", + lg: "my-6", + }; + + const variantStyles: Record = { + solid: "border-t border-border", + dashed: "border-t border-dashed border-border", + dotted: "border-t border-dotted border-border", + gradient: + "h-px bg-gradient-to-r from-transparent via-bg-accent to-transparent", + }; + + return ( +
+ {label ? ( +
+
+
+ + {label} + +
+
+ ) : ( +
+ )} +
+ ); +}; 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..88a393b7a --- /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..4c28c447e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/FieldWrapper.tsx @@ -0,0 +1,52 @@ +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/HeadingField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/HeadingField.tsx new file mode 100644 index 000000000..58db089d4 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/HeadingField.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { cx } from "@/ui/cx"; + +type HeadingFieldProps = { + field: any; + templateContext: any; + resolveTemplate: (s?: any) => any; +}; + +/** + * Heading field type - Text headings/titles + * + * Schema: + * { + * "type": "heading", + * "text": "Heading text", + * "level": 1 | 2 | 3 | 4, // h1, h2, h3, h4 + * "icon": "Settings", // Optional Lucide icon + * "align": "left" | "center" | "right", + * "color": "primary" | "secondary" | "muted" | "accent", + * "span": { "base": 12 } + * } + */ +export const HeadingField: React.FC = ({ + field, + resolveTemplate, +}) => { + const text = resolveTemplate(field.text); + const level = field.level || 2; + const icon = field.icon; + const align = field.align || "left"; + const color = field.color || "primary"; + const span = field.span?.base ?? 12; + + const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; + + const sizeStyles: Record = { + 1: "text-2xl font-bold", + 2: "text-xl font-semibold", + 3: "text-lg font-semibold", + 4: "text-base font-medium", + }; + + const colorStyles: Record = { + primary: "text-foreground", + secondary: "text-text-secondary", + muted: "text-muted-foreground", + accent: "text-primary", + }; + + const alignStyles: Record = { + left: "justify-start text-left", + center: "justify-center text-center", + right: "justify-end text-right", + }; + + return ( + +
+ {icon && ( + + )} + + {text} + +
+
+ ); +}; diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/NumberField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/NumberField.tsx new file mode 100644 index 000000000..19db1b22b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/NumberField.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +export const NumberField: React.FC = ({ + field, + value, + error, + templateContext, + dsValue, + onChange, + resolveTemplate, + setVal, +}) => { + const currentValue = value ?? (dsValue?.value ?? '') + const hasFeatures = !!(field.features?.length) + + const step = (field as any).integer ? 1 : (field as any).step ?? 'any' + const common = 'w-full h-11 sm:h-12 bg-background/60 border placeholder:text-muted-foreground/70 text-foreground rounded-xl px-3 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none' + const border = error ? 'border-red-600' : 'border-border/70' + const paddingRight = hasFeatures ? 'pr-24' : '' + + return ( + + onChange(e.currentTarget.value)} + min={(field as any).min} + max={(field as any).max} + /> + + ) +} 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/RangeField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/RangeField.tsx new file mode 100644 index 000000000..860898a62 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/RangeField.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import { cx } from '@/ui/cx' +import { FieldWrapper } from './FieldWrapper' +import { BaseFieldProps } from './types' + +const toFiniteNumber = (value: any, fallback: number) => { + const n = Number(value) + return Number.isFinite(n) ? n : fallback +} + +export const RangeField: React.FC = ({ + field, + value, + error, + templateContext, + dsValue, + onChange, + resolveTemplate, + setVal, +}) => { + const rawMin = resolveTemplate((field as any).min) + const rawMax = resolveTemplate((field as any).max) + const rawStep = resolveTemplate((field as any).step) + + const min = toFiniteNumber(rawMin, 0) + const max = toFiniteNumber(rawMax, 100) + const step = toFiniteNumber(rawStep, 1) + + const fallbackValue = toFiniteNumber(resolveTemplate((field as any).value), min) + const dsRaw = dsValue?.value ?? dsValue + const current = toFiniteNumber(value ?? dsRaw, fallbackValue) + const clamped = Math.min(max, Math.max(min, current)) + + const showInput = (field as any).showInput !== false + const suffix = resolveTemplate((field as any).suffix ?? '') + const marks = Array.isArray((field as any).marks) ? (field as any).marks : [] + const presets = Array.isArray((field as any).presets) ? (field as any).presets : [] + const hasFeatures = !!(field.features?.length) + const range = max - min + const progress = range > 0 ? ((clamped - min) / range) * 100 : 0 + + const inputClass = cx( + 'w-full h-11 sm:h-12 bg-background/60 border placeholder:text-muted-foreground/70 text-foreground rounded-xl px-3 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none', + error ? 'border-red-600' : 'border-border/70', + hasFeatures && showInput ? 'pr-24' : '', + ) + + return ( + +
+
+ {min}{suffix} + {clamped}{suffix} + {max}{suffix} +
+ +
+
+
+
+ onChange(Number(e.currentTarget.value))} + /> +
+ + {marks.length > 0 && ( +
+ {marks.map((m: any, i: number) => ( + {m}{suffix} + ))} +
+ )} + + {presets.length > 0 && ( +
+ {presets.map((preset: any, i: number) => { + const presetValue = toFiniteNumber(resolveTemplate(preset?.value), clamped) + const active = presetValue === clamped + return ( + + ) + })} +
+ )} + + {showInput && ( + onChange(Number(e.currentTarget.value))} + /> + )} +
+ + ) +} diff --git a/cmd/rpc/web/wallet-new/src/actions/fields/SectionField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/SectionField.tsx new file mode 100644 index 000000000..f80d762d4 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/SectionField.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { cx } from "@/ui/cx"; + +type SectionFieldProps = { + field: any; + templateContext: any; + resolveTemplate: (s?: any) => any; +}; + +/** + * Section field type - Visual grouping component + * + * Schema: + * { + * "type": "section", + * "title": "Section Title", + * "description": "Optional description text", + * "icon": "Settings", // Optional Lucide icon name + * "variant": "default" | "info" | "warning" | "success" | "error" | "primary", + * "collapsible": false, + * "defaultCollapsed": false, + * "span": { "base": 12 } + * } + */ +export const SectionField: React.FC = ({ + field, + resolveTemplate, +}) => { + const title = resolveTemplate(field.title); + const description = resolveTemplate(field.description); + const icon = field.icon; + const variant = field.variant || "default"; + const collapsible = field.collapsible || false; + const [collapsed, setCollapsed] = React.useState(field.defaultCollapsed || false); + + const span = field.span?.base ?? 12; + + // Variant styling + const variantStyles: Record = { + default: { + bg: "bg-card/50", + border: "border-border", + text: "text-foreground", + icon: "text-muted-foreground", + }, + info: { + bg: "bg-blue-950/30", + border: "border-blue-800/30", + text: "text-blue-100", + icon: "text-blue-400", + }, + warning: { + bg: "bg-yellow-950/30", + border: "border-yellow-800/30", + text: "text-yellow-100", + icon: "text-yellow-400", + }, + success: { + bg: "bg-emerald-950/30", + border: "border-emerald-800/30", + text: "text-emerald-100", + icon: "text-emerald-400", + }, + error: { + bg: "bg-red-950/30", + border: "border-red-800/30", + text: "text-red-100", + icon: "text-red-400", + }, + primary: { + bg: "bg-primary/10", + border: "border-primary/30", + text: "text-primary-foreground", + icon: "text-primary", + }, + }; + + const styles = variantStyles[variant] || variantStyles.default; + + return ( + +
+
collapsible && setCollapsed(!collapsed)} + > + {icon && ( +
+ +
+ )} +
+ {title && ( +

+ {title} +

+ )} + {description && !collapsed && ( +

{description}

+ )} +
+ {collapsible && ( +
+ +
+ )} +
+
+
+ ); +}; 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..1da7d1fab --- /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/SpacerField.tsx b/cmd/rpc/web/wallet-new/src/actions/fields/SpacerField.tsx new file mode 100644 index 000000000..68dfaf984 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/SpacerField.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { cx } from "@/ui/cx"; + +type SpacerFieldProps = { + field: any; +}; + +/** + * Spacer field type - Empty space for layout + * + * Schema: + * { + * "type": "spacer", + * "height": "sm" | "md" | "lg" | "xl" | "2xl", + * "span": { "base": 12 } + * } + */ +export const SpacerField: React.FC = ({ field }) => { + const height = field.height || "md"; + const span = field.span?.base ?? 12; + + const heightStyles: Record = { + sm: "h-2", + md: "h-4", + lg: "h-6", + xl: "h-8", + "2xl": "h-12", + }; + + 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..bdabd03eb --- /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-muted 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..1f0ac5fed --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/TextField.tsx @@ -0,0 +1,75 @@ +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) + + // Track previous resolved value to sync when template changes + const prevResolvedRef = React.useRef(null) + + // Sync field value when the resolved template changes (e.g., table selection) + // This allows computed fields to stay in sync while still being editable + React.useEffect(() => { + if (field.value && resolvedValue != null) { + const resolvedStr = String(resolvedValue) + if (prevResolvedRef.current !== null && prevResolvedRef.current !== resolvedStr) { + // Template value changed, sync the input + onChange(resolvedStr) + } + prevResolvedRef.current = resolvedStr + } + }, [resolvedValue, field.value, onChange]) + + // For readOnly fields with a value template, always use the resolved template + // For editable fields, use form value but initialize from template if empty + const currentValue = + field.readOnly && field.value && resolvedValue != null + ? resolvedValue + : value === '' && resolvedValue != null + ? resolvedValue + : value || (dsValue?.amount ?? dsValue?.value ?? '') + + const hasFeatures = !!(field.features?.length) + const commonBase = 'w-full bg-background/60 border placeholder:text-muted-foreground/70 text-foreground rounded-xl px-3 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 transition-colors' + const common = isTextarea + ? `${commonBase} py-2.5 min-h-[112px] resize-y` + : `${commonBase} h-11 sm:h-12` + const paddingRight = hasFeatures ? 'pr-24' : '' // Increased padding for better button spacing + const border = error ? 'border-red-600' : 'border-border/70' + + 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..8b73afb5e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/fieldRegistry.tsx @@ -0,0 +1,58 @@ +import { TextField } from "./TextField"; +import { AmountField } from "./AmountField"; +import { NumberField } from "./NumberField"; +import { RangeField } from "./RangeField"; +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"; +import { SectionField } from "./SectionField"; +import { DividerField } from "./DividerField"; +import { SpacerField } from "./SpacerField"; +import { HeadingField } from "./HeadingField"; +import { CollapsibleGroupField } from "./CollapsibleGroupField"; +import type { FC } from "react"; + +type FieldComponent = FC; + +/** + * Central registry for all field types used in the manifest-driven forms. + * Maps field type strings to their corresponding React components. + * + * IMPORTANT: All imports must be kept even if the linter marks them as unused. + * These components are used dynamically based on manifest configuration. + */ +export const fieldRegistry: Record = { + text: TextField, + textarea: TextField, + amount: AmountField, + number: NumberField, + range: RangeField, + address: AddressField, + select: SelectField, + advancedSelect: AdvancedSelectField, + switch: SwitchField, + option: OptionField, + optionCard: OptionCardField, + tableSelect: TableSelectField, + dynamicHtml: DynamicHtmlField, + // Layout and structural fields - DO NOT REMOVE + section: SectionField, + divider: DividerField, + spacer: SpacerField, + heading: HeadingField, + collapsibleGroup: CollapsibleGroupField, +}; + +/** + * Gets the renderer component for a given field type + * @param fieldType - The type of field to render (e.g., "text", "amount", "section") + * @returns The field component or null if not found + */ +export const getFieldRenderer = (fieldType: string): FieldComponent | 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..556685bfc --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/fields/index.ts @@ -0,0 +1,15 @@ +export { TextField } from './TextField' +export { AmountField } from './AmountField' +export { NumberField } from './NumberField' +export { RangeField } from './RangeField' +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..2a49cbea2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/useActionDs.ts @@ -0,0 +1,297 @@ +import React from "react"; +import { useDS } from "@/core/useDs"; +import { template, templateBool, 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') { + const trimmed = val.trim(); + // Consider "undefined" string as empty (failed template resolution) + if (trimmed === '' || trimmed === 'undefined' || trimmed === 'null') return true; + } + return false; + }; + + // Helper to check if DS params have all required values + // Returns true only if ALL leaf values are non-empty + 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 - ALL must be non-empty for the DS to be valid + const checkDeep = (obj: any, depth = 0): boolean => { + if (obj === null || obj === undefined) return false; + + if (typeof obj === 'string') { + const trimmed = obj.trim(); + // Reject empty strings and "undefined"/"null" strings + if (trimmed === '' || trimmed === 'undefined' || trimmed === 'null') { + return false; + } + return true; + } + + if (typeof obj === 'number' || typeof obj === 'boolean') { + return true; + } + + if (Array.isArray(obj)) { + // Empty arrays at depth 0 are invalid, but nested empty arrays are ok + if (obj.length === 0 && depth === 0) return false; + // ALL array items must be valid + return obj.every(item => checkDeep(item, depth + 1)); + } + + if (typeof obj === 'object') { + const values = Object.values(obj); + // Empty object at depth 0 is valid (no params needed) + if (values.length === 0) return depth === 0; + // ALL object values must be valid + return values.every(v => checkDeep(v, depth + 1)); + } + + return true; + }; + + return checkDeep(params, 0); + }; + + // 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) + // Use templateBool which properly handles "undefined", "null", "", "false" as false + const enabledValue = dsLocalOptions.enabled ?? globalOptions.enabled ?? true; + let isManuallyEnabled = true; + if (typeof enabledValue === 'string') { + try { + // templateBool correctly handles edge cases like empty string, "undefined", etc. + isManuallyEnabled = templateBool(enabledValue, ctx); + } catch { + isManuallyEnabled = false; + } + } else if (typeof enabledValue === 'boolean') { + isManuallyEnabled = enabledValue; + } 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) => { + const queryResult = allDsResults[idx]; + return { + dsKey: config.dsKey, + // Spread query result but override isEnabled with our config value + data: queryResult.data, + isLoading: queryResult.isLoading, + isFetched: queryResult.isFetched, + error: queryResult.error, + refetch: queryResult.refetch, + // Our config-based enabled flag (whether we intended to enable this DS) + isEnabled: config.dsOptions?.enabled ?? false, + }; + }); + }, [dsConfigs, ...allDsResults.map(d => d.data), ...allDsResults.map(d => d.isFetched)]); + + // 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); + + // Build a map of DS key -> fetch status + // A DS is "fetched" when: + // - It's not enabled (no fetch needed), OR + // - It has been fetched at least once (success or error) + const fetchStatus = React.useMemo(() => { + const status: Record = {}; + for (const result of dsResults) { + if (!result.dsKey || result.dsKey === "__disabled__") continue; + status[result.dsKey] = { + // Consider "fetched" if: disabled (no fetch needed) OR actually fetched + isFetched: !result.isEnabled || result.isFetched === true, + isLoading: result.isLoading ?? false, + hasError: !!result.error, + }; + } + return status; + }, [dsResults]); + + return { + ds: allDsData, + isLoading, + hasError, + fetchStatus, // New: per-DS fetch status + 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/usePopulateController.ts b/cmd/rpc/web/wallet-new/src/actions/usePopulateController.ts new file mode 100644 index 000000000..f10ef1785 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/usePopulateController.ts @@ -0,0 +1,251 @@ +import React from "react"; +import { template } from "@/core/templater"; + +/** + * Populate Controller - manages form initialization phases + * + * Phases: + * - "waiting": Critical DS is loading, form shows skeleton/loading state + * - "initializing": DS ready, running initial populate + * - "ready": Form is interactive, DS only updates display/validation (not form values) + * + * This solves race conditions between DS loading and form population. + */ + +export type PopulatePhase = "waiting" | "initializing" | "ready"; + +export interface PopulateControllerConfig { + /** All fields from the current step/form */ + fields: any[]; + /** Current form values */ + form: Record; + /** Data sources (merged) */ + ds: Record; + /** Is DS currently loading? */ + isDsLoading: boolean; + /** Critical DS keys that must load before showing form (e.g., ['keystore', 'validator']) */ + criticalDsKeys: string[]; + /** Fetch status per DS key - tells us if a DS has completed (success OR error) */ + dsFetchStatus?: Record; + /** Template context for resolving field values */ + templateContext: Record; + /** Callback to update form values */ + onFormChange: (patch: Record) => void; + /** Prefilled data passed to the form (edit mode) */ + prefilledData?: Record; + /** Whether this is an edit operation (affects which DS are critical) */ + isEditMode?: boolean; +} + +export interface PopulateControllerResult { + /** Current phase */ + phase: PopulatePhase; + /** Fields that have been populated at least once */ + populatedFields: Set; + /** Whether form should show loading/skeleton */ + showLoading: boolean; + /** Whether critical DS has loaded */ + criticalDsReady: boolean; + /** Force re-run initial populate (for edge cases) */ + reinitialize: () => void; +} + +export function usePopulateController({ + fields, + form, + ds, + isDsLoading, + criticalDsKeys, + dsFetchStatus, + templateContext, + onFormChange, + prefilledData, + isEditMode, +}: PopulateControllerConfig): PopulateControllerResult { + const [phase, setPhase] = React.useState("waiting"); + const [populatedFields, setPopulatedFields] = React.useState>( + new Set(prefilledData ? Object.keys(prefilledData) : []) + ); + + // Track if we've completed initial population + const hasInitializedRef = React.useRef(false); + + // Track DS snapshot at initialization time + const initialDsSnapshotRef = React.useRef(null); + + // Determine which DS are critical + const effectiveCriticalKeys = React.useMemo(() => { + const keys = new Set(criticalDsKeys); + + // keystore is always critical for address selects + keys.add("keystore"); + + // In edit mode, validator DS is often critical + if (isEditMode && prefilledData?.operator) { + keys.add("validator"); + } + + return Array.from(keys); + }, [criticalDsKeys, isEditMode, prefilledData?.operator]); + + // Check if all critical DS have loaded (completed fetch, regardless of success/error) + const criticalDsReady = React.useMemo(() => { + // Check each critical DS key using fetch status + for (const key of effectiveCriticalKeys) { + const status = dsFetchStatus?.[key]; + + // If we have fetch status, use it (more accurate) + if (status) { + // DS is ready if it has been fetched (success or error) and is not currently loading + if (!status.isFetched || status.isLoading) { + return false; + } + } else { + // Fallback: check if DS data exists (for backwards compatibility) + // Also consider DS ready if it doesn't exist in fetchStatus but isDsLoading is false + // This handles cases where the DS key doesn't exist in the config + if (isDsLoading) { + return false; + } + } + } + + return true; + }, [dsFetchStatus, effectiveCriticalKeys, isDsLoading]); + + // Run initial population when critical DS becomes ready + React.useEffect(() => { + // Skip if already initialized + if (hasInitializedRef.current) return; + + // Skip if critical DS not ready + if (!criticalDsReady) { + setPhase("waiting"); + return; + } + + // Transition to initializing + setPhase("initializing"); + + // Run initial populate + const defaults: Record = {}; + const newlyPopulated: string[] = []; + + for (const field of fields) { + const fieldName = field.name; + const fieldValue = field.value; + const autoPopulate = field.autoPopulate ?? "always"; + + // Skip fields without name (visual fields like section, divider) + if (!fieldName) continue; + + // Skip if autoPopulate is disabled + if (autoPopulate === false) continue; + + // Skip if already prefilled + if (prefilledData && prefilledData[fieldName] !== undefined) { + continue; + } + + // Skip if form already has a value (user might have typed something) + if (form[fieldName] !== undefined && form[fieldName] !== "" && form[fieldName] !== null) { + continue; + } + + // Try to resolve the default value + if (fieldValue != null) { + try { + const resolved = template(fieldValue, templateContext); + if (resolved !== undefined && resolved !== "" && resolved !== null) { + defaults[fieldName] = resolved; + newlyPopulated.push(fieldName); + } + } catch (e) { + // Template resolution failed, skip + console.warn(`[PopulateController] Failed to resolve default for ${fieldName}:`, e); + } + } + } + + // Apply defaults to form + if (Object.keys(defaults).length > 0) { + onFormChange(defaults); + } + + // Mark fields as populated + setPopulatedFields(prev => { + const next = new Set(prev); + newlyPopulated.forEach(f => next.add(f)); + return next; + }); + + // Mark initialization as complete + hasInitializedRef.current = true; + initialDsSnapshotRef.current = JSON.stringify(ds); + + // Transition to ready (slight delay for UI smoothness) + requestAnimationFrame(() => { + setPhase("ready"); + }); + }, [criticalDsReady, fields, form, templateContext, onFormChange, prefilledData, ds]); + + // Handle DS changes AFTER initialization (only for autoPopulate: "always" fields) + React.useEffect(() => { + // Only run in "ready" phase + if (phase !== "ready") return; + + // Skip if DS hasn't actually changed + const currentDsSnapshot = JSON.stringify(ds); + if (currentDsSnapshot === initialDsSnapshotRef.current) return; + + const updates: Record = {}; + + for (const field of fields) { + const fieldName = field.name; + const fieldValue = field.value; + const autoPopulate = field.autoPopulate; + + // Only update fields with explicit autoPopulate: "always" + // (default behavior after initialization is to NOT auto-populate) + if (autoPopulate !== "always") continue; + + // Skip fields without name + if (!fieldName) continue; + + // Resolve and update + if (fieldValue != null) { + try { + const resolved = template(fieldValue, templateContext); + if (resolved !== undefined && resolved !== null) { + // Only update if value changed + if (form[fieldName] !== resolved) { + updates[fieldName] = resolved; + } + } + } catch (e) { + // Skip + } + } + } + + if (Object.keys(updates).length > 0) { + onFormChange(updates); + } + }, [phase, ds, fields, templateContext, form, onFormChange]); + + // Reinitialize function for edge cases + const reinitialize = React.useCallback(() => { + hasInitializedRef.current = false; + initialDsSnapshotRef.current = null; + setPhase("waiting"); + setPopulatedFields(new Set(prefilledData ? Object.keys(prefilledData) : [])); + }, [prefilledData]); + + return { + phase, + populatedFields, + showLoading: phase === "waiting", + criticalDsReady, + reinitialize, + }; +} 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..1ad28a9d1 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/actions/validators.ts @@ -0,0 +1,254 @@ +// validators.ts +import type { Field, AmountField, NumberField, RangeField } from "@/manifest/types"; +import {template, templateBool} from "@/core/templater"; + +/** + * Evaluate the required field which can be a boolean or a template string + */ +function evalRequired(required: boolean | string | undefined, ctx: Record): boolean { + if (required === undefined || required === null) return false; + if (typeof required === "boolean") return required; + if (typeof required === "string") { + // Use templateBool which handles "undefined", "null", "", "false" correctly + try { + return templateBool(required, ctx); + } catch { + return false; + } + } + return !!required; +} + +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 (evalRequired(field.required, ctx) && (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 (evalRequired(field.required, ctx) && 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 (evalRequired(field.required, ctx) && (formattedValue == null || formattedValue === "")) { + return { + ok: false, + code: "required", + message: resolveMsg(messages, "required", { field, value, ...ctx }), + }; + } + + // AMOUNT / NUMBER + if (field.type === "amount" || field.type === "number" || field.type === "range") { + const f = field as AmountField | NumberField | RangeField; + + 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..22b6045b6 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Accounts.tsx @@ -0,0 +1,433 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { + ArrowLeftRight, + Box, + Layers, + Lock, + Search, + Send, + Shield, + Wallet, + TrendingUp, + TrendingDown, + Users, + Droplets, + Percent, +} from "lucide-react"; +import { useAccountData } from "@/hooks/useAccountData"; +import { useBalanceHistory } from "@/hooks/useBalanceHistory"; +import { useStakedBalanceHistory } from "@/hooks/useStakedBalanceHistory"; +import { useActionModal } from "@/app/providers/ActionModalProvider"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; + +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 { openAction } = useActionModal(); + const { chain } = useConfig(); + + const symbol = chain?.denom?.symbol || "CNPY"; + const decimals = chain?.denom?.decimals ?? 6; + const divisor = Math.pow(10, decimals); + + const [searchTerm, setSearchTerm] = useState(""); + + // ── Derived aggregates ──────────────────────────────────────────────────── + const totalLiquid = totalBalance - totalStaked; + const stakingRate = totalBalance > 0 ? (totalStaked / totalBalance) * 100 : 0; + const stakingCount = stakingData.filter(s => (s.staked || 0) > 0).length; + const liquidCount = accounts.length - stakingCount; + + const balanceChangePercentage = balanceHistory?.changePercentage ?? 0; + const stakedChangePercentage = stakedHistory?.changePercentage ?? 0; + + // ── Helpers ─────────────────────────────────────────────────────────────── + const fmt = (raw: number) => + (raw / divisor).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + + const fmtAddress = (addr: string) => + `${addr.slice(0, 5)}…${addr.slice(-6)}`; + + const getAccountIcon = (index: number) => { + const icons = [ + { icon: Wallet, bg: "bg-gradient-to-br from-primary/80 to-primary/40" }, + { icon: Layers, bg: "bg-gradient-to-br from-blue-500/80 to-blue-500/40" }, + { icon: ArrowLeftRight, bg: "bg-gradient-to-br from-purple-500/80 to-purple-500/40" }, + { icon: Shield, bg: "bg-gradient-to-br from-green-500/80 to-green-500/40" }, + { icon: Box, bg: "bg-gradient-to-br from-red-500/80 to-red-500/40" }, + ]; + return icons[index % icons.length]; + }; + + const getRealTotal = (address: string) => { + const liquid = balances.find(b => b.address === address)?.amount ?? 0; + const staked = stakingData.find(s => s.address === address)?.staked ?? 0; + return { liquid, staked, total: liquid + staked }; + }; + + const getStatusInfo = (address: string) => { + const staked = stakingData.find(s => s.address === address)?.staked ?? 0; + return staked > 0 + ? { label: "Staked", cls: "bg-primary/15 text-primary border border-primary/20" } + : { label: "Liquid", cls: "bg-muted/40 text-muted-foreground border border-border/60" }; + }; + + const processedAddresses = accounts.map((account, index) => { + const { liquid, staked, total } = getRealTotal(account.address); + const { label: statusLabel, cls: statusCls } = getStatusInfo(account.address); + const { icon, bg } = getAccountIcon(index); + return { + id: account.address, + fullAddress: account.address, + address: fmtAddress(account.address), + nickname: account.nickname || fmtAddress(account.address), + total, + liquid, + staked, + stakedPct: total > 0 ? (staked / total) * 100 : 0, + liquidPct: total > 0 ? (liquid / total) * 100 : 0, + statusLabel, + statusCls, + icon, + iconBg: bg, + }; + }); + + const filteredAddresses = processedAddresses.filter(addr => + addr.address.toLowerCase().includes(searchTerm.toLowerCase()) || + addr.nickname.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + const handleSendAction = (address: string) => { + const account = accounts.find(a => a.address === address); + if (account && selectedAccount !== account) switchAccount(account.id); + openAction("send", { + prefilledData: { output: address }, + onFinish: () => { console.log("Send completed"); }, + }); + }; + + // ── Loading skeleton ────────────────────────────────────────────────────── + if (accountsLoading || dataLoading) { + return ( +
+
+
+ {[0, 1, 2].map(i => ( +
+ ))} +
+
+
+ ); + } + + // ── Change pill helper ──────────────────────────────────────────────────── + const ChangePill = ({ + loading, + pct, + label = "24h", + }: { + loading: boolean; + pct: number; + label?: string; + }) => { + if (loading) return
; + const pos = pct >= 0; + return ( + + {pos ? : } + {pos ? "+" : ""}{pct.toFixed(2)}% + {label} + + ); + }; + + return ( + + {/* ── Page header ── */} +
+
+

+ Accounts +

+

+ {accounts.length} address{accounts.length !== 1 ? "es" : ""} across your keystore +

+
+ + {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="h-9 w-72 rounded-lg border border-border/60 bg-secondary/80 pl-9 pr-3 text-sm font-body text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-primary/40 transition-colors" + /> +
+
+ + {/* ── Summary cards ── */} +
+ + {/* Card 1 — Total Balance */} + +
+
+
+ +
+ + Total Balance + +
+
+ +
+ + + + {symbol} +
+ +
+ +
+ + {fmt(totalLiquid)} + liquid +
+
+
+ + {/* Card 2 — Total Staked */} + +
+
+
+ +
+ + Total Staked + +
+
+ +
+ + + + {symbol} +
+ +
+ +
+ + {stakingRate.toFixed(1)}% + of total +
+
+
+ + {/* Card 3 — Portfolio */} + +
+
+ +
+ + Portfolio + +
+ +
+ + {accounts.length} + + + address{accounts.length !== 1 ? "es" : ""} + +
+ +
+
+ + {stakingCount} + staking +
+
+ + {liquidCount} + liquid only +
+
+
+
+ + {/* ── Address portfolio table ── */} + + {/* Table header */} +
+

+ Address Portfolio +

+
+ + + + + Live +
+
+ +
+ + + + + + + + + + + + + {filteredAddresses.length === 0 ? ( + + + + ) : ( + filteredAddresses.map((addr, index) => ( + + {/* Address */} + + + {/* Total */} + + + {/* Staked */} + + + {/* Liquid */} + + + {/* Status */} + + + {/* Actions */} + + + )) + )} + +
AddressTotalStakedLiquidStatusActions
+ No addresses found +
+
+
+ +
+
+
+ {addr.nickname} +
+
+ {addr.address} +
+
+
+
+ + {fmt(addr.total)} + + {symbol} + +
+ {fmt(addr.staked)} + {symbol} +
+ {addr.stakedPct.toFixed(1)}% +
+
+
+
+ {fmt(addr.liquid)} + {symbol} +
+ {addr.liquidPct.toFixed(1)}% +
+
+
+ + {addr.statusLabel} + + + +
+
+
+
+ ); +}; 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..f652ab2a2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/AllAddresses.tsx @@ -0,0 +1,289 @@ +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-muted/20 text-muted-foreground border border-border/40"; + default: + return "bg-muted/20 text-muted-foreground border border-border/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-background border border-border rounded-lg text-foreground 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..cabba1faa --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/AllTransactions.tsx @@ -0,0 +1,449 @@ +import React, { useState, useMemo, useCallback, useEffect } from "react"; +import { motion } from "framer-motion"; +import { Search, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; +import { useDashboard } from "@/hooks/useDashboard"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { TransactionDetailModal, type TxDetail } from "@/components/transactions/TransactionDetailModal"; + +const ITEMS_PER_PAGE = 25; + +/* ─── helpers ──────────────────────────────────────────────── */ + +const getStatusColor = (s: string) => + s === "Confirmed" + ? "bg-green-500/20 text-green-400" + : s === "Failed" + ? "bg-red-500/20 text-red-400" + : s === "Open" + ? "bg-orange-500/20 text-orange-400" + : s === "Pending" + ? "bg-yellow-500/20 text-yellow-400" + : "bg-muted/20 text-muted-foreground"; + +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 diff = Math.max(0, Date.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) => + new Date(tsMs).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + +/* ─── pagination helpers ─────────────────────────────────────── */ + +function getPageNumbers(current: number, total: number): (number | "…")[] { + if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1); + + if (current <= 4) + return [1, 2, 3, 4, 5, "…", total]; + + if (current >= total - 3) + return [1, "…", total - 4, total - 3, total - 2, total - 1, total]; + + return [1, "…", current - 1, current, current + 1, "…", total]; +} + +/* ─── pagination bar ─────────────────────────────────────────── */ + +interface PaginationBarProps { + current: number; + total: number; + from: number; + to: number; + count: number; + hasMore: boolean; + isFetchingMore: boolean; + onChange: (page: number) => void; + onLoadMore: () => void; +} + +const PaginationBar: React.FC = ({ + current, total, from, to, count, hasMore, isFetchingMore, onChange, onLoadMore, +}) => { + const pages = getPageNumbers(current, total); + + return ( +
+ {/* Left: result range + load more */} +
+ + Showing {from}–{to} of{" "} + {count} loaded + + {hasMore && ( + + )} +
+ + {/* Right: page controls */} + {total > 1 && ( +
+ + + {pages.map((p, idx) => + p === "…" ? ( + + … + + ) : ( + + ) + )} + + +
+ )} +
+ ); +}; + +/* ─── main page ──────────────────────────────────────────────── */ + +export const AllTransactions = () => { + const { + allTxs, + isTxLoading, + hasMoreTxs, + isFetchingMoreTxs, + fetchMoreTxs, + } = useDashboard(); + const { manifest, chain } = useConfig(); + + const [searchTerm, setSearchTerm] = useState(""); + const [filterType, setFilterType] = useState("all"); + const [filterStatus, setFilterStatus] = useState("all"); + const [selectedTx, setSelectedTx] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + 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], + ); + + // Unique tx types for filter dropdown + const txTypes = useMemo(() => { + const types = new Set(allTxs.map((tx) => tx.type)); + return ["all", ...Array.from(types)]; + }, [allTxs]); + + // Filtered set + 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]); + + // Total pages for current filter + const totalPages = Math.max(1, Math.ceil(filteredTransactions.length / ITEMS_PER_PAGE)); + + // Reset to page 1 whenever filters change + useEffect(() => { + setCurrentPage(1); + }, [searchTerm, filterType, filterStatus]); + + // Current page slice + const from = (currentPage - 1) * ITEMS_PER_PAGE; + const paginatedTransactions = filteredTransactions.slice(from, from + ITEMS_PER_PAGE); + const displayFrom = filteredTransactions.length === 0 ? 0 : from + 1; + const displayTo = Math.min(from + ITEMS_PER_PAGE, filteredTransactions.length); + + const openDetail = useCallback((tx: any) => { + setSelectedTx({ + hash: tx.hash, + type: tx.type, + amount: tx.amount, + fee: tx.fee, + status: tx.status, + time: tx.time, + address: tx.address, + error: tx.error, + }); + }, []); + + if (isTxLoading) { + return ( +
+
+ + Loading transactions... +
+
+ ); + } + + return ( + +
+ {/* Header */} +
+

+ All Transactions +

+

+ View and manage your complete transaction history +

+
+ + {/* Filters */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-4 py-2 bg-background border border-border rounded-lg text-foreground + placeholder-text-muted text-sm focus:outline-none focus:border-primary/40 transition-colors" + /> +
+ + {/* Type filter */} + + + {/* Status filter */} + +
+
+ + {/* Stats */} +
+ {[ + { label: "Loaded", value: allTxs.length, color: "text-foreground" }, + { label: "Confirmed", value: allTxs.filter(tx => tx.status === "Confirmed").length, color: "text-green-400" }, + { label: "Failed", value: allTxs.filter(tx => tx.status === "Failed").length, color: "text-red-400" }, + { label: "Filtered", value: filteredTransactions.length, color: "text-primary" }, + ].map(({ label, value, color }) => ( +
+
{label}
+
{value}
+
+ ))} +
+ + {/* Table */} +
+
+ + + + + + + + + + + + + {paginatedTransactions.length > 0 ? ( + paginatedTransactions.map((tx, i) => { + const fundsWay = getFundWay(tx.type); + const isFailed = tx.status === "Failed"; + const prefix = fundsWay === "out" ? "−" : fundsWay === "in" ? "+" : ""; + const amountTxt = `${prefix}${toDisplay(Number(tx.amount || 0)).toFixed(2)} ${symbol}`; + const epochMs = toEpochMs(tx.time); + + return ( + openDetail(tx)} + > + + + + + + + + ); + }) + ) : ( + + + + )} + +
TimeTypeHashAmountStatusDetail
+
{formatTimeAgo(epochMs)}
+
{formatDate(epochMs)}
+
+
+ + {getTxMap(tx.type)} +
+
+ + {tx.hash.slice(0, 8)}…{tx.hash.slice(-6)} + + + + {amountTxt} + + + + {tx.status} + + + +
+ No transactions match the current filters +
+
+ + {/* Pagination bar */} + +
+
+ + {/* Transaction Detail Modal */} + setSelectedTx(null)} + /> +
+ ); +}; + +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..5dae2d2c6 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Dashboard.tsx @@ -0,0 +1,117 @@ +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'; + +const item = { + hidden: { opacity: 0, y: 14 }, + visible: { opacity: 1, y: 0 }, +}; + +export const Dashboard = () => { + const { + manifestLoading, + manifest, + isTxLoading, + allTxs, + onRunAction, + isActionModalOpen, + setIsActionModalOpen, + selectedActions, + prefilledData, + } = useDashboard(); + + if (manifestLoading) { + return ( +
+
+ + + + + Loading dashboard… +
+
+ ); + } + + return ( + + + {/* Page heading */} + +
+

+ Dashboard +

+

+ Wallet overview & node management +

+
+
+ + {/* ── Row 1: Balance + Staked + Quick Actions ── */} + + + + + + + +
+ + + +
+
+ + {/* ── Row 2: Transactions + Addresses ── */} + +
+ + + +
+
+ + + +
+
+ + {/* ── Row 3: Node Management ── */} + + + + + +
+ + +
+ ); +}; + +export default Dashboard; 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..9b115325e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Governance.tsx @@ -0,0 +1,328 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { motion } from "framer-motion"; +import { + BarChart3, + Settings, + Coins, + Info, + CircleHelp, + CheckCircle2, + Vote, + RefreshCcw, +} from "lucide-react"; +import { Poll, Proposal, useGovernanceData } 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"; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.4, + staggerChildren: 0.08, + }, + }, +}; + +const GOVERNANCE_ACTION_IDS = { + startPoll: "govStartPoll", + votePoll: "govVotePoll", + generateParamChange: "govGenerateParamChange", + generateDaoTransfer: "govGenerateDaoTransfer", + submitProposalTx: "govSubmitProposalTx", + addProposalVote: "govAddProposalVote", + deleteProposalVote: "govDeleteProposalVote", +} as const; + +export const Governance = () => { + const { proposals, polls } = useGovernanceData(); + const { manifest } = useManifest(); + + const [isActionModalOpen, setIsActionModalOpen] = useState(false); + const [selectedActions, setSelectedActions] = useState([]); + const [selectedProposal, setSelectedProposal] = useState(null); + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + const openAction = useCallback( + (actionId: string, prefilledData?: Record) => { + const action = manifest?.actions?.find((item: any) => item.id === actionId); + if (!action) return; + setSelectedActions([{ ...action, prefilledData: prefilledData ?? {} }]); + setIsActionModalOpen(true); + }, + [manifest], + ); + + const { allProposals, activeCount } = useMemo(() => { + const ordered = [...proposals].sort((a, b) => { + const rank = (status: Proposal["status"]) => { + if (status === "active") return 0; + if (status === "pending") return 1; + if (status === "passed") return 2; + return 3; + }; + return rank(a.status) - rank(b.status); + }); + const active = proposals.filter((p) => p.status === "active" || p.status === "pending").length; + return { allProposals: ordered, activeCount: active }; + }, [proposals]); + + const handleVoteProposal = useCallback( + (proposalHash: string, vote: "approve" | "reject") => { + openAction(GOVERNANCE_ACTION_IDS.addProposalVote, { + proposalId: proposalHash, + approve: vote === "approve", + }); + }, + [openAction], + ); + + const handleDeleteProposalVote = useCallback( + (proposalHash: string) => { + openAction(GOVERNANCE_ACTION_IDS.deleteProposalVote, { + proposalId: proposalHash, + }); + }, + [openAction], + ); + + const handleVotePoll = useCallback( + (_pollHash: string, vote: "approve" | "reject", poll?: Poll) => { + if (!poll) return; + openAction(GOVERNANCE_ACTION_IDS.votePoll, { + proposalHash: poll.proposalHash || poll.hash, + proposal: poll.proposal, + endBlock: poll.endBlock, + URL: poll.url, + voteApprove: vote === "approve", + }); + }, + [openAction], + ); + + const handleViewDetails = useCallback( + (hash: string) => { + const proposal = proposals.find((p) => p.hash === hash); + if (!proposal) return; + setSelectedProposal(proposal); + setIsDetailsModalOpen(true); + }, + [proposals], + ); + + const criticalActions = useMemo( + () => [ + { + id: GOVERNANCE_ACTION_IDS.startPoll, + title: "Start Poll", + description: "Create a governance poll and open it for community voting.", + help: "Creates a new on-chain poll transaction. Use this when you want token holders and validators to vote on a question.", + icon: BarChart3, + }, + { + id: GOVERNANCE_ACTION_IDS.generateParamChange, + title: "Create Protocol Change", + description: "Create and submit a parameter-change proposal in one flow.", + help: "Submits a governance proposal that changes a protocol parameter (space/key/value) with a voting window.", + icon: Settings, + }, + { + id: GOVERNANCE_ACTION_IDS.generateDaoTransfer, + title: "Create Treasury Subsidy", + description: "Create and submit a treasury transfer proposal in one flow.", + help: "Submits a governance proposal to transfer funds from DAO treasury. It still requires governance approval on-chain.", + icon: Coins, + }, + { + id: GOVERNANCE_ACTION_IDS.votePoll, + title: "Vote on Poll", + description: "Approve or reject an on-chain poll with auto-filled fields.", + help: "Casts your on-chain poll vote. Select a poll and submit Approve or Reject with fields prefilled from live data.", + icon: Vote, + }, + ], + [], + ); + + return ( + + +
+
+
+

Governance

+

+ Manage polls and proposals with guided, one-step submissions and explicit review details. +

+
+
+ +
+
+
Primary Governance Actions
+ +
+
+ {criticalActions.map((item) => { + const Icon = item.icon; + return ( + openAction(item.id)} + className="group text-left rounded-xl border border-primary/25 bg-card/85 hover:bg-card px-4 py-4 transition-all duration-200" + > +
+ + + + {item.title} + + + + + + {item.help} + + +
+

+ {item.description} +

+
+ Open Action +
+
+ ); + })} +
+
+ +
+
+
+ + Create and Submit +
+

+ Protocol and treasury proposals are submitted directly after confirmation. No manual JSON paste is required. +

+
+
+
+ + Review and Vote +
+

+ Proposal and poll vote forms are prefilled from selected records to reduce mistakes. +

+
+
+
+ + Advanced Broadcast +
+

+ Need full control? Use manual raw transaction broadcast from the advanced action. +

+ +
+
+ +
+
+
+

Active Proposals

+

+ All proposals are listed here. Vote actions are enabled only for active/pending items. +

+

+ Active now: {activeCount} | Total loaded: {allProposals.length} +

+
+ + + Tip: open View Details to review the technical msg before voting. + +
+
+ + + +
+ +
+
+

Active Polls

+

+ When you click Approve/Reject, the voting form opens with poll fields prefilled. +

+
+ +
+ {polls.length === 0 ? ( +
+ +

No active polls

+
+ ) : ( + polls.map((poll) => ( + + handleVotePoll(hash, vote, poll)} + onViewDetails={undefined} + /> + + )) + )} +
+
+
+ +
+ + setIsActionModalOpen(false)} + /> + + 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..d2ac617eb --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/KeyManagement.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Download, ShieldCheck, KeyRound, WalletCards } 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 walletCount = Object.keys(keystore?.addressMap || {}).length; + + 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 ( +
+
+ +
+
+
+ + Security Control Center +
+

Key Management

+

+ Create, import, protect, and maintain wallet keys with explicit security safeguards. +

+
+
+
+
+ + Wallets +
+
{walletCount}
+
+ +
+
+
+ + Always keep encrypted backups offline before deleting or rotating keys. +
+
+ + + + + + + +
+
+ ); +}; + 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..160995733 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Monitoring.tsx @@ -0,0 +1,191 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { useAvailableNodes, useNodeData, useNodeLogs, useChainCommitteeId } 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" | "config" | "peerInfo" | "peerBook" + >("quorum"); + const [isPaused, setIsPaused] = useState(false); + + // Get the chain's committeeId first + const { committeeId, isLoading: committeeLoading, error: committeeError } = useChainCommitteeId(); + + // Get current node (single node only) + const { data: availableNodes = [], isLoading: nodesLoading, error: nodesError } = + useAvailableNodes(); + const currentNode = availableNodes[0]; // Always use the first (and only) node + + // Get data for current node + const { data: nodeData, isLoading: nodeDataLoading, error: nodeDataError } = useNodeData( + currentNode?.id || "", + ); + + // Logs are fetched independently so a large log payload never delays metrics + const { data: rawLogs } = useNodeLogs(currentNode?.id || "", isPaused); + + // 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?.peers?.id?.netAddress || "", + publicKey: nodeData?.consensus?.publicKey || "", + peers: nodeData?.peers?.peers || [], + }; + + const logs = + typeof rawLogs === "string" + ? rawLogs.split("\n").filter(Boolean).reverse() + : []; + + 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 - include committeeLoading since data depends on it + if (committeeLoading || nodesLoading || nodeDataLoading) { + return ; + } + + // Error state - show error message instead of empty page + const error = committeeError || nodesError || nodeDataError; + if (error) { + return ( +
+
+

Monitoring Error

+

+ {error instanceof Error ? error.message : "Failed to load monitoring data. Please try again."} +

+
+
+ ); + } + + return ( + +
+ + + {/* Two column layout for main content */} +
+ {/* Left column */} +
+ + +
+ + {/* Right column */} +
+ + + +
+
+
+
+ ); +} diff --git a/cmd/rpc/web/wallet-new/src/app/pages/Orders.tsx b/cmd/rpc/web/wallet-new/src/app/pages/Orders.tsx new file mode 100644 index 000000000..c99adaf5f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/pages/Orders.tsx @@ -0,0 +1,695 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { + AlertTriangle, + ArrowLeftRight, + CheckCircle2, + CircleDashed, + Droplets, + Info, + Loader2, + Lock, + Pencil, + PlusCircle, + RefreshCcw, + ShoppingCart, + Trash2, +} from "lucide-react"; +import { Button } from "@/components/ui/Button"; +import { StatusBadge } from "@/components/ui/StatusBadge"; +import { useActionModal } from "@/app/providers/ActionModalProvider"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { isOrderLocked, RpcOrder, useOrdersData } from "@/hooks/useOrdersData"; +import { cx } from "@/ui/cx"; + +const POLL_INTERVAL_MS = 6000; +const PER_PAGE = 20; + +const ACTION_IDS = { + createOrder: "orderCreate", + repriceOrder: "orderReprice", + voidOrder: "orderVoid", + lockOrder: "orderLock", + closeOrder: "orderClose", + dexLimitOrder: "dexLimitOrder", + dexLiquidityDeposit: "dexLiquidityDeposit", + dexLiquidityWithdraw: "dexLiquidityWithdraw", +} as const; + +const shortHex = (value: string, head = 6, tail = 4) => { + const v = String(value ?? ""); + if (!v) return "-"; + if (v.length <= head + tail + 2) return v; + return `${v.slice(0, head)}...${v.slice(-tail)}`; +}; + +const asNumber = (value: unknown): number => { + const n = Number(value ?? 0); + return Number.isFinite(n) ? n : 0; +}; + +const microToDisplay = (value: unknown, decimals: number): number => + asNumber(value) / Math.pow(10, decimals); + +const formatAmount = (value: unknown, decimals: number, symbol?: string): string => { + const normalized = microToDisplay(value, decimals); + const amount = normalized.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + return symbol ? `${amount} ${symbol}` : amount; +}; + +const getErrorMessage = (error: unknown): string => { + if (error instanceof Error) return error.message; + return "Could not load data from RPC."; +}; + +type SectionCardProps = { + title: string; + subtitle?: string; + actions?: React.ReactNode; + children: React.ReactNode; +}; + +const SectionCard: React.FC = ({ title, subtitle, actions, children }) => ( +
+
+
+

{title}

+ {subtitle ?

{subtitle}

: null} +
+ {actions} +
+ {children} +
+); + +const TableShell: React.FC<{ + isLoading: boolean; + error?: unknown; + isEmpty: boolean; + emptyText: string; + children: React.ReactNode; +}> = ({ isLoading, error, isEmpty, emptyText, children }) => { + if (isLoading) { + return ( +
+ + Loading orders... +
+ ); + } + + if (error) { + return ( +
+ {getErrorMessage(error)} +
+ ); + } + + if (isEmpty) { + return
{emptyText}
; + } + + return <>{children}; +}; + +type InfoMessageProps = { + title: string; + description: string; + tone?: "info" | "warning" | "success"; + meta?: React.ReactNode; +}; + +const InfoMessage: React.FC = ({ + title, + description, + tone = "info", + meta, +}) => { + const toneClass = + tone === "warning" + ? "border-amber-500/30 bg-amber-500/[0.06]" + : tone === "success" + ? "border-emerald-500/30 bg-emerald-500/[0.06]" + : "border-blue-500/30 bg-blue-500/[0.06]"; + + return ( +
+
+ {tone === "warning" ? ( + + ) : ( + + )} +
+

{title}

+

{description}

+ {meta ?
{meta}
: null} +
+
+
+ ); +}; + +type OrderTypeMetricProps = { + title: string; + hint: string; + count: number; + status: React.ComponentProps["status"]; + icon: React.ReactNode; +}; + +const OrderTypeMetric: React.FC = ({ + title, + hint, + count, + status, + icon, +}) => ( +
+
+
+ {icon} +

{title}

+
+ +
+

{hint}

+
+); + +export default function Orders(): JSX.Element { + const { chain } = useConfig(); + const { openAction } = useActionModal(); + + const { + selectedAddress, + committeeId, + myOrders, + availableOrders, + fulfillOrders, + queries, + refetchAll, + } = useOrdersData({ + perPage: PER_PAGE, + pollIntervalMs: POLL_INTERVAL_MS, + }); + + const symbol = String(chain?.denom?.symbol ?? "CNPY"); + const decimals = asNumber(chain?.denom?.decimals) || 6; + + const runAction = React.useCallback( + (actionId: string, prefilledData?: Record) => { + openAction(actionId, { + prefilledData, + onFinish: () => { + void refetchAll(); + }, + }); + }, + [openAction, refetchAll], + ); + + const baseOrderPrefill = React.useCallback( + (order: RpcOrder) => ({ + address: selectedAddress || order.sellersSendAddress || "", + receiveAddress: order.sellerReceiveAddress || "", + committees: String(order.committee ?? committeeId ?? ""), + orderId: order.id, + amount: microToDisplay(order.amountForSale, decimals), + receiveAmount: microToDisplay(order.requestedAmount, decimals), + data: order.data || "", + fee: 0, + memo: "", + }), + [selectedAddress, committeeId, decimals], + ); + + const dexPrefill = React.useMemo( + () => ({ + address: selectedAddress || "", + committees: String(committeeId ?? ""), + fee: 0, + memo: "", + }), + [selectedAddress, committeeId], + ); + + const myLockedCount = React.useMemo( + () => myOrders.filter((order) => isOrderLocked(order)).length, + [myOrders], + ); + + const myOpenCount = myOrders.length - myLockedCount; + + const nextFulfillDeadline = React.useMemo(() => { + let next: number | undefined; + for (const order of fulfillOrders) { + const deadline = asNumber(order.buyerChainDeadline || 0); + if (!deadline) continue; + if (next == null || deadline < next) next = deadline; + } + return next; + }, [fulfillOrders]); + + return ( + +
+
+
+
+
+ +

Orders

+
+

+ Manage sell orders, lock available offers, close pending purchases, and execute DEX + operations from one module. +

+
+ +
+ +
+ + + +
+ +
+ } + /> + } + /> + } + /> + } + /> +
+
+ +
+
+
+
+
+

Cross-Chain Orderbook

+

+ Seller and buyer lifecycle on committee orders: create, lock, reprice, void, and close. +

+
+
+ + +
+
+
+ + + runAction(ACTION_IDS.createOrder, { + address: selectedAddress || "", + receiveAddress: selectedAddress || "", + committees: String(committeeId ?? ""), + fee: 0, + }) + } + disabled={!selectedAddress} + > + + Create Order + + } + > + + + + + } + /> + +
+ + + + + + + + + + + + + + {myOrders.map((order) => { + const locked = isOrderLocked(order); + return ( + + + + + + + + + + ); + })} + +
Order IDTypeCommitteeAmount For SaleRequestedStatusActions
+ {shortHex(order.id, 8, 6)} + + + {order.committee} + {formatAmount(order.amountForSale, decimals, symbol)} + + {formatAmount(order.requestedAmount, decimals)} + + + + {!locked ? ( +
+ + +
+ ) : ( + Waiting buyer close + )} +
+
+
+
+ + + } + /> + +
+ + + + + + + + + + + + + + {availableOrders.map((order) => ( + + + + + + + + + + ))} + +
Order IDTypeSellerFor SaleRequestedReceive AddressAction
+ {shortHex(order.id, 8, 6)} + + + + {shortHex(order.sellersSendAddress, 8, 6)} + + {formatAmount(order.amountForSale, decimals, symbol)} + + {formatAmount(order.requestedAmount, decimals)} + + {shortHex(order.sellerReceiveAddress, 8, 6)} + + +
+
+
+
+ + + + + {nextFulfillDeadline ? ( + + ) : null} + + } + /> + +
+ + + + + + + + + + + + + + + {fulfillOrders.map((order) => ( + + + + + + + + + + + ))} + +
Order IDTypeSellerAmount To ReceiveAmount To SendDeadlineSend ToAction
+ {shortHex(order.id, 8, 6)} + + + + {shortHex(order.sellersSendAddress, 8, 6)} + + {formatAmount(order.amountForSale, decimals, symbol)} + + {formatAmount(order.requestedAmount, decimals)} + + {asNumber(order.buyerChainDeadline || 0).toLocaleString()} + + {shortHex(order.sellerReceiveAddress, 8, 6)} + + +
+
+
+
+ +
+ + +
+
+
+ ); +} 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..5092b89ec --- /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 { useMultipleValidatorRewardsHistory } from "@/hooks/useMultipleValidatorRewardsHistory"; +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: rewardsHistory = {} } = + useMultipleValidatorRewardsHistory(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: rewardsHistory[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, rewardsHistory]); + + 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 to add stake - opens the "stake" action from 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/AccountsListProvider.tsx b/cmd/rpc/web/wallet-new/src/app/providers/AccountsListProvider.tsx new file mode 100644 index 000000000..0b625c4fa --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/AccountsListProvider.tsx @@ -0,0 +1,125 @@ +'use client' + +import React, { createContext, useCallback, useContext, useMemo } from 'react' +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 AccountsListContextValue = { + accounts: Account[] + loading: boolean + error: string | null + isReady: boolean + refetch: () => Promise + createNewAccount: (nickname: string, password: string) => Promise + deleteAccount: (accountId: string, onDeleted?: (nextAccountId: string | null) => void) => Promise +} + +const AccountsListContext = createContext(undefined) + +export function AccountsListProvider({ children }: { children: React.ReactNode }) { + const { data: ks, isLoading, isFetching, error, refetch, isFetched } = + 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 stableError = useMemo( + () => (error ? ((error as any).message ?? 'Error') : null), + [error] + ) + + // Only show loading on initial load, not during background refetch + const loading = isLoading && !isFetched + const isReady = isFetched || !!ks + + const createNewAccount = useCallback(async (nickname: string, password: string): Promise => { + try { + const response = await dsFetch('keystoreNewKey', { + nickname, + password + }) + await refetch() + 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, + onDeleted?: (nextAccountId: string | null) => void + ): Promise => { + try { + const account = accounts.find(acc => acc.id === accountId) + if (!account) { + throw new Error('Account not found') + } + + await dsFetch('keystoreDelete', { + nickname: account.nickname + }) + + // Notify caller about which account to switch to + if (onDeleted) { + const nextAccount = accounts.find(acc => acc.id !== accountId) + onDeleted(nextAccount?.id ?? null) + } + + await refetch() + } catch (err) { + console.error('Error deleting account:', err) + throw err + } + }, [accounts, dsFetch, refetch]) + + const value: AccountsListContextValue = useMemo(() => ({ + accounts, + loading, + error: stableError, + isReady, + refetch, + createNewAccount, + deleteAccount, + }), [accounts, loading, stableError, isReady, refetch, createNewAccount, deleteAccount]) + + return ( + + {children} + + ) +} + +export function useAccountsList() { + const ctx = useContext(AccountsListContext) + if (!ctx) throw new Error('useAccountsList must be used within ') + return ctx +} 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..935d0e32d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/AccountsProvider.tsx @@ -0,0 +1,79 @@ +'use client' + +import React, { useCallback, useMemo } from 'react' +import { AccountsListProvider, useAccountsList, Account } from './AccountsListProvider' +import { SelectedAccountProvider, useSelectedAccount } from './SelectedAccountProvider' + +// Re-export Account type for backward compatibility +export type { Account } + +// Legacy combined context type for backward compatibility +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 +} + +/** + * Composed provider that wraps AccountsListProvider and SelectedAccountProvider. + * This maintains backward compatibility while allowing components to use + * more granular hooks (useAccountsList, useSelectedAccount) for better performance. + */ +export function AccountsProvider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} + +/** + * Legacy hook that combines both contexts. + * Use this for backward compatibility, but prefer useAccountsList() or useSelectedAccount() + * for components that only need part of the data. + */ +export function useAccounts(): AccountsContextValue { + const list = useAccountsList() + const selected = useSelectedAccount() + + // Wrap deleteAccount to integrate with switchAccount + const deleteAccount = useCallback(async (accountId: string): Promise => { + await list.deleteAccount(accountId, (nextAccountId) => { + if (selected.selectedId === accountId && nextAccountId) { + selected.switchAccount(nextAccountId) + } + }) + }, [list, selected]) + + return useMemo(() => ({ + // From AccountsListProvider + accounts: list.accounts, + loading: list.loading, + error: list.error, + isReady: list.isReady, + refetch: list.refetch, + createNewAccount: list.createNewAccount, + deleteAccount, + + // From SelectedAccountProvider + selectedId: selected.selectedId, + selectedAccount: selected.selectedAccount, + selectedAddress: selected.selectedAddress, + switchAccount: selected.switchAccount, + }), [list, selected, deleteAccount]) +} + +// Re-export granular hooks for direct use +export { useAccountsList } from './AccountsListProvider' +export { useSelectedAccount } from './SelectedAccountProvider' 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..0c813779a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/ActionModalProvider.tsx @@ -0,0 +1,219 @@ +import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, Suspense } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { createPortal } from 'react-dom'; +import { useManifest } from '@/hooks/useManifest'; +import { XIcon, Loader2 } from 'lucide-react'; +import { cx } from '@/ui/cx'; +import { ModalTabs, Tab } from '@/actions/ModalTabs'; +import { LucideIcon } from '@/components/ui/LucideIcon'; + +const ActionRunner = React.lazy(() => import('@/actions/ActionRunner')); + +const ActionRunnerFallback = () => ( +
+ + Loading action... +
+); + +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[]; +} + +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(); + } + setTimeout(() => { + setCurrentActionId(null); + setSelectedTab(undefined); + setOptions({}); + }, 300); + }, [options]); + + const handleFinish = useCallback(() => { + if (options.onFinish) { + options.onFinish(); + } + closeAction(); + }, [options, closeAction]); + + 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, + }, + ]; + + 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]); + + useEffect(() => { + if (availableTabs.length > 0 && !selectedTab) { + setSelectedTab(availableTabs[0]); + } + }, [availableTabs, selectedTab]); + + const activeActionId = selectedTab?.value || currentActionId; + + 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; + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = 'auto'; + }; + } + }, [isOpen]); + + const modalNode = ( + + {isOpen && currentActionId && ( + + e.stopPropagation()} + > + + + {availableTabs.length > 1 ? ( +
+ +
+ ) : ( + availableTabs.length === 1 && ( +
+
+ {availableTabs[0].icon && ( +
+ +
+ )} +

+ {availableTabs[0].label} +

+
+
+ ) + )} + + {selectedTab && ( + + }> + + + + )} +
+
+ )} +
+ ); + + return ( + + {children} + {typeof document !== 'undefined' ? createPortal(modalNode, document.body) : null} + + ); +}; 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/providers/SelectedAccountProvider.tsx b/cmd/rpc/web/wallet-new/src/app/providers/SelectedAccountProvider.tsx new file mode 100644 index 000000000..5eafb3821 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/providers/SelectedAccountProvider.tsx @@ -0,0 +1,83 @@ +'use client' + +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useAccountsList, Account } from './AccountsListProvider' + +type SelectedAccountContextValue = { + selectedId: string | null + selectedAccount: Account | null + selectedAddress?: string + switchAccount: (id: string | null) => void +} + +const SelectedAccountContext = createContext(undefined) + +const STORAGE_KEY = 'activeAccountId' + +export function SelectedAccountProvider({ children }: { children: React.ReactNode }) { + const { accounts, isReady: accountsReady } = useAccountsList() + + const [selectedId, setSelectedId] = useState(null) + const [isInitialized, setIsInitialized] = useState(false) + + // Load from localStorage on mount + useEffect(() => { + try { + const saved = typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null + if (saved) setSelectedId(saved) + } finally { + setIsInitialized(true) + } + + // Listen for changes from other tabs + const onStorage = (e: StorageEvent) => { + if (e.key === STORAGE_KEY) setSelectedId(e.newValue ?? null) + } + window.addEventListener('storage', onStorage) + return () => window.removeEventListener('storage', onStorage) + }, []) + + // Auto-select first account if none selected + useEffect(() => { + if (!isInitialized || !accountsReady) return + if (!selectedId && accounts.length > 0) { + const first = accounts[0].id + setSelectedId(first) + if (typeof window !== 'undefined') localStorage.setItem(STORAGE_KEY, first) + } + }, [isInitialized, accountsReady, selectedId, accounts]) + + const selectedAccount = useMemo( + () => accounts.find(a => a.id === selectedId) ?? null, + [accounts, selectedId] + ) + + const selectedAddress = useMemo(() => selectedAccount?.address, [selectedAccount]) + + 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 value: SelectedAccountContextValue = useMemo(() => ({ + selectedId, + selectedAccount, + selectedAddress, + switchAccount, + }), [selectedId, selectedAccount, selectedAddress, switchAccount]) + + return ( + + {children} + + ) +} + +export function useSelectedAccount() { + const ctx = useContext(SelectedAccountContext) + if (!ctx) throw new Error('useSelectedAccount must be used within ') + return ctx +} 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..617a80fb2 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/app/routes.tsx @@ -0,0 +1,39 @@ + +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' +import Orders from '@/app/pages/Orders' + +// Placeholder components for the new routes +const Portfolio = () =>
Portfolio - Coming Soon
+ +const router = createBrowserRouter([ + { + element: , + children: [ + { path: '/', element: }, + { path: '/accounts', element: }, + { path: '/portfolio', element: }, + { path: '/staking', element: }, + { path: '/governance', element: }, + { path: '/orders', 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..5dfc41959 --- /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..19ac91e65 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/UnlockModal.tsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect, useRef } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { useSession } from '../state/session' +import { Shield, Eye, EyeOff, X, Unlock, Clock, AlertCircle } from 'lucide-react' + +interface UnlockModalProps { + address: string + ttlSec: number + open: boolean + onClose: () => void +} + +export default function UnlockModal({ address, ttlSec, open, onClose }: UnlockModalProps) { + const [pwd, setPwd] = useState('') + const [err, setErr] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const inputRef = useRef(null) + const unlock = useSession(s => s.unlock) + + // Focus input when modal opens + useEffect(() => { + if (open && inputRef.current) { + setTimeout(() => inputRef.current?.focus(), 100) + } + // Reset state when modal opens + if (open) { + setPwd('') + setErr('') + setShowPassword(false) + setIsSubmitting(false) + } + }, [open]) + + // Handle Enter key + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && pwd) { + submit() + } else if (e.key === 'Escape') { + onClose() + } + } + + const submit = async () => { + if (!pwd) { + setErr('Password is required') + inputRef.current?.focus() + return + } + + setIsSubmitting(true) + setErr('') + + // Simulate brief delay for UX + await new Promise(resolve => setTimeout(resolve, 200)) + + unlock(address, pwd, ttlSec) + onClose() + } + + const minutes = Math.round(ttlSec / 60) + + return ( + + {open && ( + + {/* Backdrop */} + + + {/* Modal */} + + {/* Header accent */} +
+ + {/* Close button */} + + +
+ {/* Icon */} +
+
+
+
+ +
+
+
+ + {/* Title */} +

+ Unlock Wallet +

+ + {/* Description */} +

+ Enter your password to authorize transactions +

+ + {/* Session info badge */} +
+
+ + + Session valid for {minutes} minutes + +
+
+ + {/* Password input */} +
+ +
+ { + setPwd(e.target.value) + if (err) setErr('') + }} + onKeyDown={handleKeyDown} + placeholder="Enter your wallet password" + className={` + w-full bg-background/50 text-foreground rounded-xl px-4 py-3 pr-12 + border transition-all duration-200 outline-none + placeholder:text-muted-foreground + ${err + ? 'border-red-500/50 focus:border-red-500 focus:ring-2 focus:ring-red-500/20' + : 'border-border/50 focus:border-primary/50 focus:ring-2 focus:ring-primary/20' + } + `} + disabled={isSubmitting} + /> + +
+ + {/* Error message */} + + {err && ( + + + {err} + + )} + +
+ + {/* Actions */} +
+ + +
+
+ + {/* Footer hint */} +
+

+ Your session will automatically extend while you're active +

+
+ + + )} + + ) +} + 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..64a4235d0 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/accounts/AddressRow.tsx @@ -0,0 +1,110 @@ +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-muted/20 text-muted-foreground'; + } + }; + + 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..cda9434d9 --- /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..47d379349 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/AllAddressesCard.tsx @@ -0,0 +1,125 @@ +import React, { useMemo, useCallback } from 'react'; +import { motion } from 'framer-motion'; +import { ChevronRight, WalletCards} from 'lucide-react'; +import { useAccountData } from '@/hooks/useAccountData'; +import { useAccountsList } from '@/app/providers/AccountsProvider'; +import { NavLink } from 'react-router-dom'; +import { StatusBadge } from '@/components/ui/StatusBadge'; +import { LoadingState } from '@/components/ui/LoadingState'; +import { EmptyState } from '@/components/ui/EmptyState'; + +const shortAddr = (address: string) => `${address.slice(0, 6)}…${address.slice(-4)}`; + +interface AddressData { + id: string; + address: string; + nickname: string; + totalValue: string; + status: string; +} + +const AddressRow = React.memo<{ address: AddressData; index: number }>(({ address, index }) => ( + +
+ + {address.nickname.charAt(0).toUpperCase()} + +
+ +
+
{address.nickname}
+
{address.address}
+
+ +
+ {Number(address.totalValue).toLocaleString()} + +
+
+)); + +AddressRow.displayName = 'AddressRow'; + +export const AllAddressesCard = React.memo(() => { + const { accounts, loading: accountsLoading } = useAccountsList(); + const { balances, stakingData, loading: dataLoading } = useAccountData(); + + const formatBalance = useCallback((amount: number) => (amount / 1_000_000).toFixed(2), []); + + const getStatus = useCallback((address: string) => { + const info = stakingData.find(d => d.address === address); + return info && info.staked > 0 ? 'Staked' : 'Liquid'; + }, [stakingData]); + + const processedAddresses = useMemo((): AddressData[] => + accounts.map(account => { + const balance = balances.find(b => b.address === account.address)?.amount || 0; + return { + id: account.address, + address: shortAddr(account.address), + nickname: account.nickname || 'Unnamed', + totalValue: formatBalance(balance), + status: getStatus(account.address), + }; + }), + [accounts, balances, formatBalance, getStatus] + ); + + if (accountsLoading || dataLoading) { + return ( + + + + ); + } + + return ( + + {/* Header */} +
+
+
+ +
+ + Addresses + +
+ + All ({processedAddresses.length}) + + +
+ +
+ {processedAddresses.length > 0 ? ( + processedAddresses.slice(0, 4).map((address, index) => ( + + )) + ) : ( + + )} +
+
+ ); +}); + +AllAddressesCard.displayName = 'AllAddressesCard'; 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..61c8daf2d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/NodeManagementCard.tsx @@ -0,0 +1,285 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { Play, Pause, Workflow } 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'; +import { StatusBadge } from '@/components/ui/StatusBadge'; +import { LoadingState } from '@/components/ui/LoadingState'; +import { EmptyState } from '@/components/ui/EmptyState'; +import { useDS } from '@/core/useDs'; + +const shortAddr = (address: string) => `${address.substring(0, 8)}…${address.substring(address.length - 4)}`; + +const NODE_ACCENT_COLORS = [ + 'from-primary/60 to-primary/30', + 'from-orange-500/60 to-orange-500/30', + 'from-blue-500/60 to-blue-500/30', + 'from-rose-500/60 to-rose-500/30', +]; + +interface ProcessedNode { + address: string; + stakeAmount: string; + status: string; + rewardsDelta24h: string; + rewardsDelta24hValue: number; + originalValidator: any; +} + +const rewardDeltaClass = (value: number) => { + if (value > 0) return 'text-primary'; + if (value < 0) return 'text-red-400'; + return 'text-muted-foreground'; +}; + +const ValidatorRow = React.memo<{ + node: ProcessedNode; + index: number; + onPauseUnpause: (validator: any, action: 'pause' | 'unpause') => void; +}>(({ node, index, onPauseUnpause }) => ( + + +
+
+
+
+ {node.originalValidator.nickname || `Node ${index + 1}`} +
+
+ {shortAddr(node.originalValidator.address)} +
+
+
+ + + {node.stakeAmount} + + + + + + {node.rewardsDelta24h} + + + + + +)); + +ValidatorRow.displayName = 'ValidatorRow'; + +const ValidatorMobileCard = React.memo<{ + node: ProcessedNode; + index: number; + onPauseUnpause: (validator: any, action: 'pause' | 'unpause') => void; +}>(({ node, index, onPauseUnpause }) => ( + +
+
+
+
+
+ {node.originalValidator.nickname || `Node ${index + 1}`} +
+
{shortAddr(node.originalValidator.address)}
+
+
+ +
+
+
+
Stake
+
{node.stakeAmount}
+
+
+
Status
+ +
+
+
Rewards
+
{node.rewardsDelta24h}
+
+
+ +)); + +ValidatorMobileCard.displayName = 'ValidatorMobileCard'; + +export const NodeManagementCard = React.memo((): JSX.Element => { + const { data: keystore, isLoading: keystoreLoading } = useDS('keystore', {}); + const { data: validators = [], isLoading: validatorsLoading, error } = useValidators(); + const { manifest } = useManifest(); + + const validatorAddresses = useMemo(() => validators.map(v => v.address), [validators]); + const { data: rewardsData = {} } = useMultipleValidatorRewardsHistory(validatorAddresses); + + 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 isLoading = keystoreLoading || validatorsLoading; + + const formatStakeAmount = useCallback((amount: number) => + (amount / 1000000).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','), []); + + const formatRewardsDelta = useCallback((rewards: number) => { + const value = rewards / 1000000; + const sign = value > 0 ? '+' : value < 0 ? '-' : ''; + return `${sign}${Math.abs(value).toFixed(2)} CNPY`; + }, []); + + const getStatus = useCallback((validator: any) => { + if (!validator) return 'Liquid'; + if (validator.unstaking) return 'Unstaking'; + if (validator.paused) return 'Paused'; + return 'Staked'; + }, []); + + 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 processedKeystores = useMemo((): ProcessedNode[] => { + if (!keystore?.addressMap) return []; + const addressMap = keystore.addressMap as Record; + const validatorMap = new Map(validators.map(v => [v.address, v])); + return Object.entries(addressMap) + .slice(0, 8) + .map(([address, keyData]) => { + const validator = validatorMap.get(address); + return { + address: shortAddr(address), + stakeAmount: validator ? formatStakeAmount(validator.stakedAmount) : '0.00', + status: getStatus(validator), + rewardsDelta24h: validator ? formatRewardsDelta(rewardsData[address]?.change24h || 0) : '0.00 CNPY', + rewardsDelta24hValue: validator ? Number(rewardsData[address]?.change24h || 0) : 0, + originalValidator: validator || { address, nickname: keyData.keyNickname || 'Unnamed Key', stakedAmount: 0 }, + }; + }) + .sort((a, b) => { + if (a.status === 'Staked' && b.status !== 'Staked') return -1; + if (a.status !== 'Staked' && b.status === 'Staked') return 1; + return 0; + }); + }, [keystore, validators, formatStakeAmount, getStatus, formatRewardsDelta, rewardsData]); + + const cardBase = 'canopy-card p-5'; + const cardMotion = { initial: { opacity: 0, y: 12 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.35, delay: 0.28 } }; + + if (isLoading) return ( + + + + ); + if (error) return ( + + + + ); + + return ( + <> + + {/* Header */} +
+
+ +
+ + Node Management + + {processedKeystores.length > 0 && ( + + {processedKeystores.length} + + )} +
+ + {/* Desktop table */} +
+ {processedKeystores.length > 0 ? ( + + + + {['Key', 'Staked', 'Status', 'Rewards Δ24h', 'Action'].map(h => ( + + ))} + + + + {processedKeystores.map((node, index) => ( + + ))} + +
+ {h} +
+ ) : ( + + )} +
+ + {/* Mobile cards */} +
+ {processedKeystores.length > 0 ? ( + processedKeystores.map((node, index) => ( + + )) + ) : ( + + )} +
+
+ + setIsActionModalOpen(false)} + /> + + ); +}); + +NodeManagementCard.displayName = 'NodeManagementCard'; 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..212eb0414 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/QuickActionsCard.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Zap } from 'lucide-react'; +import { LucideIcon } from '@/components/ui/LucideIcon'; +import { EmptyState } from '@/components/ui/EmptyState'; +import { selectQuickActions } from '@/core/actionForm'; +import { Action } from '@/manifest/types'; +import { useAccountData } from '@/hooks/useAccountData'; +import { useValidators } from '@/hooks/useValidators'; +import { useSelectedAccount } from '@/app/providers/AccountsProvider'; + +export const QuickActionsCard = React.memo(function QuickActionsCard({ actions, onRunAction, maxNumberOfItems }: { + actions?: Action[]; + onRunAction?: (a: Action, prefilledData?: Record) => void; + maxNumberOfItems?: number; +}) { + const { selectedAccount } = useSelectedAccount(); + const { stakingData } = useAccountData(); + const { data: validators = [] } = useValidators(); + + const selectedAccountStake = React.useMemo(() => { + if (!selectedAccount?.address) return null; + const stakeInfo = stakingData.find(s => s.address === selectedAccount.address); + return stakeInfo && stakeInfo.staked > 0 ? stakeInfo : null; + }, [selectedAccount?.address, stakingData]); + + const selectedValidator = React.useMemo(() => { + if (!selectedAccount?.address) return null; + return (validators as any[]).find(v => v.address === selectedAccount.address) || null; + }, [validators, selectedAccount?.address]); + + const hasStake = !!selectedAccountStake; + + const modifiedActions = React.useMemo(() => { + const quickActions = selectQuickActions(actions, maxNumberOfItems); + return quickActions.map(action => { + if (action.id === 'stake' && hasStake) { + return { ...action, title: 'Edit Stake', icon: 'Lock', __isEditStake: true }; + } + return action; + }); + }, [actions, maxNumberOfItems, hasStake]); + + const handleRunAction = React.useCallback((action: Action & { __isEditStake?: boolean }) => { + if (action.__isEditStake && selectedAccount?.address) { + onRunAction?.(action, { + operator: selectedAccount.address, + selectCommittees: (selectedValidator as any)?.committees || [], + }); + } else { + onRunAction?.(action); + } + }, [onRunAction, selectedAccount?.address, selectedValidator]); + + const cols = React.useMemo( + () => Math.min(Math.max(modifiedActions.length || 1, 1), 2), + [modifiedActions.length] + ); + + return ( + + {/* Header */} +
+
+ +
+ + Quick Actions + +
+ +
+ {modifiedActions.map(a => ( + handleRunAction(a)} + className="group flex flex-col items-center justify-center gap-2 rounded-lg border border-border/60 p-3.5 min-h-[72px] transition-all duration-150 hover:border-primary/35 hover:bg-primary/5 btn-glow" + whileHover={{ scale: 1.015 }} + whileTap={{ scale: 0.975 }} + aria-label={a.title ?? a.id} + > +
+ +
+ + {a.title ?? a.id} + +
+ ))} + {modifiedActions.length === 0 && ( + + )} +
+
+ ); +}); + +QuickActionsCard.displayName = 'QuickActionsCard'; 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..0aa79a1eb --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/RecentTransactionsCard.tsx @@ -0,0 +1,222 @@ +import React, { useCallback, useState } from 'react'; +import { motion } from 'framer-motion'; +import { ChevronRight, Receipt } from 'lucide-react'; +import { useConfig } from '@/app/providers/ConfigProvider'; +import { LucideIcon } from '@/components/ui/LucideIcon'; +import { NavLink } from 'react-router-dom'; +import { StatusBadge } from '@/components/ui/StatusBadge'; +import { LoadingState } from '@/components/ui/LoadingState'; +import { EmptyState } from '@/components/ui/EmptyState'; +import { TransactionDetailModal, type TxDetail } from '@/components/transactions/TransactionDetailModal'; + +export interface TxError { + code: number; + module: string; + msg: string; +} + +export interface Transaction { + hash: string; + time: number; + type: string; + amount: number; + fee?: number; + status: string; + address?: string; + error?: TxError; +} + +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); + 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), + h = Math.floor(diff / 3600000), + d = Math.floor(diff / 86400000); + if (m < 60) return `${m}m ago`; + if (h < 24) return `${h}h ago`; + return `${d}d ago`; +}; + +interface TransactionRowProps { + tx: Transaction; + index: number; + getIcon: (type: string) => string; + getTxMap: (type: string) => string; + getFundWay: (type: string) => string; + toDisplay: (amount: number) => number; + symbol: string; + onViewDetail: (tx: Transaction) => void; +} + +const TransactionRow = React.memo(({ + tx, index, getIcon, getTxMap, getFundWay, toDisplay, symbol, onViewDetail, +}) => { + const fundsWay = getFundWay(tx?.type); + const isFailed = tx.status === 'Failed'; + const prefix = fundsWay === 'out' ? '-' : fundsWay === 'in' ? '+' : ''; + const amountTxt = `${prefix}${toDisplay(Number(tx.amount || 0)).toFixed(2)} ${symbol}`; + const timeAgo = formatTimeAgo(toEpochMs(tx.time)); + + const iconBg = isFailed ? 'bg-status-error/10' : fundsWay === 'in' ? 'bg-status-success/10' : fundsWay === 'out' ? 'bg-primary/8' : 'bg-muted/40'; + const iconColor = isFailed ? 'text-status-error' : fundsWay === 'in' ? 'text-status-success' : fundsWay === 'out' ? 'text-primary' : 'text-muted-foreground'; + const amountColor = isFailed ? 'text-status-error line-through opacity-50' : fundsWay === 'in' ? 'text-status-success' : fundsWay === 'out' ? 'text-status-error' : 'text-foreground'; + + return ( + onViewDetail(tx)} + > +
+ +
+ +
+
+ {getTxMap(tx?.type)} +
+
{timeAgo}
+
+ +
+ + {amountTxt} + + +
+ + +
+ ); +}); + +TransactionRow.displayName = 'TransactionRow'; + +const cardBase = 'canopy-card p-5'; +const cardMotion = { initial: { opacity: 0, y: 12 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.35, delay: 0.18 } }; + +export const RecentTransactionsCard: React.FC = React.memo(({ + transactions, isLoading = false, hasError = false, +}) => { + const { manifest, chain } = useConfig(); + const [selectedTx, setSelectedTx] = useState(null); + + const openDetail = useCallback((tx: Transaction) => { + setSelectedTx({ hash: tx.hash, type: tx.type, amount: tx.amount, status: tx.status, time: tx.time, error: tx.error }); + }, []); + + 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 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 ( + + + + ); + if (isLoading) return ( + + + + ); + if (hasError) return ( + + + + ); + if (!transactions?.length) return ( + + + + ); + + return ( + + {/* Header */} +
+
+
+ +
+ + Recent Transactions + + {/* Live indicator */} + + + + + + Live + +
+ + See all + + +
+ +
+ {transactions.slice(0, 5).map((tx, i) => ( + + ))} +
+ + {transactions.length > 5 && ( +
+ + All {transactions.length} transactions → + +
+ )} + + setSelectedTx(null)} + /> +
+ ); +}); + +RecentTransactionsCard.displayName = 'RecentTransactionsCard'; 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..c3e238ea7 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/StakedBalanceCard.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Coins, ArrowUpRight } from 'lucide-react'; +import { useAccountData } from '@/hooks/useAccountData'; +import { useBalanceChart } from '@/hooks/useBalanceChart'; +import { useConfig } from '@/app/providers/ConfigProvider'; +import AnimatedNumber from '@/components/ui/AnimatedNumber'; +import { SparklineChart } from '@/components/ui/SparklineChart'; + +export const StakedBalanceCard = React.memo(() => { + const { totalStaked, loading } = useAccountData(); + const { data: chartData = [], isLoading: chartLoading } = useBalanceChart({ points: 12, type: 'staked' }); + const { chain } = useConfig(); + + const symbol = chain?.denom?.symbol || 'CNPY'; + const decimals = chain?.denom?.decimals ?? 6; + + const formatValue = (v: number) => + `${(v / Math.pow(10, decimals)).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} ${symbol}`; + + return ( + +
+ + {/* Header */} +
+
+
+ +
+ + Staked Balance + +
+ +
+ + {/* Balance */} +
+ {loading ? ( +
+ ) : ( +
+ + + + {symbol} +
+ )} +
+ + {/* Chart */} +
+
+ {chartLoading && chartData.length === 0 ? ( +
+ ) : ( + + )} +
+
+ + ); +}); + +StakedBalanceCard.displayName = 'StakedBalanceCard'; 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..bfc4c2e5c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/dashboard/TotalBalanceCard.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Wallet, TrendingUp, TrendingDown, ArrowUpRight } from 'lucide-react'; +import { useAccountData } from '@/hooks/useAccountData'; +import { useBalanceHistory } from '@/hooks/useBalanceHistory'; +import { useBalanceChart } from '@/hooks/useBalanceChart'; +import { useConfig } from '@/app/providers/ConfigProvider'; +import AnimatedNumber from '@/components/ui/AnimatedNumber'; +import { SparklineChart } from '@/components/ui/SparklineChart'; + +export const TotalBalanceCard = React.memo(() => { + const { totalBalance, loading } = useAccountData(); + const { data: historyData, isLoading: historyLoading } = useBalanceHistory(); + const { data: chartData = [], isLoading: chartLoading } = useBalanceChart({ points: 12, type: 'balance' }); + const { chain } = useConfig(); + const [hasAnimated, setHasAnimated] = useState(false); + + const symbol = chain?.denom?.symbol || 'CNPY'; + const decimals = chain?.denom?.decimals ?? 6; + + const isPositive = (historyData?.changePercentage ?? 0) >= 0; + + const formatValue = (v: number) => + `${(v / Math.pow(10, decimals)).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} ${symbol}`; + + return ( + setHasAnimated(true)} + > + {/* Ambient glow */} +
+ + {/* Header */} +
+
+
+ +
+ + Total Balance + +
+ +
+ + {/* Balance */} +
+ {loading ? ( +
+ ) : ( +
+ + + + {symbol} +
+ )} +
+ + {/* Chart + 24h change */} +
+ {/* 24h change pill */} +
+ {historyLoading ? ( +
+ ) : historyData ? ( +
+ {isPositive + ? + : + } + + % + 24h +
+ ) : ( + No history + )} +
+ + {/* Sparkline */} +
+ {chartLoading && chartData.length === 0 ? ( +
+ ) : ( + + )} +
+
+ + ); +}); + +TotalBalanceCard.displayName = 'TotalBalanceCard'; 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..47e51c91e --- /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-foreground' + }, + { + id: 'activeProposals', + title: 'Active Proposals', + value: activeProposals.toString(), + subtitle: ( + + + Open for voting + + ), + icon: 'fa-solid fa-vote-yea', + iconColor: 'text-primary', + valueColor: 'text-foreground' + }, + { + id: 'passedProposals', + title: 'Passed Proposals', + value: passedProposals.toString(), + subtitle: `${totalProposals} total proposals`, + icon: 'fa-solid fa-check-circle', + iconColor: 'text-primary', + valueColor: 'text-foreground' + }, + { + id: 'participation', + title: 'Your Participation', + value: '0', + subtitle: 'Votes cast', + icon: 'fa-solid fa-chart-line', + iconColor: 'text-muted-foreground', + valueColor: 'text-foreground' + } + ]; + + 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..2b18ef115 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/PollCard.tsx @@ -0,0 +1,181 @@ +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-muted/20 text-muted-foreground border-border/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} +

+

+ Vote actions auto-fill proposal, endBlock, and URL fields. +

+
+ + {/* Voting Progress Bars */} +
+
+ APPROVE: {poll.yesPercent.toFixed(1)}% + REJECT: {poll.noPercent.toFixed(1)}% +
+ + {/* Combined Progress Bar */} +
+
+
+
+ + {/* Account vs Validator Stats */} +
+ {/* Account Votes */} +
+
+ + Accounts +
+
+
+ Approve + + {poll.accountVotes.yes} + +
+
+ Reject + + {poll.accountVotes.no} + +
+
+
+ + {/* Validator Votes */} +
+
+ + Validators +
+
+
+ Approve + + {poll.validatorVotes.yes} + +
+
+ Reject + + {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..134770219 --- /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-muted/20 text-muted-foreground border-border/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..92469c224 --- /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..8371fd865 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/governance/ProposalTable.tsx @@ -0,0 +1,230 @@ +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; + onDeleteVote?: (proposalHash: string) => void; + onViewDetails?: (proposalHash: string) => void; +} + +export const ProposalTable: React.FC = ({ + proposals, + title, + isPast = false, + onVote, + onDeleteVote, + 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 +

+ )} + {!isPast && ( +

+ Approve/Reject/Delete opens a guided voting flow with explicit proposal changes. +

+ )} +
+
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-lg text-sm text-foreground 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' || proposal.status === 'pending') && onVote && ( + <> + + + {onDeleteVote && ( + + )} + + )} + +
+
+
+
+ ); +}; 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..2fec359cf --- /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-background border border-border rounded-lg text-foreground 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..2a3a84de3 --- /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..5a7ed988b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/key-management/CurrentWallet.tsx @@ -0,0 +1,514 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { motion } from "framer-motion"; +import { + Copy, + Download, + Key, + AlertTriangle, + Eye, + EyeOff, + Trash2, + Wallet, +} 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"; +import { useQueryClient } from "@tanstack/react-query"; + +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 [showDeleteModal, setShowDeleteModal] = useState(false); + const [deleteConfirmation, setDeleteConfirmation] = useState(""); + const [isDeleting, setIsDeleting] = useState(false); + const { copyToClipboard } = useCopyToClipboard(); + const toast = useToast(); + const dsFetch = useDSFetcher(); + const queryClient = useQueryClient(); + 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); + } + }; + + const handleDeleteAccount = () => { + if (!selectedAccount) { + toast.error({ + title: "No Account Selected", + description: "Please select an account to delete", + }); + return; + } + + if (accounts.length === 1) { + toast.error({ + title: "Cannot Delete", + description: "You must have at least one account", + }); + return; + } + + setDeleteConfirmation(""); + setShowDeleteModal(true); + }; + + const handleConfirmDelete = async () => { + if (!selectedAccount) return; + + const nickname = selectedKeyEntry?.keyNickname || selectedAccount.nickname; + if (deleteConfirmation !== nickname) { + toast.error({ + title: "Confirmation Failed", + description: `Please type "${nickname}" to confirm deletion`, + }); + return; + } + + setIsDeleting(true); + + try { + await dsFetch("keystoreDelete", { + nickname: nickname, + }); + + // Invalidate keystore cache + await queryClient.invalidateQueries({ queryKey: ["ds", "keystore"] }); + + toast.success({ + title: "Account Deleted", + description: `Account "${nickname}" has been permanently deleted.`, + }); + + setShowDeleteModal(false); + setDeleteConfirmation(""); + + // Switch to another account + const otherAccounts = accounts.filter((acc) => acc.id !== selectedAccount.id); + if (otherAccounts.length > 0) { + setTimeout(() => { + switchAccount(otherAccounts[0].id); + }, 500); + } + } catch (error) { + toast.error({ + title: "Delete Failed", + description: error instanceof Error ? error.message : String(error), + }); + } finally { + setIsDeleting(false); + } + }; + + return ( + +
+
+

Current Wallet

+

+ Inspect keys, export backups, and manage account lifecycle. +

+
+ + + Active + +
+ +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + + +
+ +
+
+ +
+

+ 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-muted text-foreground border border-border rounded-lg px-3 py-2.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35" + /> + {passwordError && ( +
{passwordError}
+ )} +
+ + +
+
+
+ )} + + {showDeleteModal && ( +
+
+
+
+ +
+

+ Delete Account +

+
+ +
+

+ This action is permanent and irreversible +

+

+ Make sure you have backed up your private key before deleting this account. + You will lose access to all funds if you haven't saved your private key. +

+
+ +

+ Type + {selectedKeyEntry?.keyNickname || selectedAccount?.nickname} + to confirm deletion: +

+ + setDeleteConfirmation(e.target.value)} + placeholder="Type wallet name to confirm" + className="w-full bg-muted text-foreground border border-border rounded-lg px-3 py-2.5 mb-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/35" + autoFocus + /> + +
+ + +
+
+
+ )} +
+ ); +}; + + 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..1bbaaab93 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/key-management/ImportWallet.tsx @@ -0,0 +1,286 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { AlertTriangle, Eye, EyeOff, KeyRound, FileJson } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { useToast } from '@/toast/ToastContext'; +import { useDSFetcher } from '@/core/dsFetch'; +import { useQueryClient } from '@tanstack/react-query'; + +export const ImportWallet = (): JSX.Element => { + const toast = useToast(); + const dsFetch = useDSFetcher(); + const queryClient = useQueryClient(); + + const [showPrivateKey, setShowPrivateKey] = useState(false); + const [activeTab, setActiveTab] = useState<'key' | 'keystore'>('key'); + const [importForm, setImportForm] = useState({ + privateKey: '', + password: '', + confirmPassword: '', + nickname: '' + }); + + 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.nickname) { + toast.error({ title: 'Missing wallet name', description: 'Please enter a wallet name.' }); + 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; + } + + // Validate private key format (should be hex, 64-128 chars) + const cleanPrivateKey = importForm.privateKey.trim().replace(/^0x/, ''); + if (!/^[0-9a-fA-F]{64,128}$/.test(cleanPrivateKey)) { + toast.error({ + title: 'Invalid private key', + description: 'Private key must be 64-128 hexadecimal characters.' + }); + return; + } + + const loadingToast = toast.info({ + title: 'Importing wallet...', + description: 'Please wait while your wallet is imported.', + sticky: true, + }); + + try { + const response = await dsFetch('keystoreImportRaw', { + nickname: importForm.nickname, + password: importForm.password, + privateKey: cleanPrivateKey + }); + + // Invalidate keystore cache to refetch + await queryClient.invalidateQueries({ queryKey: ['ds', 'keystore'] }); + + toast.dismiss(loadingToast); + toast.success({ + title: 'Wallet imported', + description: `Wallet "${importForm.nickname}" has been imported successfully.`, + }); + + setImportForm({ privateKey: '', password: '', confirmPassword: '', nickname: '' }); + + // Switch to the newly imported account if response contains address + const newAddress = typeof response === 'string' ? response : (response as any)?.address; + if (newAddress) { + // Wait a bit for keystore to update, then try to switch + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: ['ds', 'keystore'] }); + }, 500); + } + } catch (error) { + toast.dismiss(loadingToast); + toast.error({ + title: 'Error importing wallet', + description: error instanceof Error ? error.message : String(error) + }); + } + }; + + return ( + +
+
+

Import Wallet

+

Bring an existing key into this node securely.

+
+ + + Recovery + +
+ +
+ + +
+ + {activeTab === 'key' && ( +
+
+ + setImportForm({ ...importForm, nickname: e.target.value })} + className="w-full bg-muted border border-border rounded-lg px-3 py-2.5 text-foreground" + /> +
+ +
+ +
+ setImportForm({ ...importForm, privateKey: e.target.value })} + className="w-full bg-muted border border-border rounded-lg px-3 py-2.5 text-foreground pr-10 placeholder:font-mono focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35" + /> + +
+
+ +
+ + setImportForm({ ...importForm, password: e.target.value })} + className="w-full bg-muted border border-border rounded-lg px-3 py-2.5 text-foreground" + /> +
+ +
+ + setImportForm({ ...importForm, confirmPassword: e.target.value })} + className="w-full bg-muted border border-border rounded-lg px-3 py-2.5 text-foreground" + /> +
+ +
+
+ +
+

Import Security Warning

+

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

+
+
+
+ + +
+ )} + + {activeTab === 'keystore' && ( +
+
+ +
+ + Upload encrypted JSON 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..b108cc75f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/key-management/NewKey.tsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Sparkles, ShieldCheck } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useToast } from '@/toast/ToastContext'; +import { useDSFetcher } from '@/core/dsFetch'; +import { useQueryClient } from '@tanstack/react-query'; + +export const NewKey = (): JSX.Element => { + const { switchAccount } = useAccounts(); + const toast = useToast(); + const dsFetch = useDSFetcher(); + const queryClient = useQueryClient(); + + 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.walletName) { + toast.error({ title: 'Missing wallet name', description: 'Please enter a wallet name.' }); + return; + } + + if (!newKeyForm.password) { + toast.error({ title: 'Missing password', description: 'Please enter a password.' }); + return; + } + + const loadingToast = toast.info({ + title: 'Creating wallet...', + description: 'Please wait while your wallet is created.', + sticky: true, + }); + + try { + const response = await dsFetch('keystoreNewKey', { + nickname: newKeyForm.walletName, + password: newKeyForm.password + }); + + // Invalidate keystore cache to refetch + await queryClient.invalidateQueries({ queryKey: ['ds', 'keystore'] }); + + toast.dismiss(loadingToast); + toast.success({ + title: 'Wallet created', + description: `Wallet "${newKeyForm.walletName}" is ready.`, + }); + + setNewKeyForm({ password: '', walletName: '' }); + + // Switch to the newly created account if response contains address + const newAddress = typeof response === 'string' ? response : (response as any)?.address; + if (newAddress) { + // Wait a bit for keystore to update, then try to switch + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: ['ds', 'keystore'] }); + }, 500); + } + } catch (error) { + toast.dismiss(loadingToast); + toast.error({ + title: 'Error creating wallet', + description: error instanceof Error ? error.message : String(error), + }); + } + }; + + return ( + +
+
+

Create New Key

+

Generate a fresh encrypted wallet identity.

+
+ + + New + +
+ +
+
+
+ + setNewKeyForm({ ...newKeyForm, walletName: e.target.value })} + className="w-full bg-muted border border-border rounded-lg px-3 py-2.5 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35" + /> +
+
+ + setNewKeyForm({ ...newKeyForm, password: e.target.value })} + className="w-full bg-muted border border-border rounded-lg px-3 py-2.5 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35" + /> +
+
+ + This key is encrypted using your password and stored in the local keystore. +
+
+ + +
+
+ ); +}; + diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/AppSidebar.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/AppSidebar.tsx new file mode 100644 index 000000000..207b8376c --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/AppSidebar.tsx @@ -0,0 +1,235 @@ +import React, { useState } from 'react'; +import { NavLink, Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + LayoutDashboard, + Wallet, + TrendingUp, + Vote, + ShoppingCart, + Activity, + KeyRound, + ChevronLeft, + ChevronRight, + Menu, + X, +} from 'lucide-react'; +import { CnpyLogoIcon } from '@/components/ui/CnpyLogo'; + +const navItems = [ + { name: 'Dashboard', path: '/', icon: LayoutDashboard }, + { name: 'Accounts', path: '/accounts', icon: Wallet }, + { name: 'Staking', path: '/staking', icon: TrendingUp }, + { name: 'Governance', path: '/governance', icon: Vote }, + { name: 'Orders', path: '/orders', icon: ShoppingCart }, + { name: 'Monitoring', path: '/monitoring', icon: Activity }, + { name: 'Keys', path: '/key-management', icon: KeyRound }, +]; + +const NAV_BASE = + 'relative flex w-full min-w-0 items-center gap-3 rounded-lg border py-2 pr-2.5 pl-4 text-sm font-medium transition-all duration-150'; +const NAV_ACTIVE = + 'nav-item-active border-primary/30 text-primary shadow-[0_0_0_1px_rgba(53,205,72,0.16)]'; +const NAV_INACTIVE = + 'border-transparent text-muted-foreground hover:border-primary/20 hover:bg-accent/65 hover:text-foreground'; + +export const AppSidebar = (): JSX.Element => { + const [collapsed, setCollapsed] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + + const sidebarW = collapsed ? 72 : 238; + + return ( + <> + +
+ +
+ +
+ +
+ + {!collapsed && ( + + Canopy Wallet + + )} + + +
+ + + +
+ +
+ + +
+
+ + +
+ +
+ Canopy Wallet + +
+
+ + + {mobileOpen && ( + <> + setMobileOpen(false)} + /> + +
+
+ setMobileOpen(false)} + className="flex items-center gap-2.5" + > +
+ +
+ + Canopy Wallet + + + +
+
+ + +
+ + )} +
+
+ + ); +}; 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..4398debcb --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Footer.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +export const Footer = (): JSX.Element => { + const links = [ + { label: 'Terms', href: '#' }, + { label: 'Privacy', href: '#' }, + { label: 'Security', href: '#' }, + { label: 'Support', href: '#' }, + ]; + + return ( +
+
+
+ {links.map(({ label, href }) => ( + + {label} + + ))} + + v1.0 + +
+
+
+ ); +}; 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..0abcff183 --- /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..023d6cde3 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/MainLayout.tsx @@ -0,0 +1,23 @@ +import { Outlet } from 'react-router-dom' +import { AppSidebar } from './AppSidebar' +import { TopBar } from './TopBar' + +export default function MainLayout() { + return ( +
+
+ + + +
+ + +
+
+ +
+
+
+
+ ) +} 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..04eb2a416 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Navbar.tsx @@ -0,0 +1,219 @@ +import React, { useState } from 'react'; +import { Key, Menu, X, Blocks } 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 { useDS } from "@/core/useDs"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; +import Logo from './Logo'; +import { Link, NavLink } from 'react-router-dom'; + +const navItems = [ + { name: 'Dashboard', path: '/' }, + { name: 'Accounts', path: '/accounts' }, + { name: 'Staking', path: '/staking' }, + { name: 'Governance', path: '/governance' }, + { name: 'Monitoring', path: '/monitoring' }, +]; + +const mobileMenuVariants: Variants = { + closed: { opacity: 0, height: 0, transition: { duration: 0.25, ease: 'easeInOut' } }, + open: { opacity: 1, height: 'auto', transition: { duration: 0.25, ease: 'easeInOut' } }, +}; + +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 { data: blockHeight } = useDS<{ height: number }>('height', {}, { + staleTimeMs: 10_000, + refetchIntervalMs: 10_000, + }); + + return ( + +
+ + {/* ── Mobile header bar ── */} +
+ + {/* Logo + block height */} +
+ + + + Wallet + + + + {/* Block height pill */} +
+ + + + + + + {blockHeight != null ? blockHeight.height.toLocaleString() : '—'} + +
+
+ + {/* Hamburger */} + setIsMobileMenuOpen(!isMobileMenuOpen)} + whileTap={{ scale: 0.93 }} + > + {isMobileMenuOpen + ? + : + } + +
+ + {/* ── Dropdown menu ── */} + + {isMobileMenuOpen && ( + +
+ + {/* Nav links */} + + +
+ + {/* Total CNPY */} +
+ Total +
+ {stageLoading ? ( + + ) : ( + + )} + CNPY +
+
+ + {/* Account selector */} + + + {/* Key Management */} + setIsMobileMenuOpen(false)} + className="w-full h-11 bg-primary hover:bg-primary-light text-primary-foreground rounded-lg flex items-center justify-center gap-2 text-sm font-semibold transition-colors duration-150" + > + + Key Management + +
+ + )} + + + ); +}; + 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..1176c23da --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/Sidebar.tsx @@ -0,0 +1,2 @@ +// Re-exported from AppSidebar — kept for backward compatibility +export { AppSidebar as Sidebar } from './AppSidebar'; diff --git a/cmd/rpc/web/wallet-new/src/components/layouts/TopBar.tsx b/cmd/rpc/web/wallet-new/src/components/layouts/TopBar.tsx new file mode 100644 index 000000000..361b7b52f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/TopBar.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Link } from 'react-router-dom'; +import { Key, Blocks } from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/Select'; +import { useAccounts } from '@/app/providers/AccountsProvider'; +import { useTotalStage } from '@/hooks/useTotalStage'; +import { useDS } from '@/core/useDs'; +import AnimatedNumber from '@/components/ui/AnimatedNumber'; + +export const TopBar = (): JSX.Element => { + const { + accounts, + loading, + error: hasErrorInAccounts, + switchAccount, + selectedAccount, + } = useAccounts(); + + const { data: totalStage, isLoading: stageLoading } = useTotalStage(); + const { data: blockHeight } = useDS<{ height: number }>('height', {}, { + staleTimeMs: 10_000, + refetchIntervalMs: 10_000, + }); + + return ( + +
+ +
+
+ + + + + + + {blockHeight != null ? `#${blockHeight.height.toLocaleString()}` : '-'} + +
+
+ +
+
+ Total + {stageLoading ? ( + ... + ) : ( + + )} + CNPY +
+ +
+ + + + + + Keys + +
+ + ); +}; 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..b5fb65627 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/layouts/TopNavbar.tsx @@ -0,0 +1,2 @@ +// Re-exported from TopBar — kept for backward compatibility +export { TopBar as TopNavbar } from './TopBar'; 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..761b4439d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/MetricsCard.tsx @@ -0,0 +1,100 @@ +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-card rounded-xl border border-border/60 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..3562b03e2 --- /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..bf6cadb4a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkPeers.tsx @@ -0,0 +1,57 @@ +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..25c0da7dd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NetworkStatsCard.tsx @@ -0,0 +1,64 @@ +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-primary' + }, + { + id: 'connections', + label: 'Connections', + value: `${connections.in} in / ${connections.out} out`, + color: 'text-foreground' + } + ]; + + 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..d5a038890 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NodeLogs.tsx @@ -0,0 +1,146 @@ +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
+ )} +
+
+ ); +} 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..b6d508884 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/NodeStatus.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import { Copy } from "lucide-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, + onCopyAddress, +}: NodeStatusProps): JSX.Element { + const currentNode = + availableNodes.find((n) => n.id === selectedNode) || availableNodes[0]; + + const truncate = (addr: string) => + addr ? `${addr.slice(0, 8)}...${addr.slice(-4)}` : "Connecting..."; + + return ( + <> + {/* Node identity row */} +
+
+ + {nodeStatus.synced && ( + + )} + + +
+

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

+ {currentNode?.netAddress && ( +

{currentNode.netAddress}

+ )} +
+
+ + +
+ + {/* Status bar */} +
+ {/* Sync */} +
+ Sync Status + + {nodeStatus.synced ? "SYNCED" : "SYNCING"} + +
+ + {/* Block height */} +
+ Block Height + + #{nodeStatus.blockHeight.toLocaleString()} + +
+ + {/* Progress */} +
+ Round Progress +
+
+
+ {nodeStatus.syncProgress}% +
+ + {/* Address */} +
+ Node Address + + {truncate(nodeStatus.nodeAddress)} + +
+
+ + ); +} 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..2a0d95676 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetrics.tsx @@ -0,0 +1,67 @@ +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; + }; +} + +/** Returns tailwind color classes based on usage level */ +const barColor = (pct: number) => + pct >= 85 ? 'bg-red-500' + : pct >= 60 ? 'bg-status-warning' + : 'bg-primary'; + +export default function PerformanceMetrics({ metrics }: PerformanceMetricsProps): JSX.Element { + const items = [ + { label: 'Process CPU', value: metrics.processCPU, unit: '%', pct: Math.max(metrics.processCPU, 0.5) }, + { label: 'System CPU', value: metrics.systemCPU, unit: '%', pct: Math.max(metrics.systemCPU, 0.5) }, + { label: 'Process RAM', value: metrics.processRAM, unit: '%', pct: Math.min(metrics.processRAM, 100) }, + { label: 'System RAM', value: metrics.systemRAM, unit: '%', pct: Math.min(metrics.systemRAM, 100) }, + { label: 'Disk Usage', value: metrics.diskUsage, unit: '%', pct: Math.min(metrics.diskUsage, 100) }, + { label: 'Network I/O', value: metrics.networkIO, unit: ' MB/s', pct: Math.min((metrics.networkIO / 10) * 100, 100) }, + ]; + + return ( +
+

Performance Metrics

+ +
+ {items.map((item) => ( +
+
+ {item.label} + + {item.value.toFixed(1)}{item.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..2c572eca5 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/PerformanceMetricsCard.tsx @@ -0,0 +1,79 @@ +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: 'hsl(var(--primary))' + }, + { + 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..ef893298e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/RawJSON.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import { useDSFetcher } from '@/core/dsFetch'; +import { useQuery } from '@tanstack/react-query'; + +type RawJSONTab = 'quorum' | 'config' | 'peerInfo' | 'peerBook'; + +interface RawJSONProps { + activeTab: RawJSONTab; + onTabChange: (tab: RawJSONTab) => void; + onExportLogs: () => void; +} + +const tabData: Array<{ + id: RawJSONTab; + label: string; + icon: string; + dsKey: string; + refetchInterval: number; + staleTime: number; +}> = [ + { + id: 'quorum', + label: 'Quorum', + icon: 'fa-users', + dsKey: 'admin.consensusInfo', + refetchInterval: 2000, + staleTime: 1000, + }, + { + id: 'config', + label: 'Config', + icon: 'fa-gear', + dsKey: 'admin.config', + refetchInterval: 30000, + staleTime: 25000, + }, + { + id: 'peerInfo', + label: 'Peer Info', + icon: 'fa-circle-info', + dsKey: 'admin.peerInfo', + refetchInterval: 5000, + staleTime: 4000, + }, + { + id: 'peerBook', + label: 'Peer Book', + icon: 'fa-address-book', + dsKey: 'admin.peerBook', + refetchInterval: 30000, + staleTime: 25000, + }, +]; + +export default function RawJSON({ + activeTab, + onTabChange, + onExportLogs, +}: RawJSONProps): JSX.Element { + const dsFetch = useDSFetcher(); + + 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: currentTab?.refetchInterval ?? 5000, + staleTime: currentTab?.staleTime ?? 4000, + }); + + 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..bc571d617 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResources.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +interface SystemResourcesProps { + systemResources: { + threadCount: number; + fileDescriptors: number; + maxFileDescriptors: number; + }; +} + +export default function SystemResources({ systemResources }: SystemResourcesProps): JSX.Element { + const fdMax = systemResources.maxFileDescriptors || 1024; + const fdPct = Math.min((systemResources.fileDescriptors / fdMax) * 100, 100); + const threadPct = Math.min((systemResources.threadCount / 100) * 100, 100); + + const barColor = (pct: number) => + pct >= 85 ? 'bg-red-500' : pct >= 60 ? 'bg-status-warning' : 'bg-primary'; + + return ( +
+

System Resources

+ +
+ {/* Thread Count */} +
+
+ Thread Count + + {systemResources.threadCount} + +
+
+
+
+
+ + {/* File Descriptors */} +
+
+ File Descriptors + + {systemResources.fileDescriptors.toLocaleString()} / {fdMax.toLocaleString()} + +
+
+
+
+
+
+
+ ); +} 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..ace63a018 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/monitoring/SystemResourcesCard.tsx @@ -0,0 +1,71 @@ +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..a14f0e762 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/StatsCards.tsx @@ -0,0 +1,120 @@ +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-foreground' + }, + { + id: 'rewardsEarned', + title: 'Rewards Earned', + value: `${formatRewards(totalRewards)} CNPY`, + subtitle: `Last 24 hours - ${validatorsCount} validators`, + icon: 'fa-solid fa-ellipsis', + iconColor: 'text-muted-foreground', + valueColor: 'text-primary' + }, + { + id: 'activeValidators', + title: 'Active Validators', + value: validatorsCount.toString(), + subtitle: ( + + + {'All online'} + + ), + icon: 'fa-solid fa-shield-halved', + iconColor: 'text-muted-foreground', + valueColor: 'text-foreground' + }, + { + id: 'chainsStaked', + title: 'Chains Staked', + value: (chainCount || 0).toString(), + icon: 'fa-solid fa-link', + iconColor: 'text-muted-foreground', + valueColor: 'text-foreground' + } + ]; + + return ( +
+ {statsData.map((stat) => ( + +
+

+ {stat.title} +

+ +
+

+ {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..3116ad490 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/Toolbar.tsx @@ -0,0 +1,83 @@ +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-card border border-border rounded-lg pl-10 pr-4 py-2 text-foreground 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..b462fa95d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/ValidatorCard.tsx @@ -0,0 +1,225 @@ +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..ccb4470ee --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/staking/ValidatorList.tsx @@ -0,0 +1,51 @@ +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/transactions/TransactionDetailModal.tsx b/cmd/rpc/web/wallet-new/src/components/transactions/TransactionDetailModal.tsx new file mode 100644 index 000000000..551edc267 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/transactions/TransactionDetailModal.tsx @@ -0,0 +1,350 @@ +import React from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { X, Copy, ExternalLink, CheckCircle2, AlertTriangle } from "lucide-react"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; + +export interface TxError { + code: number; + module: string; + msg: string; +} + +export interface TxDetail { + hash: string; + type: string; + amount: number; + fee?: number; + status: string; + time: number; + address?: string; + error?: TxError; +} + +interface TransactionDetailModalProps { + tx: TxDetail | null; + open: boolean; + onClose: () => void; +} + +/* --- helpers --------------------------------------------------- */ + +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 formatDate = (tsMs: number) => + new Date(tsMs).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + +const formatTimeAgo = (tsMs: number) => { + const diff = Math.max(0, Date.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 getStatusColor = (s: string) => { + if (s === "Confirmed") return "bg-green-500/20 text-green-400 border-green-500/30"; + if (s === "Pending") return "bg-yellow-500/20 text-yellow-400 border-yellow-500/30"; + if (s === "Open") return "bg-orange-500/20 text-orange-400 border-orange-500/30"; + if (s === "Failed") return "bg-red-500/20 text-red-400 border-red-500/30"; + return "bg-muted/20 text-muted-foreground border-border/30"; +}; + +/* --- sub-components -------------------------------------------- */ + +const DetailRow = ({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) => ( +
+ + {label} + +
+ {children} +
+
+); + +const CopyHash = ({ hash }: { hash: string }) => { + const { copyToClipboard } = useCopyToClipboard(); + const [copied, setCopied] = React.useState(false); + + const handleCopy = async () => { + const ok = await copyToClipboard(hash, "Transaction hash"); + if (ok) { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } + }; + + return ( + + ); +}; + +/* --- main modal ------------------------------------------------ */ + +export const TransactionDetailModal: React.FC = ({ + tx, + open, + onClose, +}) => { + const { manifest, chain } = useConfig(); + + const getIcon = (type: string) => + manifest?.ui?.tx?.typeIconMap?.[type] ?? "Circle"; + const getTxMap = (type: string) => + manifest?.ui?.tx?.typeMap?.[type] ?? type; + const getFundWay = (type: string) => + manifest?.ui?.tx?.fundsWay?.[type] ?? "neutral"; + + const symbol = chain?.denom?.symbol ?? "CNPY"; + const decimals = Number(chain?.denom?.decimals ?? 6); + const toDisplay = (n: number) => n / Math.pow(10, decimals); + + const explorerBase = chain?.explorer ?? ""; + + React.useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [open, onClose]); + + return ( + + {open && tx && ( + <> + + + + e.stopPropagation()} + > +
+
+
+ +
+
+

+ {getTxMap(tx.type)} +

+

+ Transaction detail +

+
+
+
+ + {tx.status} + + +
+
+ +
+
+

+ Transaction Hash +

+
+ + {explorerBase && ( + + + + )} +
+
+ +
+ {tx.time > 0 && ( + +
+
{formatDate(toEpochMs(tx.time))}
+
+ {formatTimeAgo(toEpochMs(tx.time))} +
+
+
+ )} + + {tx.amount != null && ( + + + {getFundWay(tx.type) === "out" + ? "-" + : getFundWay(tx.type) === "in" + ? "+" + : ""} + {toDisplay(Number(tx.amount)).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 6, + })}{" "} + {symbol} + + + )} + + {tx.fee != null && tx.fee > 0 && ( + + + {toDisplay(Number(tx.fee)).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 6, + })}{" "} + {symbol} + + + )} + + {tx.address && ( + + + {tx.address.slice(0, 10)}...{tx.address.slice(-8)} + + + )} + + +
+ + {getTxMap(tx.type)} +
+
+
+ + {tx.error && ( +
+
+ +

+ Transaction Error +

+
+
+
+ + Code + + {tx.error.code} +
+
+ + Module + + {tx.error.module} +
+
+ + Message + + {tx.error.msg} +
+
+
+ )} +
+ + {explorerBase && ( + + )} +
+
+ + )} +
+ ); +}; + +export default TransactionDetailModal; 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..c56f8853f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/AlertModal.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Button } from '@/components/ui/Button'; + +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..be898f155 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Badge.tsx @@ -0,0 +1,38 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import { cx } from "@/ui/cx"; + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-[0.02em] whitespace-nowrap [&>svg]:size-3 focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-colors", + { + variants: { + variant: { + default: "border-primary/35 bg-primary/18 text-primary", + secondary: "border-border/70 bg-secondary/85 text-secondary-foreground", + destructive: "border-destructive/45 bg-destructive/18 text-red-200", + outline: "border-border/85 bg-secondary/55 text-foreground", + virtual_active: "border-primary/32 bg-primary/16 text-primary", + pending_launch: "border-amber-500/35 bg-amber-500/15 text-amber-300", + draft: "border-border/60 bg-muted/65 text-muted-foreground", + rejected: "border-destructive/45 bg-destructive/18 text-red-200", + graduated: "border-emerald-500/36 bg-emerald-500/16 text-emerald-300", + failed: "border-destructive/45 bg-destructive/18 text-red-200", + moderated: "border-orange-500/35 bg-orange-500/16 text-orange-300", + }, + }, + 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..24738d474 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Button.tsx @@ -0,0 +1,65 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cx } from "@/ui/cx"; + +const buttonVariants = cva( + "inline-flex items-center justify-center cursor-pointer gap-2 whitespace-nowrap rounded-[10px] text-sm font-semibold font-body transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none aria-invalid:ring-destructive/20 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-[0_0_0_1px_hsl(var(--primary)/0.35)_inset,0_8px_18px_hsl(var(--primary)/0.22)] hover:bg-primary/90 hover:-translate-y-[1px] btn-glow", + destructive: + "bg-destructive text-white shadow-[0_0_0_1px_rgba(239,68,68,0.35)_inset,0_8px_16px_rgba(239,68,68,0.2)] hover:bg-destructive/90 hover:-translate-y-[1px] focus-visible:ring-destructive/25", + outline: + "border border-border/85 bg-secondary/70 text-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] hover:bg-secondary/85 hover:border-primary/40", + secondary: + "border border-border/70 bg-secondary text-secondary-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:bg-secondary/85 hover:border-primary/30", + ghost: "text-foreground hover:bg-accent/70 hover:text-accent-foreground", + clear: "border-none bg-transparent hover:bg-accent/70 hover:text-accent-foreground", + clear2: "bg-transparent hover:bg-accent/70 hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + neomorphic: + "border border-white/15 bg-[linear-gradient(145deg,rgba(255,255,255,0.14),rgba(255,255,255,0.06))] text-white hover:border-primary/35", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + freeflow: "", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ComponentProps<"button">, + 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..a43eba378 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Card.tsx @@ -0,0 +1,151 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cx } from "@/ui/cx"; + +const cardVariants = cva("relative flex flex-col border shadow-sm", { + variants: { + variant: { + default: "canopy-card-soft gap-5 text-card-foreground", + dark: "surface-card gap-5 text-white", + glass: + "gap-5 border border-white/15 bg-white/[0.045] text-white backdrop-blur-xl shadow-[inset_0_1px_0_rgba(255,255,255,0.08),0_14px_28px_rgba(0,0,0,0.32)]", + outline: + "gap-5 border-border/85 bg-secondary/55 text-foreground backdrop-blur-md shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-primary/35", + ghost: "bg-transparent border-transparent shadow-none", + gradient: + "gap-5 border-primary/20 bg-[linear-gradient(145deg,hsl(0_0%_16%_/_0.95),hsl(0_0%_10%_/_0.95))] text-white shadow-[0_0_0_1px_hsl(var(--primary)/0.14),0_20px_36px_hsl(0_0%_0%/0.32)]", + launchpad: "gap-4 lg:gap-5", + }, + size: { + default: "py-4 lg:py-6", + launchpad: "py-0", + sm: "py-4", + lg: "py-8", + xl: "py-12", + none: "py-0", + }, + padding: { + default: "px-5 lg:px-6", + launchpad: "px-0", + sm: "px-4", + lg: "px-8", + xl: "px-12", + none: "p-0", + explorer: "px-6", + }, + rounded: { + default: "rounded-xl", + lg: "rounded-lg", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + padding: "default", + rounded: "default", + }, +}); + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, size, padding, rounded, ...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"; + +const CardAction = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardAction.displayName = "CardAction"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, + cardVariants, +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/CnpyLogo.tsx b/cmd/rpc/web/wallet-new/src/components/ui/CnpyLogo.tsx new file mode 100644 index 000000000..3c4a34d8f --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/CnpyLogo.tsx @@ -0,0 +1,66 @@ +import { SVGProps } from "react"; +import { canopyIconSvg } from "@/lib/utils/brand"; + +/** CNPY logo in a gradient circle — used for token avatars and badges */ +export function CnpyLogo({ size = 36 }: { size?: number }) { + return ( +
+ ); +} + +/** Raw CNPY logo SVG — used inline where currentColor fill is needed */ +export function CnpyLogoIcon(props: SVGProps) { + return ( + + ); +} 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..21d4aac74 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/ConfirmModal.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Button } from '@/components/ui/Button'; + +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/Dialog.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Dialog.tsx new file mode 100644 index 000000000..ab22356ea --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Dialog.tsx @@ -0,0 +1,163 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import * as VisuallyHiddenPrimitive from "@radix-ui/react-visually-hidden"; +import { ArrowLeft, XIcon } from "lucide-react"; + +import { cx } from "@/ui/cx"; + +function Dialog(props: React.ComponentProps) { + return ; +} + +function DialogTrigger(props: React.ComponentProps) { + return ; +} + +function DialogPortal(props: React.ComponentProps) { + return ; +} + +function DialogClose(props: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + showBackButton = false, + onBack, + title, + // noAnimation can be passed by parent components and should not reach the DOM + noAnimation, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; + showBackButton?: boolean; + onBack?: () => void; + title?: string; + noAnimation?: boolean; +}) { + void noAnimation; + + return ( + + + + {title && ( + + {title} + + )} + {showBackButton && onBack && ( + + )} + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +const VisuallyHidden = VisuallyHiddenPrimitive.Root; + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, + VisuallyHidden, +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/EmptyState.tsx b/cmd/rpc/web/wallet-new/src/components/ui/EmptyState.tsx new file mode 100644 index 000000000..78b1d4faa --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/EmptyState.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { LucideIcon } from "@/components/ui/LucideIcon"; +import { cx } from "@/ui/cx"; + +export interface EmptyStateProps { + /** Icon name from Lucide icons */ + icon?: string; + /** Main title text */ + title: string; + /** Optional description text */ + description?: string; + /** Optional action button */ + action?: { + label: string; + onClick: () => void; + }; + /** Additional CSS classes */ + className?: string; + /** Size variant */ + size?: "sm" | "md" | "lg"; +} + +const sizeConfig = { + sm: { + icon: "w-8 h-8", + iconBg: "w-12 h-12", + title: "text-sm", + description: "text-xs", + padding: "py-6", + gap: "gap-2", + }, + md: { + icon: "w-10 h-10", + iconBg: "w-16 h-16", + title: "text-base", + description: "text-sm", + padding: "py-8", + gap: "gap-3", + }, + lg: { + icon: "w-12 h-12", + iconBg: "w-20 h-20", + title: "text-lg", + description: "text-base", + padding: "py-12", + gap: "gap-4", + }, +}; + +export const EmptyState = React.memo(({ + icon = "Inbox", + title, + description, + action, + className, + size = "md", +}) => { + const config = sizeConfig[size]; + + return ( + +
+ +
+ +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ + {action && ( + + )} +
+ ); +}); + +EmptyState.displayName = "EmptyState"; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Input.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Input.tsx new file mode 100644 index 000000000..524ce25ff --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Input.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cx } from "@/ui/cx"; + +const inputVariants = cva( + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex w-full min-w-0 rounded-md border bg-transparent text-foreground transition-[color,box-shadow,border-color] outline-none file:inline-flex file:border-0 file:bg-transparent file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "border-input/85 bg-secondary/70 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-primary/35 focus-visible:border-primary focus-visible:ring-primary/25 focus-visible:ring-[3px]", + wallet: + "border-none bg-transparent focus-visible:outline-none focus-visible:ring-0", + ghost: + "border-transparent bg-transparent hover:border-input/85 hover:bg-secondary/40 focus-visible:border-primary focus-visible:ring-primary/25 focus-visible:ring-[3px]", + destructive: + "border-destructive/80 bg-destructive/10 focus-visible:border-destructive focus-visible:ring-destructive/35 focus-visible:ring-[3px]", + }, + size: { + sm: "h-8 px-2 py-1 text-sm file:h-6 file:text-xs", + default: "h-9 px-3 py-1 text-base md:text-sm file:h-7 file:text-sm", + lg: "h-10 px-4 py-2 text-lg file:h-8 file:text-base", + wallet: "h-14 px-4 text-3xl file:h-8 file:text-base", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +type InputProps = Omit, "size"> & + VariantProps; + +const Input = React.forwardRef( + ({ className, type, variant, size, ...props }, ref) => { + return ( + + ); + }, +); + +Input.displayName = "Input"; + +export { Input }; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Label.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Label.tsx new file mode 100644 index 000000000..60cb2c172 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Label.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; + +import { cx } from "@/ui/cx"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/LoadingState.tsx b/cmd/rpc/web/wallet-new/src/components/ui/LoadingState.tsx new file mode 100644 index 000000000..730c50269 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/LoadingState.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Loader2 } from "lucide-react"; +import { cx } from "@/ui/cx"; + +export interface LoadingStateProps { + /** Loading message */ + message?: string; + /** Size variant */ + size?: "sm" | "md" | "lg"; + /** Additional CSS classes */ + className?: string; + /** Show as overlay on existing content */ + overlay?: boolean; +} + +const sizeConfig = { + sm: { + spinner: "w-5 h-5", + text: "text-xs", + padding: "py-4", + gap: "gap-2", + }, + md: { + spinner: "w-8 h-8", + text: "text-sm", + padding: "py-8", + gap: "gap-3", + }, + lg: { + spinner: "w-10 h-10", + text: "text-base", + padding: "py-12", + gap: "gap-4", + }, +}; + +export const LoadingState = React.memo(({ + message = "Loading...", + size = "md", + className, + overlay = false, +}) => { + const config = sizeConfig[size]; + + const content = ( + + + {message && ( + {message} + )} + + ); + + if (overlay) { + return ( +
+ {content} +
+ ); + } + + return content; +}); + +LoadingState.displayName = "LoadingState"; + +// Skeleton components for more sophisticated loading states +export const Skeleton = React.memo<{ + className?: string; + animate?: boolean; +}>(({ className, animate = true }) => ( +
+)); + +Skeleton.displayName = "Skeleton"; + +export const SkeletonText = React.memo<{ + lines?: number; + className?: string; +}>(({ lines = 1, className }) => ( +
+ {Array.from({ length: lines }).map((_, i) => ( + 1 ? "w-3/4" : "w-full" + )} + /> + ))} +
+)); + +SkeletonText.displayName = "SkeletonText"; + +export const SkeletonCard = React.memo<{ className?: string }>(({ className }) => ( +
+
+ +
+ + +
+
+ +
+)); + +SkeletonCard.displayName = "SkeletonCard"; 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..374a5da2d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/LucideIcon.tsx @@ -0,0 +1,62 @@ +import React, { Suspense } from 'react'; +import dynamicIconImports from 'lucide-react/dynamicIconImports'; +import { CircleHelp } from 'lucide-react'; + +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') // separate uppercase letters with "-" + .replace(/[_\s]+/g, '-') // convert spaces or underscores to "-" + .toLowerCase() + .trim(); +}; + +const FALLBACKS = ['circle-help', 'help-circle', 'zap', 'circle', 'square']; +const FALLBACK_ICON: React.ComponentType = CircleHelp; +const ICON_ALIASES: Record = { + 'check-square': 'check', + 'square-check-big': 'check', +}; + +const cache = new Map>>(); + +export function LucideIcon({ name = 'HelpCircle', className }: Props) { + const normalizedName = normalize(name); + const key = ICON_ALIASES[normalizedName] || normalizedName; + + 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) { + const safeImporter: Importer = () => + importer().catch((error) => { + if (typeof console !== 'undefined') { + console.warn(`[LucideIcon] Failed to load icon "${resolvedName}"`, error); + } + return Promise.resolve({ default: FALLBACK_ICON }); + }); + + Icon = React.lazy(safeImporter); + 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..30cf7cd1e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/PauseUnpauseModal.tsx @@ -0,0 +1,505 @@ +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"; +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import { Label } from "@/components/ui/Label"; + +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-muted border border-border rounded-lg text-foreground" + 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-muted border border-border rounded-lg text-foreground" + 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-muted border border-border rounded-lg text-foreground" + 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..a415d6174 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Select.tsx @@ -0,0 +1,182 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; + +import { cx } from "@/ui/cx"; + +function Select(props: React.ComponentProps) { + return ; +} + +function SelectGroup(props: React.ComponentProps) { + return ; +} + +function SelectValue({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "md" | "default"; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/SparklineChart.tsx b/cmd/rpc/web/wallet-new/src/components/ui/SparklineChart.tsx new file mode 100644 index 000000000..b7c72c97b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/SparklineChart.tsx @@ -0,0 +1,227 @@ +/** + * SparklineChart — thin area line chart built on Chart.js v4 + react-chartjs-2. + * + * Registers only the modules it needs (tree-shakeable). + * Designed for small embedded charts inside cards. + */ +import React, { useMemo, useRef } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Filler, + Tooltip, + type ChartOptions, + type ChartData, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; + +// Register once at module level — safe to call multiple times +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler, Tooltip); + +// ── Design tokens ──────────────────────────────────────────────────────────── +const PRIMARY = '#35CD48'; // hsl(128 60% 51%) +const PRIMARY_STROKE = 'rgba(53,205,72,0.50)'; // dimmed line — softer on dark bg +const PRIMARY_FILL = 'rgba(53,205,72,0.06)'; // subtle area fill fallback +const CARD_BG = 'hsl(0,0%,13%)'; +const BORDER_CLR = 'hsl(0,0%,20%)'; +const MUTED = '#6b7280'; + +// ── Types ──────────────────────────────────────────────────────────────────── +export interface SparklinePoint { + value: number; + label: string; +} + +export interface SparklineChartProps { + /** Data points — {value, label} */ + data: SparklinePoint[]; + /** Function to format a raw value into the tooltip body string */ + formatValue?: (v: number) => string; + /** + * Accent colour used for hover dot and tooltip text. + * Defaults to primary green. + */ + color?: string; + /** + * Stroke (line) colour. Defaults to `color` at ~50% opacity for a softer look. + * Pass an explicit rgba string to override. + */ + strokeColor?: string; + /** Fill colour. If omitted, derived from `color`. */ + fillColor?: string; + /** Whether to show grid lines — default false */ + showGrid?: boolean; + /** className applied to the wrapper div */ + className?: string; + /** Explicit height (CSS). Defaults to 100% */ + height?: number | string; +} + +// ── Colour helpers ──────────────────────────────────────────────────────────── +/** Extract [r,g,b] from a hex (#rrggbb) or rgb/rgba(...) string. */ +function toRgb(color: string): [number, number, number] { + const hex = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i); + if (hex) return [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; + const rgb = color.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/); + if (rgb) return [+rgb[1], +rgb[2], +rgb[3]]; + return [53, 205, 72]; // fallback: primary green +} + +/** Build a top-to-bottom fade gradient for the area fill. */ +function createGradient( + ctx: CanvasRenderingContext2D, + chartArea: { top: number; bottom: number }, + color: string, +): CanvasGradient { + const [r, g, b] = toRgb(color); + const grad = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); + grad.addColorStop(0, `rgba(${r},${g},${b},0.12)`); + grad.addColorStop(1, `rgba(${r},${g},${b},0)`); + return grad; +} + +// ── Component ──────────────────────────────────────────────────────────────── +export const SparklineChart = React.memo(({ + data, + formatValue, + color = PRIMARY, + strokeColor, + fillColor = PRIMARY_FILL, + showGrid = false, + className = '', + height = '100%', +}) => { + const chartRef = useRef>(null); + + // Compute a dimmed stroke if the caller didn't provide one explicitly + const resolvedStroke = useMemo(() => { + if (strokeColor) return strokeColor; + if (color === PRIMARY) return PRIMARY_STROKE; + const [r, g, b] = toRgb(color); + return `rgba(${r},${g},${b},0.50)`; + }, [color, strokeColor]); + + const labels = useMemo(() => data.map(d => d.label), [data]); + const values = useMemo(() => data.map(d => d.value), [data]); + + const chartData = useMemo((): ChartData<'line'> => ({ + labels, + datasets: [{ + data: values, + fill: 'start', + backgroundColor: fillColor, // overridden by gradient plugin + borderColor: resolvedStroke, + borderWidth: 1.5, + pointRadius: 0, + pointHoverRadius: 3, + pointHoverBackgroundColor: color, + pointHoverBorderColor: CARD_BG, + pointHoverBorderWidth: 1.5, + tension: 0.5, // smooth cubic interpolation + }], + }), [labels, values, color, resolvedStroke, fillColor]); + + const options = useMemo((): ChartOptions<'line'> => ({ + responsive: true, + maintainAspectRatio: false, + // Disable animation entirely so background refetches don't cause + // the line to re-draw from scratch and "flash". + animation: false, + + plugins: { + legend: { display: false }, + tooltip: { + enabled: true, + mode: 'index', + intersect: false, + backgroundColor: CARD_BG, + borderColor: BORDER_CLR, + borderWidth: 1, + titleColor: MUTED, + titleFont: { family: "'JetBrains Mono', monospace", size: 10 }, + bodyColor: color, + bodyFont: { family: "'JetBrains Mono', monospace", size: 12, weight: 'bold' }, + padding: { x: 10, y: 8 }, + cornerRadius: 8, + callbacks: { + title: (items) => items[0]?.label ?? '', + label: (ctx) => { + const y = ctx.parsed.y ?? 0; + return formatValue + ? formatValue(y) + : y.toLocaleString('en-US', { maximumFractionDigits: 2 }); + }, + }, + }, + }, + + scales: { + x: { + display: false, + grid: { display: false }, + border: { display: false }, + }, + y: { + display: false, + grid: { display: showGrid, color: BORDER_CLR }, + border: { display: false }, + beginAtZero: false, + // Always add headroom so the line never touches the edge — + // even when all values are equal (flat line). + afterDataLimits(scale) { + const range = scale.max - scale.min; + const pad = range > 0 ? range * 0.12 : Math.abs(scale.max) * 0.08 || 1; + scale.min -= pad; + scale.max += pad; + }, + }, + }, + + interaction: { + mode: 'index', + intersect: false, + }, + + elements: { + line: { borderCapStyle: 'round', borderJoinStyle: 'round' }, + }, + }), [color, showGrid, formatValue]); + + // ── Gradient plugin (runs before draw) ─────────────────────────────────── + const gradientPlugin = useMemo(() => ({ + id: 'gradientFill', + beforeDraw(chart: ChartJS) { + const { ctx, chartArea, data: cd } = chart; + if (!chartArea || !cd.datasets[0]) return; + const grad = createGradient(ctx, chartArea, color); + (cd.datasets[0] as any).backgroundColor = grad; + }, + }), [color]); + + if (data.length === 0) { + return ( +
+ No data +
+ ); + } + + return ( +
+ +
+ ); +}); + +SparklineChart.displayName = 'SparklineChart'; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/StatusBadge.tsx b/cmd/rpc/web/wallet-new/src/components/ui/StatusBadge.tsx new file mode 100644 index 000000000..d1342a5fc --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/StatusBadge.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { type VariantProps, cva } from "class-variance-authority"; +import { cx } from "@/ui/cx"; + +const statusBadgeVariants = cva( + "inline-flex items-center rounded-md font-mono font-medium tracking-tight transition-colors", + { + variants: { + status: { + // Transaction statuses + confirmed: "bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20", + pending: "bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20", + failed: "bg-red-500/10 text-red-400 ring-1 ring-red-500/20", + open: "bg-red-500/10 text-red-400 ring-1 ring-red-500/20", + + // Validator statuses + staked: "bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20", + unstaking: "bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20", + paused: "bg-red-500/10 text-red-400 ring-1 ring-red-500/20", + + // Account statuses + liquid: "bg-muted/50 text-muted-foreground ring-1 ring-border/60", + delegated: "bg-primary/10 text-primary ring-1 ring-primary/20", + + // Generic + active: "bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20", + inactive: "bg-muted/50 text-muted-foreground ring-1 ring-border/60", + warning: "bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20", + error: "bg-red-500/10 text-red-400 ring-1 ring-red-500/20", + info: "bg-blue-500/10 text-blue-400 ring-1 ring-blue-500/20", + + // Live indicator + live: "bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20", + }, + size: { + sm: "px-1.5 py-0.5 text-[10px]", + md: "px-2 py-0.5 text-xs", + lg: "px-2.5 py-1 text-xs", + }, + }, + defaultVariants: { + status: "inactive", + size: "md", + }, + } +); + +// Map string status to variant +const statusMap: Record["status"]> = { + // Case-insensitive mapping + confirmed: "confirmed", + pending: "pending", + failed: "failed", + open: "open", + staked: "staked", + unstaking: "unstaking", + paused: "paused", + liquid: "liquid", + delegated: "delegated", + active: "active", + inactive: "inactive", + live: "live", +}; + +export interface StatusBadgeProps + extends Omit, "children">, + VariantProps { + /** Status text to display */ + label?: string; + /** Show pulsing dot indicator */ + pulse?: boolean; +} + +export const StatusBadge = React.memo(({ + className, + status, + size, + label, + pulse = false, + ...props +}) => { + // Auto-detect status from label if not provided + const resolvedStatus = status || statusMap[label?.toLowerCase() ?? ""] || "inactive"; + const displayLabel = label || (status ? status.toString() : "Unknown"); + + return ( + + {pulse && ( + + + + + )} + {displayLabel.charAt(0).toUpperCase() + displayLabel.slice(1)} + + ); +}); + +StatusBadge.displayName = "StatusBadge"; + +export { statusBadgeVariants }; diff --git a/cmd/rpc/web/wallet-new/src/components/ui/Tabs.tsx b/cmd/rpc/web/wallet-new/src/components/ui/Tabs.tsx new file mode 100644 index 000000000..b1a4804be --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/components/ui/Tabs.tsx @@ -0,0 +1,106 @@ +"use client"; + +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cx } from "@/ui/cx"; + +const tabsListVariants = cva("inline-flex items-center justify-center", { + variants: { + variant: { + default: + "h-9 w-fit rounded-lg border border-border/70 bg-secondary/75 p-[3px] text-muted-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]", + clear: "bg-transparent text-foreground h-auto w-full gap-2", + outline: "h-auto w-full gap-2 bg-transparent text-foreground", + wallet: + "h-auto w-full justify-start gap-1 overflow-x-auto rounded-none border-b border-border/70 bg-transparent p-0 text-foreground flex-nowrap", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +const tabsTriggerVariants = cva( + "inline-flex items-center justify-center text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + size: { + default: "px-3 py-2", + sm: "px-3 !py-1", + lg: "px-4 py-3", + }, + variant: { + default: + "h-[calc(100%-1px)] flex-1 rounded-md border border-transparent text-foreground focus-visible:border-ring focus-visible:ring-ring/40 focus-visible:outline-none data-[state=active]:border-border/90 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-[inset_0_1px_0_rgba(255,255,255,0.05),0_0_0_1px_rgba(53,205,72,0.16)]", + clear: + "rounded-lg border border-transparent bg-transparent text-muted-foreground leading-none tracking-normal data-[state=active]:border-border/80 data-[state=active]:bg-secondary/70 data-[state=active]:text-foreground", + outline: + "rounded-lg border border-border/80 bg-muted/65 text-muted-foreground leading-none tracking-normal data-[state=active]:border-primary/42 data-[state=active]:text-primary data-[state=active]:shadow-[0_0_0_1px_rgba(53,205,72,0.18)]", + wallet: + "shrink-0 -mb-[1px] rounded-none border-b-[2px] border-transparent bg-transparent px-4 py-3 pb-3 font-medium text-muted-foreground transition-colors hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-primary", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface TabsListProps + extends React.ComponentProps, + VariantProps {} + +export interface TabsTriggerProps + extends React.ComponentProps, + VariantProps {} + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function TabsList({ className, variant, ...props }: TabsListProps) { + return ( + + ); +} + +function TabsTrigger({ className, variant, size, ...props }: TabsTriggerProps) { + return ( + + ); +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Tabs, TabsList, TabsTrigger, TabsContent }; 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..27fe1b2a0 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/actionForm.ts @@ -0,0 +1,159 @@ +import { resolveTemplatesDeep, template, templateAny } 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; + + // by type + if (f.type === "amount" || NUMERIC_HINTS.has(n)) out[n] = asNum(out[n]); + if (f.type === "switch" || f.type === "option" || f.type === "optionCard") { + const raw = out[n]; + if ( + raw === true || + raw === false || + raw === "true" || + raw === "false" || + raw === 1 || + raw === 0 || + raw === "1" || + raw === "0" || + raw === "on" || + raw === "off" + ) { + out[n] = asBool(raw); + } + } + // by name "hint" (e.g. 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 rawEntry = (action.payload as Record | undefined)?.__raw; + if (rawEntry !== undefined) { + if (typeof rawEntry === "string") return templateAny(rawEntry, ctx); + if (typeof rawEntry === "object" && rawEntry?.value !== undefined) { + return templateAny(rawEntry.value, ctx); + } + return resolveTemplatesDeep(rawEntry, ctx); + } + + const result: Record = {}; + + for (const [key, val] of Object.entries(action.payload || {})) { + if (key === "__raw") continue; + + // case 1: simple string => resolve template + if (typeof val === "string") { + result[key] = templateAny(val, ctx); + continue; + } + + if (typeof val === "object" && val?.value !== undefined) { + let resolved: any = templateAny(val?.value, ctx); + + if (val?.coerce) { + switch (val.coerce) { + case "number": + //@ts-ignore + resolved = Number(resolved); + break; + case "string": + resolved = resolved == null ? "" : String(resolved); + break; + case "boolean": + const resolvedStr = String(resolved).toLowerCase(); + resolved = resolvedStr === "true" || resolvedStr === "1"; + break; + } + } + + result[key] = resolved; + continue; + } + // fallback + result[key] = resolveTemplatesDeep(val, ctx); + } + + 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..f2c3f6844 --- /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, cache: 'no-store' as RequestCache }, 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 } // heuristic + 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..44089ed4d --- /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; // e.g.: "fee" to only take the fee block from /params +}; +export type FeeProviderStatic = { + type: "static"; + data: any; // literal fee object +}; +export type FeeProviderExternal = { + type: "external"; + url: string; + method?: "GET" | "POST"; + headers?: Record; + body?: any; + selector?: string; +}; + +export type FeesConfig = { + denom: string; // e.g.: "{{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 which fee key to use based on the action +const feeKeyForAction = (actionId?: string) => { + // maps to what you have in manifest: 'send'|'stake'|'unstake'... + if (actionId === "send") return "sendFee"; + if (actionId === "stake") return "stakeFee"; + if (actionId === "unstake") return "unstakeFee"; + return "sendFee"; // sensible fallback +}; + +// Apply bucket (multiplier) if defined +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..c6e453c72 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/templater.ts @@ -0,0 +1,282 @@ +import { templateFns } from "./templaterFunctions"; + +const banned = + /(constructor|prototype|__proto__|globalThis|window|document|import|Function|eval)\b/; + +function splitArgs(src: string): string[] { + // split by commas ignoring quotes and nesting <...>, (...), {{...}} + 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; +} + +// evaluates a safe JS expression using context as arguments +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) => any, +): string { + let out = ""; + let i = 0; + while (i < input.length) { + const start = input.indexOf("{{", i); + if (start === -1) { + out += input.slice(i); + break; + } + // text before the block + out += input.slice(i, start); + + // find balanced closing + 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; + } + + // if not closed, copy the rest and exit + if (depth !== 0) { + out += input.slice(start); + break; + } + + const exprRaw = input.slice(start + 2, j - 2); + const replacement = resolver(exprRaw.trim()); + // Convert undefined/null to empty string to avoid "undefined" in output + out += replacement == null ? "" : String(replacement); + i = j; + } + return out; +} + +/** Evaluates an expression: function like fn<...> or data path 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)); // each arg can have nested {{...}} + 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) => { + // if the arg is an expression/template, resolve it; if literal, evaluate it + 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) free JS expression (e.g. form.amount * 0.05, Object.keys(ds)...) + try { + return evalJsExpression(expr, ctx); + } catch { + // 4) normal path: 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..1eb6198a6 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/templaterFunctions.ts @@ -0,0 +1,69 @@ +const resolveHeightInput = (v: any): number => { + if (v == null || v === "") return 0 + if (typeof v === "number") return Number.isFinite(v) ? v : 0 + if (typeof v === "string") { + const n = Number(v) + return Number.isFinite(n) ? n : 0 + } + if (typeof v === "object") { + const candidate = (v as any).height ?? (v as any).latestHeight ?? (v as any).result ?? (v as any).value + const n = Number(candidate) + return Number.isFinite(n) ? n : 0 + } + return 0 +} + +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 }) + }, + resolveHeight: (v: any) => resolveHeightInput(v), + 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..06dc6dd15 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/core/useDs.ts @@ -0,0 +1,91 @@ +// src/core/useDS.ts +import { useQuery, keepPreviousData } 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, + // Keep previous data during refetch to prevent UI flashing + placeholderData: keepPreviousData, + 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/stakingRewardsEvents.ts b/cmd/rpc/web/wallet-new/src/hooks/stakingRewardsEvents.ts new file mode 100644 index 000000000..4337268ad --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/stakingRewardsEvents.ts @@ -0,0 +1,98 @@ +export interface RewardEvent { + eventType: string; + msg?: { + amount?: number; + }; + height: number; + reference?: string; + chainId?: number; + address?: string; +} + +interface FetchRewardEventsInRangeParams { + address: string; + toHeight: number; + secondsPerBlock: number; + hours?: number; + perPage?: number; + maxPages?: number; +} + +type DsFetch = (key: string, ctx?: Record) => Promise; + +export interface RewardRangeResult { + events: RewardEvent[]; + fromHeight: number; + toHeight: number; + blocksInRange: number; +} + +/** + * Fetch reward events for an address in a height range derived from a time window. + * The endpoint is paginated, so we iterate until leaving the range or exhausting pages. + */ +export async function fetchRewardEventsInRange( + dsFetch: DsFetch, + { + address, + toHeight, + secondsPerBlock, + hours = 24, + perPage = 100, + maxPages = 100, + }: FetchRewardEventsInRangeParams +): Promise { + const safeToHeight = Math.max(0, Number(toHeight) || 0); + const safeSecondsPerBlock = Math.max(1, Number(secondsPerBlock) || 1); + const blocksInRange = Math.max(1, Math.round((hours * 60 * 60) / safeSecondsPerBlock)); + const fromHeight = Math.max(0, safeToHeight - blocksInRange); + + if (!address || safeToHeight <= 0) { + return { events: [], fromHeight, toHeight: safeToHeight, blocksInRange }; + } + + const inRangeEvents: RewardEvent[] = []; + + for (let page = 1; page <= maxPages; page += 1) { + const pageEvents = await dsFetch("events.byAddress", { + address, + height: safeToHeight, + page, + perPage, + }); + + if (!Array.isArray(pageEvents) || pageEvents.length === 0) break; + + const rewards = pageEvents.filter( + (event) => + event?.eventType === "reward" && + Number.isFinite(event?.height) && + event.height > 0 + ); + + const rewardsInRange = rewards.filter( + (event) => event.height >= fromHeight && event.height <= safeToHeight + ); + inRangeEvents.push(...rewardsInRange); + + const oldestHeightInPage = rewards.reduce( + (min, event) => Math.min(min, event.height), + Number.POSITIVE_INFINITY + ); + + // Data comes newest -> oldest; once oldest in page is below range, following pages are older. + if (oldestHeightInPage < fromHeight) break; + if (pageEvents.length < perPage) break; + } + + return { + events: inRangeEvents, + fromHeight, + toHeight: safeToHeight, + blocksInRange, + }; +} + +export function sumRewards(events: RewardEvent[]): number { + return events.reduce((sum, event) => sum + (event?.msg?.amount || 0), 0); +} 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..91dfc8f9a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useAccountData.ts @@ -0,0 +1,126 @@ +import { useQuery, keepPreviousData } from '@tanstack/react-query' +import { useConfig } from '@/app/providers/ConfigProvider' +import { useDSFetcher } from "@/core/dsFetch" +import { hasDsKey } from "@/core/dsCore" +import { useAccountsList } from "@/app/providers/AccountsProvider" +import { useMemo } from 'react' + +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() { + // Use granular hook - only re-renders when accounts list changes, not selection + const { accounts, loading: accountsLoading } = useAccountsList() + const dsFetch = useDSFetcher() + const { chain } = useConfig() + + const chainId = chain?.chainId ?? 'chain' + const chainReadyBalances = !!chain && hasDsKey(chain, 'account') + const chainReadyValidators = !!chain && hasDsKey(chain, 'validators') + + // Create stable query key from addresses (sorted, joined string) + const addressesKey = useMemo( + () => accounts.map(a => a.address).sort().join(','), + [accounts] + ) + + // ---- CONSOLIDATED QUERY: Balances + Staking ---- + const accountDataQuery = useQuery({ + queryKey: ['accountData.consolidated', chainId, addressesKey], + enabled: !accountsLoading && accounts.length > 0 && (chainReadyBalances || chainReadyValidators), + staleTime: 30_000, + refetchInterval: 30_000, + retry: 2, + retryDelay: 1000, + // Keep previous data while refetching to avoid flashing + placeholderData: keepPreviousData, + queryFn: async () => { + const result = { + totalBalance: 0, + totalStaked: 0, + balances: [] as AccountBalance[], + stakingData: [] as StakingData[] + } + + // Fetch balances and validators in parallel + const [balancesResult, validatorsResult] = await Promise.all([ + // Balances + chainReadyBalances + ? 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 { + return { address: acc.address, amount: 0, nickname: acc.nickname } + } + }) + ) + : Promise.resolve([] as AccountBalance[]), + + // Validators/Staking + chainReadyValidators + ? dsFetch('validators', {}).catch(() => []) + : Promise.resolve([]) + ]) + + // Process balances + result.balances = balancesResult + result.totalBalance = balancesResult.reduce((s, b) => s + (b.amount || 0), 0) + + // Process staking data + const validatorsList = Array.isArray(validatorsResult) ? validatorsResult : [] + const byAddr = new Map() + for (const v of validatorsList) { + const obj = parseMaybeJson(v) + const key = obj?.address ?? obj?.validatorAddress ?? obj?.operatorAddress + if (key) byAddr.set(String(key), obj) + } + + result.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 } + }) + result.totalStaked = result.stakingData.reduce((s, d) => s + (d.staked || 0), 0) + + return result + } + }) + + // Only show loading on initial load (no data yet), not on background refetches + const isInitialLoading = accountsLoading || (accountDataQuery.isLoading && !accountDataQuery.data) + + return { + totalBalance: accountDataQuery.data?.totalBalance || 0, + totalStaked: accountDataQuery.data?.totalStaked || 0, + balances: accountDataQuery.data?.balances || [], + stakingData: accountDataQuery.data?.stakingData || [], + // loading = only true on initial load (prevents flash on refetch) + loading: isInitialLoading, + // isFetching = true during any fetch (including background) + isFetching: accountDataQuery.isFetching, + error: accountDataQuery.error, + refetch: accountDataQuery.refetch, + // Backward compatibility aliases + refetchBalances: accountDataQuery.refetch, + refetchStaking: accountDataQuery.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..e958a7ac1 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useBalanceChart.ts @@ -0,0 +1,116 @@ +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, + // Keep previous data visible while refetching — prevents skeleton flash + // every time currentHeight changes (every ~10 s). + placeholderData: (prev) => prev, + + 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) + } + + // Get data for each height + 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') { + // Get balances of all addresses at this height + 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') { + // Get staked amounts of all addresses at this height + 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) + } + + // Create appropriate label for hours + 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) + // Add point with value 0 in case of 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..ae56a994b --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useDashboard.ts @@ -0,0 +1,141 @@ +import {useDSInfinite} from "@/core/useDSInfinite"; +import React, {useCallback, 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 [prefilledData, setPrefilledData] = React.useState | undefined>(undefined); + const { manifest ,loading: manifestLoading } = useManifest(); + + + const { selectedAddress, isReady: isAccountReady } = useAccounts() + + + const txSentQuery = useDSInfinite( + 'txs.sent', + {account: {address: selectedAddress}}, + { + enabled: !!selectedAddress && isAccountReady, + refetchIntervalMs: 60_000, + perPage: 20, + } + ) + + const txReceivedQuery = useDSInfinite( + 'txs.received', + {account: {address: selectedAddress}}, + { + enabled: !!selectedAddress && isAccountReady, + refetchIntervalMs: 60_000, + perPage: 20, + } + ) + + const txFailedQuery = useDSInfinite( + 'txs.failed', + {account: {address: selectedAddress}}, + { + enabled: !!selectedAddress && isAccountReady, + refetchIntervalMs: 60_000, + perPage: 20, + } + ) + + + const isTxLoading = txSentQuery.isLoading || txReceivedQuery.isLoading || txFailedQuery.isLoading; + + const hasMoreTxs = + (txSentQuery.hasNextPage ?? false) || + (txReceivedQuery.hasNextPage ?? false) || + (txFailedQuery.hasNextPage ?? false); + + const isFetchingMoreTxs = + txSentQuery.isFetchingNextPage || + txReceivedQuery.isFetchingNextPage || + txFailedQuery.isFetchingNextPage; + + const fetchMoreTxs = useCallback(async () => { + const promises: Promise[] = []; + if (txSentQuery.hasNextPage) promises.push(txSentQuery.fetchNextPage()); + if (txReceivedQuery.hasNextPage) promises.push(txReceivedQuery.fetchNextPage()); + if (txFailedQuery.hasNextPage) promises.push(txFailedQuery.fetchNextPage()); + if (promises.length > 0) await Promise.all(promises); + }, [txSentQuery, txReceivedQuery, txFailedQuery]); + + // Total count hint from the first page of txs.sent (most reliable source for totalCount) + const serverTotalCount = useMemo(() => { + const raw = txSentQuery.data?.pages?.[0]?.raw; + return typeof raw?.totalCount === 'number' ? raw.totalCount : undefined; + }, [txSentQuery.data]); + + const allTxs = useMemo(() => { + const toTx = (i: any, typeOverride?: string, statusOverride?: string): Transaction => ({ + hash: String(i.txHash ?? ''), + type: typeOverride ?? i.transaction?.type ?? '', + amount: i.transaction?.msg?.amount ?? 0, + fee: i.transaction?.fee, + status: statusOverride ?? i.transaction?.status ?? 'Confirmed', + time: i.transaction?.time, + address: i.address, + error: i.error ?? undefined, + }); + + const received = (txReceivedQuery.data?.pages.flatMap(p => p.items) ?? []) + .map(i => toTx(i, 'receive')); + + const sent = (txSentQuery.data?.pages.flatMap(p => p.items) ?? []) + .map(i => toTx(i)); + + const failed = (txFailedQuery.data?.pages.flatMap(p => p.items) ?? []) + .map(i => toTx(i, undefined, 'Failed')); + + // Deduplicate by txHash — priority: failed > sent > received (last write wins) + const byHash = new Map(); + for (const tx of [...received, ...sent, ...failed]) { + if (tx.hash) byHash.set(tx.hash, tx); + } + + return Array.from(byHash.values()).sort((a, b) => b.time - a.time); + + }, [txSentQuery.data, txReceivedQuery.data, txFailedQuery.data]) + + const onRunAction = (action: ManifestAction, actionPrefilledData?: Record) => { + const actions = [action] ; + if (action.relatedActions) { + const relatedActions = manifest?.actions.filter(a => action?.relatedActions?.includes(a.id)) + + if (relatedActions) + actions.push(...relatedActions) + } + setSelectedActions(actions); + setPrefilledData(actionPrefilledData); + setIsActionModalOpen(true); + } + + // Clear prefilledData when modal closes + const handleCloseModal = React.useCallback(() => { + setIsActionModalOpen(false); + setPrefilledData(undefined); + }, []); + + return { + isActionModalOpen, + setIsActionModalOpen: handleCloseModal, + selectedActions, + setSelectedActions, + manifest, + manifestLoading, + isTxLoading, + allTxs, + onRunAction, + prefilledData, + hasMoreTxs, + isFetchingMoreTxs, + fetchMoreTxs, + serverTotalCount, + } +} \ 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..c91ec17d1 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useGovernance.ts @@ -0,0 +1,282 @@ +import { useMemo } from "react"; +import { useDS } from "@/core/useDs"; + +type RpcProposalRecord = Record; +type RpcPollRecord = Record; +type RpcParamsRecord = Record>; + +export interface Proposal { + id: string; + 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; + yesVotes: number; + noVotes: number; + abstainVotes: number; + totalVotes?: number; + votingStartTime?: string; + votingEndTime?: string; + 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; + }; + proposal: string; + endBlock: number; + url: string; + proposalHash?: string; +} + +const POLL_INTERVAL_MS = 4_000; + +const asNumber = (value: unknown, fallback = 0): number => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +}; + +const normalizeDate = (raw: unknown): string => { + const numeric = asNumber(raw, 0); + if (!numeric) return new Date().toISOString(); + // Backend returns microseconds in several responses. + if (numeric > 1e15) return new Date(Math.floor(numeric / 1_000)).toISOString(); + if (numeric > 1e12) return new Date(numeric).toISOString(); + if (numeric > 1e9) return new Date(numeric * 1000).toISOString(); + return new Date().toISOString(); +}; + +const categoryFromType = (type?: string): string => { + const map: Record = { + changeParameter: "Gov", + daoTransfer: "Subsidy", + }; + return map[type ?? ""] ?? "Other"; +}; + +const buildProposalList = (rpcProposals: RpcProposalRecord | undefined): Proposal[] => { + if (!rpcProposals || typeof rpcProposals !== "object") return []; + + return Object.entries(rpcProposals).map(([hash, value]) => { + const proposalData = value?.proposal ?? {}; + const msg = proposalData?.msg ?? {}; + const approve = value?.approve; + + let status: Proposal["status"] = "pending"; + let result: Proposal["result"] = "Pending"; + if (approve === true) { + status = "passed"; + result = "Pass"; + } else if (approve === false) { + status = "rejected"; + result = "Fail"; + } else if (approve == null) { + status = "active"; + result = "Pending"; + } + + const yesPercent = approve === true ? 100 : approve === false ? 0 : 50; + const noPercent = 100 - yesPercent; + const proposer = msg?.signer ?? proposalData?.signature?.publicKey ?? "Unknown"; + const title = + msg?.parameterSpace && msg?.parameterKey + ? `${String(msg.parameterSpace).toUpperCase()}: ${msg.parameterKey}` + : proposalData?.memo || `${proposalData?.type || "Proposal"} ${hash.slice(0, 8)}`; + const description = + msg?.parameterSpace && msg?.parameterKey + ? `Change ${msg.parameterKey} to ${msg.parameterValue}` + : proposalData?.memo || "No description available"; + + return { + id: hash, + hash, + title, + description, + status, + category: categoryFromType(proposalData?.type), + result, + proposer, + submitTime: normalizeDate(proposalData?.time), + endHeight: asNumber(msg?.endHeight, 0), + startHeight: asNumber(msg?.startHeight, 0), + yesPercent, + noPercent, + yesVotes: approve === true ? 1 : 0, + noVotes: approve === false ? 1 : 0, + abstainVotes: 0, + totalVotes: 1, + votingStartTime: msg?.startHeight ? `Height ${msg.startHeight}` : normalizeDate(proposalData?.time), + votingEndTime: msg?.endHeight ? `Height ${msg.endHeight}` : "", + type: proposalData?.type, + msg, + approve, + createdHeight: asNumber(proposalData?.createdHeight, 0), + fee: asNumber(proposalData?.fee, 0), + memo: proposalData?.memo, + time: asNumber(proposalData?.time, 0), + }; + }); +}; + +const buildPollList = ( + rpcPolls: RpcPollRecord | undefined, + rpcProposals: RpcProposalRecord | undefined, +): Poll[] => { + if (!rpcPolls || typeof rpcPolls !== "object") return []; + + return Object.entries(rpcPolls).map(([pollKey, value]) => { + const proposalHash = String(value?.proposalHash ?? ""); + const relatedProposal = + (proposalHash ? rpcProposals?.[proposalHash] : undefined) ?? + rpcProposals?.[pollKey]; + const relatedMsg = relatedProposal?.proposal?.msg ?? {}; + + const accountApprove = asNumber(value?.accounts?.approvedPercent, 0); + const accountReject = asNumber(value?.accounts?.rejectPercent, 0); + const validatorApprove = asNumber(value?.validators?.approvedPercent, 0); + const validatorReject = asNumber(value?.validators?.rejectPercent, 0); + + const yesPercent = (accountApprove + validatorApprove) / 2; + const noPercent = (accountReject + validatorReject) / 2; + const endBlock = asNumber(relatedMsg?.endHeight, 0); + + return { + id: proposalHash || pollKey, + hash: proposalHash || pollKey, + title: pollKey, + description: value?.proposalURL || "Community governance poll", + status: "active", + endTime: endBlock ? `Block ${endBlock}` : "Active", + yesPercent, + noPercent, + accountVotes: { yes: accountApprove, no: accountReject }, + validatorVotes: { yes: validatorApprove, no: validatorReject }, + proposal: pollKey, + endBlock, + url: value?.proposalURL || "", + proposalHash, + }; + }); +}; + +export const useGovernanceData = () => { + const pollsQuery = useDS("gov.poll", {}, { + staleTimeMs: POLL_INTERVAL_MS, + refetchIntervalMs: POLL_INTERVAL_MS, + refetchOnMount: true, + refetchOnWindowFocus: false, + }); + + const proposalsQuery = useDS("gov.proposals", {}, { + staleTimeMs: POLL_INTERVAL_MS, + refetchIntervalMs: POLL_INTERVAL_MS, + refetchOnMount: true, + refetchOnWindowFocus: false, + }); + + const paramsQuery = useDS("params", {}, { + staleTimeMs: POLL_INTERVAL_MS, + refetchIntervalMs: POLL_INTERVAL_MS, + refetchOnMount: true, + refetchOnWindowFocus: false, + }); + + const proposals = useMemo( + () => buildProposalList(proposalsQuery.data), + [proposalsQuery.data], + ); + + const polls = useMemo( + () => buildPollList(pollsQuery.data, proposalsQuery.data), + [pollsQuery.data, proposalsQuery.data], + ); + + return { + proposals, + polls, + params: paramsQuery.data ?? {}, + isLoading: pollsQuery.isLoading || proposalsQuery.isLoading || paramsQuery.isLoading, + isRefetching: pollsQuery.isFetching || proposalsQuery.isFetching || paramsQuery.isFetching, + errors: { + polls: pollsQuery.error, + proposals: proposalsQuery.error, + params: paramsQuery.error, + }, + refetchAll: () => { + void pollsQuery.refetch(); + void proposalsQuery.refetch(); + void paramsQuery.refetch(); + }, + }; +}; + +export const useGovernance = () => { + const { proposals, isLoading } = useGovernanceData(); + return { data: proposals, isLoading }; +}; + +export const useProposal = (proposalId: string) => { + const { proposals, isLoading } = useGovernanceData(); + return { + data: proposals.find((p) => p.id === proposalId || p.hash === proposalId), + isLoading, + }; +}; + +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, + }; + }, + }, + ); +}; 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..85cce7b00 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useHistoryCalculation.ts @@ -0,0 +1,59 @@ +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: currentHeightRaw } = useDS('height', {}, { staleTimeMs: 30_000 }) + + // DS `height` can come as number or object ({ height: number }). + const currentHeight = + typeof currentHeightRaw === "number" + ? currentHeightRaw + : Number(currentHeightRaw?.height ?? 0) + + // 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..9f6046dea --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useMultipleValidatorRewardsHistory.ts @@ -0,0 +1,73 @@ +import { useQuery, keepPreviousData } from '@tanstack/react-query'; +import { useHistoryCalculation, HistoryResult } from './useHistoryCalculation'; +import { useDSFetcher } from "@/core/dsFetch"; +import { useMemo } from 'react'; +import { fetchRewardEventsInRange, sumRewards } from "./stakingRewardsEvents"; + +/** + * 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, secondsPerBlock, isReady } = useHistoryCalculation(); + + // Create stable query key from addresses + const addressesKey = useMemo( + () => addresses.sort().join(','), + [addresses] + ); + + return useQuery({ + queryKey: ['multipleValidatorRewardsHistory', addressesKey, currentHeight], + enabled: addresses.length > 0 && isReady, + staleTime: 60_000, // Increased staleTime since rewards don't change rapidly + refetchInterval: 60_000, + placeholderData: keepPreviousData, + + queryFn: async (): Promise> => { + const results: Record = {}; + + // Fetch rewards for all validators in parallel + const validatorPromises = addresses.map(async (address) => { + try { + const { events } = await fetchRewardEventsInRange(dsFetch, { + address, + toHeight: currentHeight, + secondsPerBlock, + hours: 24, + perPage: 100, + maxPages: 100, + }); + const rewards24h = sumRewards(events); + + results[address] = { + current: rewards24h, + previous24h: 0, + change24h: rewards24h, + changePercentage: 0, + progressPercentage: 100, + rewards24h: rewards24h, + // With a bounded 24h query we only guarantee 24h totals here. + totalRewards: rewards24h + }; + } 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..d345d1b9d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useNodes.ts @@ -0,0 +1,206 @@ +import React from "react"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useConfig } from "@/app/providers/ConfigProvider"; +import { useDS } from "@/core/useDs"; + +type AdminConfigResponse = { + chainId?: number | string; +}; + +const toSafeInt = (value: unknown): number | undefined => { + const n = Number(value); + if (!Number.isFinite(n)) return undefined; + return Math.trunc(n); +}; + +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 fetch the current chain's committeeId from admin.config + */ +export const useChainCommitteeId = () => { + const configQ = useDS( + "admin.config", + {}, + { + staleTimeMs: 5000, + refetchIntervalMs: 10000, + refetchOnWindowFocus: false, + }, + ); + + const committeeId = React.useMemo( + () => toSafeInt(configQ.data?.chainId), + [configQ.data], + ); + + return { + committeeId, + isLoading: configQ.isLoading, + error: configQ.error, + }; +}; + +/** + * 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(); + const { committeeId, isLoading: committeeLoading } = useChainCommitteeId(); + + return useQuery({ + queryKey: ["availableNodes", committeeId], + enabled: typeof committeeId === "number" && committeeId > 0, + queryFn: async (): Promise => { + try { + const [consensusData, peerData] = await Promise.all([ + dsFetch("admin.consensusInfo"), + dsFetch("admin.peerInfo"), + ]); + + const netAddress: string = peerData?.id?.netAddress || "tcp://localhost"; + + let nodeName = netAddress.replace("tcp://", ""); + + if (nodeName !== "localhost" && nodeName.includes("-")) { + nodeName = nodeName + .replace(/-/g, " ") + .replace(/\b\w/g, (l: string) => l.toUpperCase()); + } + + 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 [ + { + id: "current_node", + name: "localhost", + address: "", + isActive: false, + netAddress: "tcp://localhost", + }, + ]; + } + }, + refetchInterval: 10000, + staleTime: 5000, + retry: 1, + placeholderData: keepPreviousData, + }); +}; + +/** + * Hook to fetch all node data for the current node using DS pattern. + * Logs are intentionally excluded — use useNodeLogs() separately so that a + * potentially large text response does not block the fast metrics cycle. + */ +export const useNodeData = (nodeId: string) => { + const dsFetch = useDSFetcher(); + const { data: availableNodes = [] } = useAvailableNodes(); + const { committeeId } = useChainCommitteeId(); + const selectedNode = + availableNodes.find((n) => n.id === nodeId) || availableNodes[0]; + + const hasCommittee = typeof committeeId === "number" && committeeId > 0; + + return useQuery({ + queryKey: ["nodeData", nodeId, committeeId], + enabled: !!nodeId && !!selectedNode && hasCommittee, + queryFn: async (): Promise => { + if (!selectedNode) throw new Error("Node not found"); + + try { + const [ + heightData, + consensusData, + peerData, + resourceData, + validatorSetData, + ] = await Promise.all([ + dsFetch("height"), + dsFetch("admin.consensusInfo"), + dsFetch("admin.peerInfo"), + dsFetch("admin.resourceUsage"), + dsFetch("validatorSet", { height: 0, committeeId: committeeId! }), + ]); + + return { + height: heightData, + consensus: consensusData, + peers: peerData, + resources: resourceData, + logs: "", + validatorSet: validatorSetData, + }; + } catch (error) { + console.error(`Error fetching node data for ${nodeId}:`, error); + throw error; + } + }, + // Poll every 2 s so CPU/RAM/consensus metrics feel near-real-time + refetchInterval: 2000, + staleTime: 1000, + placeholderData: keepPreviousData, + }); +}; + +/** + * Hook to stream node logs independently of the fast metrics cycle. + * Logs can be a large text payload; keeping them in a separate query + * prevents them from blocking consensus/resource updates. + * + * Fetches the admin log endpoint directly (plain text GET) instead of + * going through the DS pipeline so the raw text is never altered by + * JSON normalisation. + */ +export const useNodeLogs = (nodeId: string, isPaused: boolean = false) => { + const { chain } = useConfig(); + const adminBase: string = (chain as Record>)?.rpc?.admin ?? ""; + + return useQuery({ + queryKey: ["nodeLogs", nodeId], + enabled: !!nodeId && !isPaused && !!adminBase, + queryFn: async (): Promise => { + const res = await fetch(`${adminBase}/v1/admin/log`, { + method: "GET", + cache: "no-store", + }); + if (!res.ok) throw new Error(`Log fetch failed: ${res.status}`); + return res.text(); + }, + refetchInterval: isPaused ? false : 1000, + staleTime: 0, + gcTime: 0, + }); +}; diff --git a/cmd/rpc/web/wallet-new/src/hooks/useOrdersData.ts b/cmd/rpc/web/wallet-new/src/hooks/useOrdersData.ts new file mode 100644 index 000000000..e9b6041ca --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useOrdersData.ts @@ -0,0 +1,209 @@ +import React from "react"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { useAccounts } from "@/app/providers/AccountsProvider"; +import { useDS } from "@/core/useDs"; +import { useConfig } from "@/app/providers/ConfigProvider"; + +const DEFAULT_PER_PAGE = 20; +const DEFAULT_POLL_INTERVAL_MS = 6000; + +export type RpcOrder = { + id: string; + committee: number; + data?: string; + amountForSale: number; + requestedAmount: number; + sellerReceiveAddress: string; + buyerSendAddress?: string; + buyerChainDeadline?: number; + sellersSendAddress: string; +}; + +type OrdersResponse = { + pageNumber?: number; + perPage?: number; + results?: RpcOrder[]; + type?: string; + count?: number; + totalPages?: number; + totalCount?: number; +}; + +type AdminConfigResponse = { + chainId?: number | string; +}; + +const toSafeInt = (value: unknown): number | undefined => { + const n = Number(value); + if (!Number.isFinite(n)) return undefined; + return Math.trunc(n); +}; + +const asList = (payload: OrdersResponse | undefined): RpcOrder[] => + Array.isArray(payload?.results) ? payload!.results! : []; + +export const isOrderLocked = (order: RpcOrder): boolean => + !!String(order?.buyerSendAddress ?? "").trim(); + +async function fetchOrdersFromRoot( + rootRpcBase: string, + body: Record +): Promise { + const res = await fetch(`${rootRpcBase}/v1/query/orders`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(`RPC ${res.status}`); + return res.json(); +} + +export function useOrdersData(options?: { + perPage?: number; + pollIntervalMs?: number; +}) { + const perPage = options?.perPage ?? DEFAULT_PER_PAGE; + const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; + + const { selectedAddress, isReady: accountsReady } = useAccounts(); + const { chain } = useConfig(); + + const rootRpcBase = React.useMemo(() => { + return chain?.rpc?.root ?? chain?.rpc?.base ?? ""; + }, [chain?.rpc?.root, chain?.rpc?.base]); + + const configQ = useDS( + "admin.config", + {}, + { + enabled: accountsReady, + staleTimeMs: 5000, + refetchIntervalMs: 10000, + refetchOnWindowFocus: false, + }, + ); + + const committeeId = React.useMemo( + () => toSafeInt(configQ.data?.chainId), + [configQ.data], + ); + + const hasCommittee = typeof committeeId === "number" && committeeId > 0; + const hasSelectedAddress = !!selectedAddress; + + const myOrdersQ = useDS( + "orders.bySeller", + { + account: { address: selectedAddress }, + page: 1, + perPage, + }, + { + enabled: hasSelectedAddress && accountsReady, + staleTimeMs: pollIntervalMs, + refetchIntervalMs: pollIntervalMs, + refetchOnWindowFocus: false, + }, + ); + + const availableOrdersQ = useQuery({ + queryKey: ["rootrpc", "orders.byCommittee", rootRpcBase, committeeId, perPage], + enabled: hasCommittee && !!rootRpcBase, + staleTime: pollIntervalMs, + refetchInterval: pollIntervalMs, + refetchOnWindowFocus: false, + placeholderData: keepPreviousData, + queryFn: () => + fetchOrdersFromRoot(rootRpcBase, { + height: 0, + committee: committeeId, + pageNumber: 1, + perPage, + }), + }); + + const fulfillOrdersQ = useQuery({ + queryKey: [ + "rootrpc", + "orders.byBuyer", + rootRpcBase, + selectedAddress, + committeeId, + perPage, + ], + enabled: hasSelectedAddress && hasCommittee && !!rootRpcBase, + staleTime: pollIntervalMs, + refetchInterval: pollIntervalMs, + refetchOnWindowFocus: false, + placeholderData: keepPreviousData, + queryFn: () => + fetchOrdersFromRoot(rootRpcBase, { + height: 0, + buyerSendAddress: selectedAddress, + committee: committeeId, + pageNumber: 1, + perPage, + }), + }); + + const myOrders = React.useMemo(() => asList(myOrdersQ.data), [myOrdersQ.data]); + + const availableOrders = React.useMemo(() => { + const raw = asList(availableOrdersQ.data); + if (!selectedAddress) return raw.filter((order) => !isOrderLocked(order)); + + return raw.filter((order) => { + const unlocked = !isOrderLocked(order); + const isOwnOrder = + String(order?.sellersSendAddress ?? "").toLowerCase() === + selectedAddress.toLowerCase(); + return unlocked && !isOwnOrder; + }); + }, [availableOrdersQ.data, selectedAddress]); + + const fulfillOrders = React.useMemo( + () => asList(fulfillOrdersQ.data), + [fulfillOrdersQ.data], + ); + + const isLoadingAny = + configQ.isLoading || + (hasSelectedAddress && myOrdersQ.isLoading) || + (hasCommittee && availableOrdersQ.isLoading) || + (hasSelectedAddress && hasCommittee && fulfillOrdersQ.isLoading); + + const hasAnyError = + !!configQ.error || !!myOrdersQ.error || !!availableOrdersQ.error || !!fulfillOrdersQ.error; + + const refetchAll = React.useCallback(async () => { + await Promise.all([ + configQ.refetch(), + myOrdersQ.refetch(), + availableOrdersQ.refetch(), + fulfillOrdersQ.refetch(), + ]); + }, [configQ, myOrdersQ, availableOrdersQ, fulfillOrdersQ]); + + return { + selectedAddress, + committeeId, + hasCommittee, + hasSelectedAddress, + myOrders, + availableOrders, + fulfillOrders, + queries: { + config: configQ, + myOrders: myOrdersQ, + availableOrders: availableOrdersQ, + fulfillOrders: fulfillOrdersQ, + }, + isLoadingAny, + hasAnyError, + refetchAll, + }; +} + 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..807e2e605 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useStakingData.ts @@ -0,0 +1,80 @@ +import { useQuery, keepPreviousData } from '@tanstack/react-query'; +import { useValidators } from './useValidators'; +import { useMultipleValidatorRewardsHistory } from './useMultipleValidatorRewardsHistory'; +import { useDS } from '@/core/useDs'; +import { useAccountsList } from "@/app/providers/AccountsProvider"; +import { useMemo } from 'react'; + +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() { + // Use granular hook - only re-renders when accounts list changes + const { accounts, loading: accountsLoading } = useAccountsList(); + const { data: validators = [], isLoading: validatorsLoading } = useValidators(); + const { data: currentHeight = 0 } = useDS('height', {}, { staleTimeMs: 30_000 }); + const validatorAddresses = useMemo(() => validators.map((v: any) => v.address), [validators]); + const { data: rewardsHistory = {}, isLoading: rewardsLoading } = useMultipleValidatorRewardsHistory(validatorAddresses); + + // Create stable query key + const addressesKey = useMemo( + () => accounts.map(a => a.address).sort().join(','), + [accounts] + ); + const validatorAddressesKey = useMemo( + () => validatorAddresses.sort().join(','), + [validatorAddresses] + ); + + return useQuery({ + queryKey: ['stakingData.computed', addressesKey, validatorAddressesKey, 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, + placeholderData: keepPreviousData, + }); +} diff --git a/cmd/rpc/web/wallet-new/src/hooks/useStakingRewardsChart.ts b/cmd/rpc/web/wallet-new/src/hooks/useStakingRewardsChart.ts new file mode 100644 index 000000000..70cd4ea7e --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useStakingRewardsChart.ts @@ -0,0 +1,141 @@ +import { useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useHistoryCalculation } from "./useHistoryCalculation"; +import { + fetchRewardEventsInRange, + sumRewards, + type RewardEvent, +} from "./stakingRewardsEvents"; + +export interface RewardChartPoint { + index: number; + label: string; + fromHeight: number; + toHeight: number; + amount: number; + cumulative: number; + timestampMs: number; +} + +export interface StakingRewardsChartResult { + address: string; + targetHeight: number; + fromHeight: number; + blocksInRange: number; + totalRewards24h: number; + eventsCount: number; + points: RewardChartPoint[]; +} + +interface UseStakingRewardsChartOptions { + address?: string; + height?: number; + hours?: number; + points?: number; + perPage?: number; + maxPages?: number; + enabled?: boolean; + staleTimeMs?: number; + refetchIntervalMs?: number; +} + +export function useStakingRewardsChart({ + address, + height, + hours = 24, + points = 12, + perPage = 100, + maxPages = 100, + enabled = true, + staleTimeMs = 60_000, + refetchIntervalMs = 60_000, +}: UseStakingRewardsChartOptions = {}) { + const dsFetch = useDSFetcher(); + const { currentHeight, secondsPerBlock, isReady } = useHistoryCalculation(); + + const targetHeight = useMemo( + () => (typeof height === "number" && height > 0 ? height : currentHeight), + [height, currentHeight] + ); + + return useQuery({ + queryKey: [ + "stakingRewardsChart", + address, + targetHeight, + hours, + points, + perPage, + maxPages, + ], + enabled: enabled && !!address && isReady && targetHeight > 0, + staleTime: staleTimeMs, + refetchInterval: refetchIntervalMs, + queryFn: async (): Promise => { + const safeAddress = address || ""; + const safePoints = Math.max(2, Math.floor(points)); + + const { events, fromHeight, toHeight, blocksInRange } = + await fetchRewardEventsInRange(dsFetch, { + address: safeAddress, + toHeight: targetHeight, + secondsPerBlock, + hours, + perPage, + maxPages, + }); + + const totalRewards24h = sumRewards(events); + + const heightSpan = Math.max(1, toHeight - fromHeight + 1); + const bucketSize = Math.max(1, Math.ceil(heightSpan / safePoints)); + const nowMs = Date.now(); + const blocksToMs = (blocks: number) => blocks * secondsPerBlock * 1000; + + const chartPoints: RewardChartPoint[] = []; + let cumulative = 0; + + for (let i = 0; i < safePoints; i += 1) { + const bucketFrom = fromHeight + i * bucketSize; + const bucketTo = + i === safePoints - 1 + ? toHeight + : Math.min(toHeight, bucketFrom + bucketSize - 1); + + const amount = events + .filter((event: RewardEvent) => event.height >= bucketFrom && event.height <= bucketTo) + .reduce((sum, event) => sum + (event.msg?.amount || 0), 0); + + cumulative += amount; + + const blocksAgo = Math.max(0, toHeight - bucketTo); + const hoursAgo = Math.max( + 0, + Math.round((blocksAgo * secondsPerBlock) / (60 * 60)) + ); + const label = i === safePoints - 1 ? "Now" : `${hoursAgo}h`; + + chartPoints.push({ + index: i, + label, + fromHeight: bucketFrom, + toHeight: bucketTo, + amount, + cumulative, + timestampMs: nowMs - blocksToMs(blocksAgo), + }); + } + + return { + address: safeAddress, + targetHeight: toHeight, + fromHeight, + blocksInRange, + totalRewards24h, + eventsCount: events.length, + points: chartPoints, + }; + }, + }); +} 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..0c5f274fd --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useValidatorRewardsHistory.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { useHistoryCalculation, HistoryResult } from './useHistoryCalculation'; +import {useDSFetcher} from "@/core/dsFetch"; +import { fetchRewardEventsInRange, sumRewards } from "./stakingRewardsEvents"; + +/** + * 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, secondsPerBlock, isReady } = useHistoryCalculation(); + + return useQuery({ + queryKey: ['validatorRewardsHistory', address, currentHeight], + enabled: !!address && isReady, + staleTime: 30_000, + + queryFn: async (): Promise => { + const { events } = await fetchRewardEventsInRange(dsFetch, { + address: address || "", + toHeight: currentHeight, + secondsPerBlock, + hours: 24, + perPage: 100, + maxPages: 100, + }); + + const rewardsLast24h = sumRewards(events); + + // 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..7b0a3ec5d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/hooks/useValidators.ts @@ -0,0 +1,85 @@ +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { useDSFetcher } from "@/core/dsFetch"; +import { useAccountsList } from "@/app/providers/AccountsProvider"; +import { useMemo } from "react"; + +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() { + // Use granular hook - only re-renders when accounts list changes + const { accounts, loading: accountsLoading } = useAccountsList(); + const dsFetch = useDSFetcher(); + + // Create stable query key from addresses + const addressesKey = useMemo( + () => accounts.map((a) => a.address).sort().join(","), + [accounts] + ); + + return useQuery({ + queryKey: ["validators.byAccounts", addressesKey], + 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, + placeholderData: keepPreviousData, + }); +} 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..b82c147db --- /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: from context + const { wallets } = (window as any).__configCtx ?? {}; + return (wallets ?? []) as Wallet[]; + + // B: from 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..398dbc8b4 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/index.css @@ -0,0 +1,357 @@ +@import url('https://fonts.googleapis.com/css2?family=Syne:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: 0 0% 9%; + --foreground: 0 0% 98%; + --card: 0 0% 15%; + --card-foreground: 0 0% 99%; + --popover: 0 0% 15%; + --popover-foreground: 0 0% 99%; + --primary: 128 60% 51%; + --primary-foreground: 0 0% 8%; + --secondary: 0 0% 18%; + --secondary-foreground: 0 0% 99%; + --muted: 0 0% 18%; + --muted-foreground: 0 0% 78%; + --accent: 0 0% 20%; + --accent-foreground: 0 0% 99%; + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 100%; + --border: 132 10% 34%; + --input: 132 10% 38%; + --border-accent: 128 34% 52%; + --border-subtle: 132 8% 28%; + --ring: 128 60% 51%; + --radius: 0.625rem; + --sidebar-width: 220px; + --topbar-height: 52px; + --font-display: 'Syne', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --font-body: 'Plus Jakarta Sans', sans-serif; + --surface-base: linear-gradient(145deg, hsl(0 0% 17% / 0.92), hsl(0 0% 13% / 0.95)); + --surface-elevated: linear-gradient(145deg, hsl(0 0% 19% / 0.92), hsl(0 0% 14% / 0.96)); + --surface-sheen: linear-gradient(180deg, hsl(0 0% 100% / 0.12), hsl(0 0% 100% / 0) 42%); + --glow-primary: 0 0 0 1px hsl(var(--primary) / 0.22), 0 0 26px hsl(var(--primary) / 0.25); + --glow-soft: 0 0 12px hsl(var(--primary) / 0.25); +} + +.dark { + --background: 0 0% 9%; + --foreground: 0 0% 98%; + --card: 0 0% 15%; + --card-foreground: 0 0% 99%; + --popover: 0 0% 15%; + --popover-foreground: 0 0% 99%; + --primary: 128 60% 51%; + --primary-foreground: 0 0% 8%; + --secondary: 0 0% 18%; + --secondary-foreground: 0 0% 99%; + --muted: 0 0% 18%; + --muted-foreground: 0 0% 78%; + --accent: 0 0% 20%; + --accent-foreground: 0 0% 99%; + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 100%; + --border: 132 10% 34%; + --input: 132 10% 38%; + --border-accent: 128 34% 52%; + --border-subtle: 132 8% 28%; + --ring: 128 60% 51%; +} + +html, +body, +#root { + height: 100%; + max-width: 100vw; + margin: 0; + padding: 0; + overflow-x: hidden; + font-family: var(--font-body); + line-height: 1.5; +} + +body { + color: hsl(var(--foreground)); + background: + radial-gradient(120% 85% at 84% -8%, hsl(var(--primary) / 0.17), transparent 58%), + radial-gradient(100% 70% at 8% 0%, hsl(var(--primary) / 0.08), transparent 55%), + hsl(var(--background)); + position: relative; +} + +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + outline-color: hsl(var(--ring) / 0.4); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: + linear-gradient(hsl(var(--border) / 0.2) 1px, transparent 1px), + linear-gradient(90deg, hsl(var(--border) / 0.2) 1px, transparent 1px); + background-size: 34px 34px; + mask-image: radial-gradient(circle at 50% 0%, black 0%, transparent 78%); +} + +body::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background: radial-gradient(100% 90% at 50% 100%, transparent 45%, hsl(0 0% 0% / 0.2) 100%); +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + border-radius: 99px; + background: hsl(var(--border) / 0.8); +} + +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--primary) / 0.5); +} + +@layer base { + * { + @apply border-border; + } +} + +.font-display { + font-family: var(--font-display); +} + +.font-mono { + font-family: var(--font-mono); +} + +.font-body { + font-family: var(--font-body); +} + +.surface-panel { + background: var(--surface-base); + border: 1px solid hsl(var(--border) / 0.78); + backdrop-filter: blur(14px); +} + +.surface-card, +.canopy-card { + position: relative; + overflow: hidden; + border-radius: 0.875rem; + border: 1px solid hsl(var(--border) / 0.75); + background: var(--surface-elevated); + box-shadow: + inset 0 1px 0 hsl(0 0% 100% / 0.05), + 0 18px 30px hsl(0 0% 0% / 0.26); + transition: + border-color 220ms ease, + box-shadow 220ms ease, + transform 220ms ease; +} + +.surface-card::before, +.canopy-card::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: var(--surface-sheen); + opacity: 0.75; +} + +.surface-card::after, +.canopy-card::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + border-radius: inherit; + border: 1px solid transparent; + background: linear-gradient(135deg, hsl(var(--primary) / 0.2), transparent 25%) border-box; + -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} + +.surface-card:hover, +.canopy-card:hover { + transform: translateY(-1px); + border-color: hsl(var(--border-accent) / 0.52); + box-shadow: var(--glow-primary), 0 20px 34px hsl(0 0% 0% / 0.28); +} + +.canopy-card-soft { + position: relative; + overflow: hidden; + border-radius: 0.875rem; + border: 1px solid hsl(var(--border) / 0.82); + background: linear-gradient( + 180deg, + hsl(0 0% 100% / 0.08) 0%, + hsl(0 0% 100% / 0.05) 55%, + hsl(0 0% 100% / 0.035) 100% + ); + box-shadow: + inset 0 1px 0 hsl(0 0% 100% / 0.08), + 0 14px 30px hsl(0 0% 0% / 0.3); + backdrop-filter: blur(12px); + transition: + border-color 220ms ease, + box-shadow 220ms ease, + transform 220ms ease; +} + +.canopy-card-soft::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(165deg, hsl(var(--primary) / 0.11), transparent 38%); + opacity: 0.52; +} + +.canopy-card-soft:hover { + transform: translateY(-1px); + border-color: hsl(var(--primary) / 0.38); + box-shadow: + inset 0 1px 0 hsl(0 0% 100% / 0.12), + 0 16px 34px hsl(0 0% 0% / 0.34), + 0 0 0 1px hsl(var(--primary) / 0.16); +} + +.btn-glow { + box-shadow: 0 0 0 0 hsl(var(--primary) / 0); + transition: box-shadow 220ms ease, background-color 220ms ease; +} + +.btn-glow:hover { + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.18), var(--glow-soft); +} + +.nav-item-active { + background: hsl(var(--primary) / 0.14); + color: hsl(var(--primary)); + box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.24); +} + +.num { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +@keyframes pulse-ring { + 0% { + transform: scale(0.7); + opacity: 0.78; + } + 100% { + transform: scale(1.9); + opacity: 0; + } +} + +.pulse-dot::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background: hsl(var(--primary)); + animation: pulse-ring 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.skeleton { + background: linear-gradient( + 90deg, + hsl(var(--muted)) 25%, + hsl(var(--accent)) 50%, + hsl(var(--muted)) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.4s ease infinite; +} + +.text-glow { + text-shadow: 0 0 18px hsl(var(--primary) / 0.46); +} + +@keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } +} + +@keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } +} + +.animate-accordion-down { + animation: accordion-down 0.2s ease-out; +} + +.animate-accordion-up { + animation: accordion-up 0.2s ease-out; +} + +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.scrollbar-hide::-webkit-scrollbar { + width: 0; + height: 0; +} diff --git a/cmd/rpc/web/wallet-new/src/lib/utils/brand.ts b/cmd/rpc/web/wallet-new/src/lib/utils/brand.ts new file mode 100644 index 000000000..247fd405a --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/lib/utils/brand.ts @@ -0,0 +1,39 @@ +const CANOPY_COLORS = [ + "#6EE7B7", + "#38BDF8", + "#C084FC", + "#FBBF24", + "#F472B6", + "#60A5FA", + "#F87171", + "#34D399", + "#A78BFA", + "#F59E0B", +]; + +/** + * Return a pseudo-random color from the palette based on a seed (string/number). + * Falls back to a truly random pick if no seed provided. + */ +export function getCanopyAccent(seed?: string | number): string { + if (seed === undefined || seed === null) { + return CANOPY_COLORS[Math.floor(Math.random() * CANOPY_COLORS.length)]; + } + + const s = seed.toString(); + const hash = s.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0); + return CANOPY_COLORS[hash % CANOPY_COLORS.length]; +} + +/** + * Renders the canopy icon SVG with a configurable fill color. + * The SVG scales to fill its container (uses 100% width/height). + */ +export function canopyIconSvg(color: string): string { + return ``; +} + +export const EXPLORER_NEON_GREEN = "#35cd48"; +export const EXPLORER_NEON_BORDER = "#35cd48"; +export const EXPLORER_ICON_GLOW = + "text-[#35cd48] drop-shadow-[0_0_12px_rgba(53,205,72,0.4)]"; 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..bb7f13154 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/manifest/types.ts @@ -0,0 +1,353 @@ +/* =========================== + * 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; // optional if using 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; + }; + + // Confirmation step (optional and simple) + confirm?: { + title?: string; + summary?: Array<{ label: string; value: string }>; + ctaLabel?: string; + danger?: boolean; + showPayload?: boolean; + payloadSource?: "rpc.payload" | "custom"; + payloadTemplate?: any; // if using custom confirmation template + }; + + // Success configuration + success?: { + message?: string; + links?: Array<{ + label: string; + href: string; + }>; + }; + + auth?: { type: "sessionPassword" | "none" }; + + // Submit (tx or call) + 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 NumberField = FieldBase & { + type: "number"; + min?: number; + max?: number; + step?: number | "any"; + integer?: boolean; +}; + +export type RangeField = FieldBase & { + type: "range"; + min?: number; + max?: number; + step?: number; + showInput?: boolean; + suffix?: string; + marks?: number[]; + presets?: Array<{ label: string; value: 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 }; // e.g. {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 + | NumberField + | RangeField + | SwitchField + | OptionCardField + | OptionField + | TextField + | SelectField + | TableSelectField + | AdvancedSelectField + | DynamicHtml; + +/* =========================== + * Field Features (Ops) + * =========================== */ + +export type FieldOp = + | { id: string; op: "copy"; from: string } // copies the resolved value to clipboard + | { id: string; op: "paste" } // pastes from clipboard to field + | { id: string; op: "set"; field: string; value: string }; // sets a value (e.g. Max) + +/* =========================== + * UI Ops / Events + * =========================== */ + +export type UIOp = + | { op: "fetch"; source: SourceKey } // triggers a refetch/load of DS on open + | { op: "notify"; message: string }; // optional: show toast/notification + +/* =========================== + * Submit (HTTP) + * =========================== */ + +export type Submit = { + base: "rpc" | "admin"; + path: string; // e.g. '/v1/admin/tx-send' + method?: "GET" | "POST"; + headers?: Record; + encoding?: "json" | "text"; + body?: any; // template to resolve or literal value +}; + +/* =========================== + * Sources and Selectors + * =========================== */ + +export type SourceRef = { + // where the data to interpolate comes from + uses: string; + // path within the source (e.g. 'fee.sendFee', 'amount', 'address') + selector?: string; +}; + +// common keys of your current DS; allows free string to grow without touching types +export type SourceKey = + | "account" + | "params" + | "fees" + | "height" + | "validators" + | "activity" + | "txs.sent" + | "txs.received" + | "gov.proposals" + | string; + +/* =========================== + * Fees (optional, the minimum) + * =========================== */ + +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; // e.g. 'fee' within the 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 // e.g. 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..2e003039d --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/state/session.ts @@ -0,0 +1,55 @@ +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' + +type SessionState = { + unlockedUntil: number + password?: string + address?: string + unlock: (address: string, password: string, ttlSec: number) => void + lock: () => void + isUnlocked: () => boolean + getRemainingTime: () => number +} + +// Use sessionStorage for persistence within browser session +export const useSession = create()( + persist( + (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, address: undefined }), + isUnlocked: () => Date.now() < get().unlockedUntil && !!get().password, + getRemainingTime: () => Math.max(0, Math.floor((get().unlockedUntil - Date.now()) / 1000)), + }), + { + name: 'wallet-session', + storage: createJSONStorage(() => sessionStorage), + // Only persist these fields + partialize: (state) => ({ + unlockedUntil: state.unlockedUntil, + password: state.password, + address: state.address, + }), + } + ) +) + +let idleRenewAttached = false + +export function attachIdleRenew(ttlSec: number) { + if (idleRenewAttached) return // Prevent multiple attachments + idleRenewAttached = true + + const renew = () => { + const s = useSession.getState() + if (s.password && s.isUnlocked()) { + useSession.setState({ unlockedUntil: Date.now() + ttlSec * 1000 }) + } + } + ;['click','keydown','mousemove','touchstart'].forEach(e => + window.addEventListener(e, renew, { passive: true }) + ) +} 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..9009e9248 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/DefaultToastItem.tsx @@ -0,0 +1,117 @@ +// 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..c4c870a70 --- /dev/null +++ b/cmd/rpc/web/wallet-new/src/toast/ToastContext.tsx @@ -0,0 +1,138 @@ +// 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-3 right-3 sm:top-4 sm:right-4", + "top-left": "top-3 left-3 sm:top-4 sm:left-4", + "bottom-right": "bottom-3 right-3 sm:bottom-4 sm:right-4", + "bottom-left": "bottom-3 left-3 sm:bottom-4 sm: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..aa18e4535 --- /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 } // optional: 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..f8905f017 --- /dev/null +++ b/cmd/rpc/web/wallet-new/tailwind.config.js @@ -0,0 +1,162 @@ +module.exports = { + content: [ + "./src/**/*.{html,js,ts,jsx,tsx}", + "app/**/*.{ts,tsx}", + "components/**/*.{ts,tsx}", + ], + theme: { + extend: { + colors: { + // Canopy frontend palette: dark neutral surfaces + neon canopy green + canopy: { + 50: '#f2fdf4', + 100: '#defbe3', + 200: '#bcf6c7', + 300: '#8ceb96', + 400: '#45ca46', + 500: '#35cd48', + 600: '#1dd13a', + 700: '#0fa32c', + 800: '#0c7f24', + 900: '#0c5f1e', + 950: '#0e200e', + }, + bg: { + primary: '#171717', + secondary: '#1f1f1f', + tertiary: '#2f2f2f', + accent: '#242424', + }, + text: { + primary: '#ffffff', + secondary: '#ebebeb', + muted: '#b3b3b3', + accent: '#35cd48', + }, + status: { + success: '#45ca46', + 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: '#35CD48', + foreground:'#0f0f0f', + light: '#45CA46', + 50: '#f2fdf4', + 100: '#defbe3', + 200: '#bcf6c7', + 300: '#8ceb96', + 400: '#45ca46', + 500: '#35CD48', + 600: '#1dd13a', + 700: '#0fa32c', + 800: '#0c7f24', + 900: '#0c5f1e', + }, + navbar: '#171717', + back: '#6B7280', + secondary: { + DEFAULT: '#1f1f1f', + foreground:'#ffffff', + }, + destructive: { + DEFAULT: '#ef4444', + foreground:'#ffffff', + }, + muted: { + DEFAULT: '#242424', + foreground:'#b3b3b3', + }, + 'input-surface': '#242424', + accent: { + DEFAULT: '#35cd48', + foreground:'#0f0f0f', + }, + popover: { + DEFAULT: '#1f1f1f', + foreground:'#ffffff', + }, + card: { + DEFAULT: '#262626', + foreground:'#ffffff', + }, + }, + spacing: { + '18': '4.5rem', + '88': '22rem', + '52': '13rem', + '220': '220px', + }, + fontSize: { + 'xs': ['0.6875rem', { lineHeight: '1rem' }], + 'sm': ['0.8125rem', { lineHeight: '1.25rem' }], + 'base':['0.9375rem', { lineHeight: '1.5rem' }], + 'lg': ['1.0625rem', { lineHeight: '1.75rem' }], + 'xl': ['1.1875rem', { lineHeight: '1.75rem' }], + '2xl': ['1.4375rem', { lineHeight: '2rem' }], + '3xl': ['1.8125rem', { lineHeight: '2.25rem' }], + }, + boxShadow: { + 'wallet': '0 4px 24px hsl(220 20% 3% / 0.6)', + 'wallet-lg': '0 12px 40px hsl(220 20% 3% / 0.7)', + 'glow': '0 0 20px hsl(128 60% 51% / 0.18), 0 0 40px hsl(128 60% 51% / 0.08)', + 'glow-sm': '0 0 10px hsl(128 60% 51% / 0.14)', + 'inner-top': 'inset 0 1px 0 hsl(0 0% 100% / 0.06)', + }, + fontFamily: { + display: ['Syne', 'sans-serif'], + mono: ['JetBrains Mono', 'Menlo', 'Consolas', 'monospace'], + body: ['Plus Jakarta Sans', 'sans-serif'], + // keep legacy + inter: ['Plus Jakarta Sans', 'sans-serif'], + }, + borderRadius: { + 'sm': '0.25rem', + DEFAULT: '0.5rem', + 'md': '0.5rem', + 'lg': '0.625rem', + 'xl': '0.75rem', + '2xl': '1rem', + '3xl': '1.25rem', + }, + 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' }, + }, + 'pulse-glow': { + '0%, 100%': { boxShadow: '0 0 8px hsl(128 60% 51% / 0.1)' }, + '50%': { boxShadow: '0 0 20px hsl(128 60% 51% / 0.3)' }, + }, + 'slide-in-left': { + from: { transform: 'translateX(-100%)' }, + to: { transform: 'translateX(0)' }, + }, + 'fade-up': { + from: { opacity: '0', transform: 'translateY(8px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'pulse-glow': 'pulse-glow 2.4s ease-in-out infinite', + 'slide-in-left': 'slide-in-left 0.28s ease-out', + 'fade-up': 'fade-up 0.3s 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..a13d7e3ce --- /dev/null +++ b/cmd/rpc/web/wallet-new/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "moduleResolution": "Bundler", + "noEmit": true, + "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..7348f6c98 --- /dev/null +++ b/cmd/rpc/web/wallet-new/vite.config.ts @@ -0,0 +1,75 @@ +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_WALLET_BASE_PATH env var > production default > development default + const getBasePath = () => { + // If explicitly set via environment variable, use it + if (env.VITE_WALLET_BASE_PATH) { + return env.VITE_WALLET_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: { + dedupe: ["react", "react-dom"], + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".json"], + alias: { + "@": "/src", + }, + }, + plugins: [react()], + build: { + outDir: "out", + assetsDir: "assets", + }, + + // Development server configuration + server: { + port: 5173, + proxy: { + // Proxy /rpc to RPC server + '/rpc': { + target: env.VITE_WALLET_RPC_PROXY_TARGET || 'http://localhost:50002', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/rpc/, ''), + }, + // Proxy /adminrpc to Admin RPC server + '/adminrpc': { + target: env.VITE_WALLET_ADMIN_RPC_PROXY_TARGET || 'http://localhost:50003', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/adminrpc/, ''), + }, + // Proxy /rootrpc to Root Chain RPC server (for cross-chain order queries) + '/rootrpc': { + target: env.VITE_ROOT_WALLET_RPC_PROXY_TARGET || 'http://localhost:50002', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/rootrpc/, ''), + }, + }, + }, + + 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",