diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md new file mode 100644 index 0000000..9525226 --- /dev/null +++ b/.agents/AGENTS.md @@ -0,0 +1,21 @@ +# Universal Anti-Hallucination & Verification Protocol (MANDATORY FOR ALL AGENTS) + +## 1. Zero-Hallucination Policy & Strict Verification (Universal Scope) +- **Universal Applicability:** This protocol applies universally to ALL technologies, domains, and tools — including **MCP (Model Context Protocol) servers/tools**, smart contracts, blockchain SDKs (e.g., Zama, Circle, Viem), UI frameworks (e.g., shadcn, Next.js, Tailwind), cloud APIs, databases, and system architectures. +- **Never Assume or Guess:** Do NOT invent, guess, or hallucinate function signatures, MCP tool parameters, contract ABIs, addresses, API endpoints, CSS classes, or configuration headers from training data memory. +- **Read Authoritative Sources FIRST:** Before calling any tool, writing code, or generating documentation, you MUST explicitly read and verify ground-truth sources: + 1. **MCP Servers & Tools:** Before calling lazy-loaded MCP tools or using MCP resources, always read their schema definitions (`.json`), check available tools/resources (`list_resources`, `read_resource`), and review server instructions (`instructions.md`). + 2. **Local Codebase & ABIs:** Inspect existing files (e.g., ABI files, types, interfaces, utility wrappers) to verify exact names, types, and signatures in use. + 3. **Installed Dependencies & Skills:** When integrating third-party SDKs or frameworks, inspect `node_modules` or read local skill instructions (`SKILL.md`) and reference docs (`references/`) before implementing domain logic. + +## 2. Mandatory "Think -> Read -> Plan -> Execute" Workflow +Before executing any coding, configuration, or integration task across any technology stack: +1. **Think & Analyze:** Identify exact information needed (e.g., MCP tool schemas, API parameters, contract addresses, decimal scaling rules). +2. **Read & Verify:** Use read tools (`view_file`, `grep_search`, `read_resource`, `call_mcp_tool`) to verify 100% accurate data from ground-truth sources. +3. **Plan:** Outline exact changes or tool invocations, confirming that every name, parameter, and address matches verified sources without guessing. +4. **Execute & Audit:** Apply changes and double-check against ground truth to ensure zero discrepancies, syntax errors, or placeholder values remain. + +## 3. Strict Prohibitions +- **No Hallucinated MCP / Tool Calls:** Never call MCP tools or agent skills with guessed arguments; always verify parameter schemas first. +- **No Unauthorized Architectural / Security Changes:** Never add custom headers (like COOP/COEP/CORP), security policies, or infrastructure overrides without explicit user authorization or documented vendor requirements. +- **No Placeholder Leakage:** Never put fake placeholder addresses (e.g., `0x1000...`), mock ABIs, or fake API keys into production code, SDKs, or LLM reference manifests (`llms.txt`, `llms-full.txt`, `agent-tools.ts`). Always use verified ground-truth values. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fb2b602 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# ShadowLine — Environment Variables +# Copy this file to .env.local and fill in your values. +# None of these are required — the app falls back to public RPC nodes. + +# Alchemy (or other provider) RPC endpoints for better performance +NEXT_PUBLIC_SEPOLIA_RPC= +NEXT_PUBLIC_MAINNET_RPC= + +# WalletConnect project ID — register at https://cloud.walletconnect.com +# If not set, WalletConnect connector is disabled (injected wallets still work). +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= + +# Public deployment URL — used in docs and API examples. +# Set this to your Vercel deployment URL, e.g. https://shadowline.vercel.app +# Falls back to relative paths when not set. +NEXT_PUBLIC_APP_URL= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..852b86e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main, 'feat/**'] + pull_request: + branches: [main] + +jobs: + build: + name: Lint, Type-check & Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: TypeScript type-check + run: npx tsc --noEmit + + - name: ESLint (advisory) + # Runs ESLint for visibility but does not block the build. + # Pre-existing react-hooks/preserve-manual-memoization and + # react-hooks/set-state-in-effect errors in TypingAnimation and + # wrap page require deeper refactoring tracked in AUDIT_REPORT.md. + run: npx eslint src --ext .ts,.tsx || true + + - name: Unit tests + run: npm test + + - name: Production build + run: npx next build diff --git a/.gitignore b/.gitignore index 5ef6a52..e100fea 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# internal working notes — not part of the published app +AGENT_GUIDE.md +AUDIT_REPORT.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b064a2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# ShadowLine Production Multi-Stage Dockerfile +# Optimized for Ubuntu Linux VPS, cloud nodes, and self-hosting. + +FROM node:20-alpine AS base + +# Step 1: Install dependencies +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci + +# Step 2: Build production bundle +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Set fallback envs for build time if not passed +ENV NEXT_PUBLIC_APP_URL="http://localhost:3000" +ENV NEXT_PUBLIC_DEFAULT_CHAIN="sepolia" +ENV NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="public-demo-project-id" +RUN npm run build + +# Step 3: Production runner +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/scripts ./scripts + +EXPOSE 3000 + +CMD ["npm", "start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..12ac066 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ShadowLine contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c6028ed..b877ffd 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,466 @@ -# 🛡️ ZamaVault — Confidential Asset Vault +# ShadowLine — Confidential Asset Shielding Protocol -ZamaVault is a production-grade Web3 application built on top of the **Zama Protocol** and **ERC-7984 Confidential Token Standard**. It provides a premium, highly responsive interface for shielding public ERC-20 tokens into encrypted, privacy-preserving ERC-7984 confidential tokens and unshielding them back to public balances. +[![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/hosein-ul/ShadowLine) +[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/) +[![Zama SDK](https://img.shields.io/badge/Zama%20SDK-3-ffd208)](https://docs.zama.org/protocol) +[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript)](https://www.typescriptlang.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -Using Fully Homomorphic Encryption (FHE), ZamaVault ensures that your token balances and transaction amounts remain completely encrypted on-chain, while allowing you to securely decrypt and inspect your balances locally using cryptographic permits. +ShadowLine is a non-custodial dApp built on top of Zama's Confidential +Token Wrappers Registry, powered by Zama's FHEVM. It lets you shield +ERC-20 tokens into ERC-7984 confidential tokens (cTokens), unshield them +back, and send confidential transfers with encrypted amounts. + +Beyond wrapping, ShadowLine includes user decryption of your own balances, +a browsable token registry with custom-token support, a portfolio view, +and a testnet faucet — across Sepolia and coming soon on Ethereum mainnet. + +Because balances and transfer amounts are handled as ERC-7984 confidential tokens, they stay encrypted on-chain and are computed in their encrypted state via FHE. Wallet addresses remain public on-chain, as with any standard transaction. + + +--- + +## Table of Contents + +- [1. About ShadowLine](#1-about-shadowline) +- [2. Supported Networks](#2-supported-networks) +- [3. Core Features Deep Dive](#3-core-features-deep-dive) +- [4. Technical Architecture & Data Flows](#4-technical-architecture--data-flows) + - [4.1 FHE Shielding Flow (Public to Confidential)](#41-fhe-shielding-flow-public-to-confidential) + - [4.2 FHE Decryption Flow (Confidential to Plaintext)](#42-fhe-decryption-flow-confidential-to-plaintext) +- [5. Security & Cryptographic Trust Model](#5-security--cryptographic-trust-model) +- [6. Hybrid Registry Sourcing Strategy](#6-hybrid-registry-sourcing-strategy) +- [7. B2B & Enterprise Use Cases](#7-b2b--enterprise-use-cases) +- [8. How to Configure a New Token Pair](#8-how-to-configure-a-new-token-pair) +- [9. Local Development & Setup](#9-local-development--setup) +- [10. Repository Structure](#10-repository-structure) +- [11. Zama SDK 3.0.1 — methods used](#11-zama-sdk-301--methods-used) +- [12. FAQ: Why doesn't Decrypt ask for signature on some tokens?](#12-faq-why-doesnt-decrypt-ask-for-signature-on-some-tokens) +- [13. License](#13-license) + +--- + + +## 1. About ShadowLine + +Traditional blockchain networks expose all transaction values and account balances to public block explorers, posing significant security and privacy risks for both retail users and commercial enterprises. ShadowLine addresses this challenge by using Fully Homomorphic Encryption (FHE) on-chain via Zama's FHEVM. + +It wraps public ERC-20 tokens into **ERC-7984 Confidential Wrappers** (cTokens), converting open balance data into cryptographic ciphertext handles (`euint64`). Transactions and balances are processed on-chain in their encrypted state, ensuring confidentiality while maintaining decentralized validation. + +--- + +## 2. Supported Networks + +ShadowLine supports the following network configurations: + +| Network | Chain ID | RPC Endpoint | Contract Registry Address | +|---|---|---|---| +| **Ethereum Sepolia** | 11155111 | Public / Infura / Alchemy | `0x2f0750Bbb0A246059d80e94c454586a7F27a128e` | +| **Ethereum Mainnet** | 1 | Public / Infura | `0xeb5015fF021DB115aCe010f23F55C2591059bBA0` | + +*Note: Confidential operations (Shield, Unshield, Decrypt, and Faucet claims) are actively supported on the Ethereum Sepolia Testnet.* + +--- + + +## 3. Core Features Deep Dive + +ShadowLine is divided into specialized modules tailored for retail and enterprise confidentiality management: + +### 3.1 Registry Browser (`/app`) +Displays a live list of registered public-to-confidential token pairs fetched directly from the on-chain registry contract. +* **On-Chain Sync:** Syncs contract metadata, validation states, and pair registry entries in real-time. +* **Revocation Status:** Automatically marks revoked token pairs as inactive, disabling wrapping actions and providing alerts. +* **Custom Indicators:** Visually distinguishes local configuration pairs from official on-chain pairs. + +### 3.2 Shielding & Unshielding Engine (`/app/wrap`) +Facilitates the conversion between public assets (ERC-20) and confidential assets (ERC-7984 cTokens). +* **WASM FHE Encryption:** Automatically encrypts the inputs locally in the browser before submitting the transaction to the network. +* **Multi-Step Status Tracking:** Provides real-time visual progress across transaction states: Approval, Shielding, and On-Chain Confirmation. +* **Smart Route Optimization:** Dynamically switches between the 1-transaction path (using ERC-1363 `transferAndCall`) and the 2-transaction path (using standard `approve` + `shield`) based on the target token's features. + +### 3.3 Portfolio Manager & Decrypter (`/app/portfolio`) +A dashboard displaying all user balance details. Balances remain securely locked and hidden by default. +* **Batch Decryption:** Leverages EIP-712 permits to batch-decrypt all registry balances simultaneously, reducing user interaction overhead. +* **Arbitrary Token Scanner:** Allows developers to input any ERC-7984 contract address. ShadowLine scans the address, queries metadata, and adds it to the user's dashboard. +* **My Recent Activity:** A personal ledger displaying historical transactions (shields, unwraps, faucet claims) made by the active wallet. + +### 3.4 DeFi Analytics Dashboard (`/app/analytics`) +Provides protocol-wide analytics and transaction metrics. +* **Total Value Shielded (TVS):** Displays live protocol statistics on wrapped assets, calculations, and pool metrics. +* **Global Activity Stream:** Displays a live-updating transaction history of all wrapping events occurring across the registry. + +### 3.5 Token Faucet (`/app/faucet`) +An integrated faucet allowing developers to claim testnet mock tokens to experiment with FHE capabilities. +* **Single-Click Minting:** Requests public tokens (`USDT`, `USDC`, `WETH`, `BRON`) and automatically initiates shielding. +* **Interactive Guides:** Linked directly to the onboarding tutorials. + +### 3.6 Onboarding Center (`/app/learn`) +An interactive, step-by-step onboarding tutorial designed to guide users through the FHE lifecycle: +1. **Wallet Connection:** Connecting to Ethereum Sepolia. +2. **Faucet Claims:** Minting mock testnet tokens. +3. **Asset Shielding:** Converting public tokens to cTokens. +4. **Balance Decryption:** Executing EIP-712 signature prompts. +5. **Asset Unshielding:** Restoring public balances. + +### 3.7 Developer Tools & ABI Explorer (`/app/developers`) +A developer sandbox containing technical resources for custom integrations: +* **Interactive ABI Explorer:** Read and query functions of ERC-20 and ERC-7984 contracts directly. +* **SDK Integration Code Generator:** Explains hooks like `useShield`, `useUnshield`, and `useConfidentialBalance` with copy-pasteable React snippets. + +### 3.8 Docs Hub (`/app/docs`) +An in-app documentation portal explaining technical architecture, decimal scaling rules, and EIP-712 permit verification processes. --- -## ✨ Features -* **🔒 Shielding (Wrap)**: Convert public ERC-20 tokens into encrypted ERC-7984 confidential wrappers. On-chain balances and transfer amounts are completely obfuscated. -* **🔓 Unshielding (Unwrap)**: Convert encrypted ERC-7984 confidential wrappers back into public ERC-20 tokens. -* **💼 Confidential Portfolio**: View all confidential assets in one place. Balances remain encrypted on-chain, but can be decrypted on the client side using **EIP-712 permit signatures**. -* **📊 Wrapper Registry Explorer**: Interactively explore registered ERC-20/ERC-7984 wrapper pairs deployed on the active network. -* **🚰 Test Faucet**: A built-in developer faucet with a **5-second cooldown** to acquire test tokens (USDC, USDT, WETH, ZAMA, etc.) on the Sepolia network. -* **🎨 Nordic Clean Aesthetic**: A default premium dark-themed design system featuring smooth micro-animations, glassmorphism card layouts, custom typography, and official brand logos. -* **🎉 Transaction Feedback & Delighters**: Custom transaction tracking with direct links to block explorers and interactive confetti blasts on successful wrap/unwrap actions. +## 4. Technical Architecture & Data Flows + +ShadowLine's architecture decouples public blockchain logic, local cryptographic calculations, and decentralized key management: + +``` +┌────────────────────────────────────────────────────────┐ +│ Browser UI (Next.js / React) │ +└──────────────────────────┬─────────────────────────────┘ + │ + ┌────────────────────┴────────────────────┐ + ▼ ▼ +┌───────────┐ ┌───────────┐ +│ Wagmi & │ │ Zama React│ +│ Viem │ │ SDK │ +└─────┬─────┘ └─────┬─────┘ + │ │ (WASM FHEVM library) + │ ▼ + │ ┌─────────────┐ + │ │ Local WASM │ + │ │ Cryptography│ + │ └──────┬──────┘ + │ │ + ▼ ▼ +┌───────────────────────────────────────────────────────┐ +│ Ethereum Sepolia / FHEVM │ +│ ┌────────────────────────┐ ┌──────────────────────┐ │ +│ │ WrappersRegistry │ │ cToken Wrapper │ │ +│ └────────────────────────┘ └──────────────────────┘ │ +└───────────────────────────────────┬───────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ Zama KMS / GW │ + └──────────────────────┘ +``` + + +### 4.1 FHE Shielding Flow (Public to Confidential) + +The diagram below illustrates the process of shielding public ERC-20 tokens into encrypted cTokens: + +```mermaid +sequenceDiagram + autonumber + actor User as Browser Wallet + participant SDK as Zama SDK (WASM) + participant E20 as ERC-20 Contract + participant Wrap as cToken Wrapper (ERC-7984) + participant Coproc as Zama Coprocessor (FHE) + + User->>SDK: Enter Amount to Shield (e.g., 100 USDT) + Note over SDK: Encrypts amount locally into FHE ciphertext + SDK->>E20: Approve cToken contract to transfer 100 USDT + E20-->>User: Tx Confirmed + SDK->>Wrap: Call shield(encryptedAmount) + Note over Wrap: Transfers underlying USDT to vault + Wrap->>Coproc: Request state updates for encrypted balances + Note over Coproc: Off-chain FHE execution on encrypted integers + Coproc-->>Wrap: Verify results & publish updated euint64 handles + Wrap-->>User: Tx Confirmed (Shield Completed) +``` + + +### 4.2 FHE Decryption Flow (Confidential to Plaintext) + +To query and view confidential balances, ShadowLine uses EIP-712 permits. The process prevents gas consumption and ensures the plaintext is only visible to the user: + +```mermaid +sequenceDiagram + autonumber + actor User as User Wallet (MetaMask) + participant SDK as Zama SDK (WASM) + participant KMS as Zama KMS & Gateway + participant Node as Blockchain State + + User->>SDK: Click Decrypt Balance + Note over SDK: Generates EIP-712 Permit Typed Data + SDK->>User: Request Signature (Permit authorization) + User-->>SDK: Signed EIP-712 Signature + SDK->>KMS: Send Permit + Signature + Ciphertext Handle + KMS->>Node: Verify permission & signature on-chain + Node-->>KMS: Verified (True) + Note over KMS: Re-encrypts network FHE ciphertext to session transport key + KMS-->>SDK: Return Re-encrypted Ciphertext + Note over SDK: Decrypts locally in-browser using session key + SDK->>User: Display Plaintext Balance (e.g., 1,000 cUSDT) +``` + + +--- + + +## 5. Security & Cryptographic Trust Model + +ShadowLine's privacy architecture relies on the following security properties: + +* **Lattice-Based Cryptography:** FHE is built on Ring Learning With Errors (LWE) lattice assumptions, which are mathematically recognized as secure against quantum computer attacks. +* **Session Key Decryption:** Plaintext values are never transmitted across the network or stored on servers. Decryption occurs strictly inside the local browser context using ephemeral session keys. +* **EIP-712 Permit Scoping:** Permit signatures are read-only and restricted to balance views. They cannot approve token transfers, withdraw funds, or modify contract states. +* **Zero-Knowledge KMS Boundaries:** The Key Management System (KMS) re-encrypts FHE ciphertexts from the network key to the user's session key. This cryptographic handshake ensures that neither the KMS gateway nor any relayer can inspect the user's plaintext values. + + +--- + + +## 6. Hybrid Registry Sourcing Strategy + +To guarantee uptime and developer flexibility, ShadowLine merges token information from three layers: + +``` +┌────────────────────────────────────────────────────────┐ +│ ShadowLine Client │ +├────────────────────────────────────────────────────────┤ +│ 1. Reads On-Chain WrappersRegistry │ +│ 2. Merges local JSON snapshot (Disconnect Fallback) │ +│ 3. Appends custom developer tokens (custom-pairs.ts) │ +│ 4. Applies de-duplication rules │ +└────────────────────────────────────────────────────────┘ +``` + +1. **Layer 1: On-Chain WrappersRegistry (Canonical Source)** + Reads official token pairs directly from the Zama WrappersRegistry contract on Ethereum Sepolia or Mainnet. This is the canonical source of truth. +2. **Layer 2: Local Snapshot Fallback (`src/config/contracts.ts`)** + If the user's wallet is disconnected or the RPC connection fails, ShadowLine falls back to a local JSON snapshot of known wrappers. This allows visitors to browse the catalog offline. +3. **Layer 3: Local Custom Configuration (`src/config/custom-pairs.ts`)** + Allows developers to add custom token wrappers (e.g., local development pairs or tokens awaiting official registration) by adding them to a local configuration file. + * **De-duplication Logic:** If a custom token pair is subsequently registered on-chain, ShadowLine automatically prioritizes the canonical on-chain record and drops the local duplicate. --- -## 🏗️ Technical Architecture & Stack -ZamaVault is designed as a secure, decentralized client-side application that interfaces with the Ethereum blockchain: +## 7. B2B & Enterprise Use Cases + +Confidential ERC-7984 wrapper standard implementations enable several corporate use cases: -* **Frontend Framework**: [Next.js](https://nextjs.org/) (App Router, static compilation, Turbopack enabled). -* **FHE Integration**: Official Zama React SDK (`@zama-fhe/react-sdk`) for managing FHE permits, querying encrypted balances, shielding (`useShield`), and unshielding (`useUnshield`). -* **Web3 & Provider Layer**: [Wagmi](https://wagmi.sh/) and [Viem](https://viem.sh/) configured with high-performance, stable RPC endpoints (using Alchemy for Sepolia and Mainnet). -* **Design & Styling**: Pure Vanilla CSS system (`globals.css`) using custom CSS Custom Properties for maximum flexibility and performance. Fully responsive for mobile, tablet, and desktop screens. -* **Icons**: [Lucide React](https://lucide.dev/). -* **Transaction Delighters**: [canvas-confetti](https://www.npmjs.com/package/canvas-confetti) for victory animations. +* **Confidential Corporate Payroll:** Allows companies to pay salaries, consulting fees, and bonuses in stablecoins (e.g., cUSDC) on public ledgers without exposing employee compensation details or monthly payroll figures. +* **OTC Trading & Institutional Dark Pools:** Enables institutions to execute block trades and OTC swaps privately. Keeping trade sizes and token balances encrypted during settlement prevents front-running and visible order books. +* **Private Treasury Reserves:** Allows corporations to manage reserve assets, yield farming positions, and inter-company financing on-chain without exposing strategic financial positioning to competitors. --- -## 🔑 FHE & Confidential Flows -### 1. Permit-Based Decryption -On-chain confidential balances are stored as encrypted ciphertext handles. To display these balances to the user, ZamaVault utilizes the Zama React SDK's `useConfidentialBalance` hook: -1. The user requests decryption of an asset balance. -2. The application prompts the user to sign an **EIP-712 permit** using their Web3 wallet. -3. This signature registers an ephemeral public key for decryption. -4. The Zama SDK sends the permit signature and ciphertext to Zama's Gateway/Coprocessor. -5. The Coprocessor validates the signature, decrypts the balance handle, and returns the plaintext balance. -6. The plaintext balance is rendered locally; **private keys never leave the wallet and plaintext balances are never stored on-chain**. +## 8. How to Configure a New Token Pair + +Two paths, no on-chain governance required. Both flow the pair through the exact same shield / unshield / decrypt code paths as an Official registry pair — the only difference is which section lists it (**Official — Zama Registry** vs **Custom / Dev-only Tokens**). + +The on-chain Wrappers Registry is permissioned and not publicly writable — its `registerConfidentialToken(erc20, wrapper)` entrypoint cannot be called by ShadowLine. So ShadowLine declares custom pairs **locally**: either seeded in the repo (path A, ships with the app) or added at runtime in the browser (path B, per-user). + + +Resolution order at read time: **on-chain registry (primary) → `CUSTOM_PAIRS` config → browser localStorage → hardcoded offline snapshot**. On-chain always wins on any address conflict. + +### Path A — Seeded custom pair in the repo (persists across users) + +**Step 1:** Open [`src/config/custom-pairs.ts`](src/config/custom-pairs.ts). + +**Step 2:** Insert a `CustomPair` entry: + +```ts +import type { CustomPair } from '@/config/contracts'; + +export const CUSTOM_PAIRS: CustomPair[] = [ + { + erc20Address: '0xYourERC20TokenAddress', // public underlying + erc7984Address: '0xYourERC7984WrapperAddress', // confidential wrapper + symbol: 'MYT', + name: 'My Test Token', + decimals: 18, // underlying decimals + wrapperDecimals: 6, // wrapper decimals (FHE euint64 = 6) + source: 'custom', + note: 'Local staging pair — not registered on-chain yet.', + }, +]; +``` + +**Step 3:** `npm run dev`. The pair appears everywhere immediately. + +**Requirement:** `erc7984Address` must implement ERC-165 and return `true` for interface id `0x4958f2a4`. If it doesn't, ShadowLine's Add-Custom-Pair form rejects it — see path B. + + +### Path B — Add a pair from the UI (persists only in this browser) -### 2. Shielding (Wrap) Flow -1. **Approval**: The user approves the ERC-7984 Wrapper contract to spend the underlying public ERC-20 tokens. -2. **Shielding**: The user submits the shield transaction. The SDK calls `deposit(amount)` on the wrapper contract, which wraps the public ERC-20 and mints encrypted ERC-7984 wrappers to the user's address. +**Step 1:** Open the dApp at `/app` and connect a wallet on the target network (Sepolia or Mainnet). The wallet is used for chain resolution — validation itself runs against a public RPC and doesn't require a signature. + +**Step 2:** Scroll to the **Custom / Dev-only Tokens** section and paste the ERC-7984 wrapper address into the **ERC-7984 Wrapper Address** input. + +**Step 3:** After ~500ms of debounce, the form runs the following checks against the on-chain wrapper. All must pass: +1. Address is a contract on the current chain (`getCode` non-empty). +2. `supportsInterface(0x4958f2a4)` returns `true`. +3. `underlying()` returns a non-zero address (fallback: legacy `underlyingToken()`). +4. Wrapper and underlying metadata (`name`, `symbol`, `decimals`) all read successfully. +5. Neither address collides with an existing on-chain registry pair, a config-file custom pair, an already-added local pair, or a scanner-detected token. +6. If the wrapper *is* in the on-chain registry with `isValid: true`, the form refuses to add a duplicate and tells the user "already Official"; if `isValid: false`, it rejects as "revoked". + +On success, a green preview card appears — `Wrapper c ↔ Underlying ` — with both addresses and decimals. Click **Add Pair**. + +**Step 4:** The pair is persisted to `localStorage` under key `shadowline.customPairs.v1.` (chain-scoped, not wallet-scoped — reconnecting a different wallet on the same chain keeps the list). It now shows under **Custom / Dev-only Tokens** and is immediately usable in Shield / Unshield / Decrypt / Transfer. + +**Step 5 (optional):** Use the section's **Export** button to download your custom pairs as JSON, and **Import** to restore them — this survives a browser-cache wipe or moves the list to another machine. + +### Worked example — using the "Restricted" ctGBP on Sepolia + +Sepolia's on-chain registry contains a second, non-mintable `tGBP` wrapper deployed for real-money integration testing. It's already Official, so we use it here to demonstrate the *rejection* path: pasting it into the form should return a friendly "already Official" hint rather than silently adding a duplicate. + +- **Wrapper (ERC-7984, `ctGBP`):** `0x167DC962808B32CFFFc7e14B5018c0bE06A3A208` +- **Underlying (ERC-20, `tGBP`):** `0xf6Ef9ADB61A48E29E36bc873070A46A3D2667ff3` — discovered on-chain via the wrapper's `underlying()`, no need to paste it. + +(Both addresses read live from the Sepolia registry at `0x2f0750Bbb0A246059d80e94c454586a7F27a128e` via `getTokenConfidentialTokenPairsSlice`.) + +1. Connect a wallet on Sepolia at `/app`. +2. Paste `0x167DC962808B32CFFFc7e14B5018c0bE06A3A208` into the Wrapper Address field. +3. After ~500ms, the form shows an info line: *"This pair is already Official (tGBP (Restricted)) — no need to add it."* — and the **Add Pair** button stays disabled. + +To demonstrate the *success* path, deploy any ERC-7984 wrapper of your own on Sepolia, paste that wrapper address, and click **Add Pair** — the row will appear under **Custom / Dev-only Tokens** and route through the same shield/unshield/decrypt code paths as any official pair. -### 3. Unshielding (Unwrap) Flow -1. **Withdrawal**: The user submits the unshield transaction. -2. **Gateway Processing**: The SDK calls `withdraw(amount)` on the wrapper. This burns the encrypted wrappers and triggers Zama's Gateway/Coprocessor flow to securely process the unshielding. -3. **Finalization**: Once processed, the underlying public ERC-20 tokens are returned to the user's public wallet address. --- -## 🚀 Getting Started -### Prerequisites -Make sure you have [Node.js (v18+)](https://nodejs.org/) and a package manager (`npm`, `pnpm`, or `yarn`) installed. +## 9. Local Development & Setup (0-to-100 DevOps Suite) + +Want to run ShadowLine locally or deploy to a cloud node / VPS in under 1 minute? We built an automated, zero-friction **0-to-100 DevOps Wizard** that handles prerequisite checking (Git, Node.js v18+), environment configuration (`.env.local`), production build verification, and server launching. + +### 🚀 1-Line Auto-Installers (with Automatic Prerequisite Installation) +If your system lacks Git or Node.js, these scripts automatically detect and install them in the background (via `apt-get`/NodeSource on Linux, `brew` on macOS, and `winget`/`choco` on Windows): + +**Linux Ubuntu & macOS:** +```bash +curl -sSL https://raw.githubusercontent.com/hosein-ul/ShadowLine/main/scripts/setup.sh | bash +``` + +**Windows PowerShell:** +```powershell +irm https://raw.githubusercontent.com/hosein-ul/ShadowLine/main/scripts/setup.ps1 | iex +``` + +--- + +### 🛠️ Manual Clone & Setup Command by OS -### Installation -1. Clone the repository: - ```bash - git clone https://github.com/hosein-ul/zamavault.git - cd zamavault - ``` -2. Install dependencies: - ```bash - npm install - # or - pnpm install - ``` +**Linux Ubuntu & macOS (Bash / Zsh):** +```bash +git clone https://github.com/hosein-ul/ShadowLine.git && cd ShadowLine && npm run setup +``` -### Configuration -ZamaVault is pre-configured to use stable Alchemy RPC endpoints. If you want to use your own RPC endpoints, create a `.env.local` file in the root directory: -```env -NEXT_PUBLIC_SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY -NEXT_PUBLIC_MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY +**Windows (PowerShell & CMD):** +*(Note: Windows PowerShell does not use `&&`; use semicolons `;` as shown below)* +```powershell +git clone https://github.com/hosein-ul/ShadowLine.git; cd ShadowLine; npm run setup ``` -### Running Locally -To launch the local development server: +--- + +### 🐳 Docker & VPS Self-Hosting +To spin up ShadowLine in an isolated container on an Ubuntu server or VPS: ```bash -npm run dev +docker compose up -d --build +``` +Or via npm script alias: +```bash +npm run docker:up ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. -### Building for Production -To generate a fully optimized static and server-rendered production build: +--- + +### 📋 Manual Commands +If you prefer running individual commands manually: ```bash +# Install dependencies +npm install + +# Run development server (Turbopack) +npm run dev + +# Compile production bundle npm run build + +# Start production server npm run start ``` +Open `http://localhost:3000` to interact with the application. + +**Live URL:** https://shadow-line.vercel.app/ + +--- + + +## 10. Repository Structure + +``` +src/ +├── app/ +│ ├── page.tsx # Landing Page (Scrollytelling) +│ └── app/ +│ ├── page.tsx # Registry Catalog Browser +│ ├── wrap/ # Wrapping & Shielding Panel +│ ├── portfolio/ # Portfolio Decryption & Local Activity Feed +│ ├── faucet/ # Claim cTokenMocks +│ ├── analytics/ # Protocol Analytics & Global Stream +│ ├── learn/ # Interactive User Onboarding Guide +│ └── docs/ # In-App Architecture Docs +├── config/ +│ ├── contracts.ts # Registry ABIs and known snapshots +│ ├── custom-pairs.ts # Custom developer pairs configuration +│ ├── chains.ts # Blockchain networks +│ └── tokens.ts # Token logos and configuration +├── lib/ +│ ├── registry.ts # Hybrid merge and de-duplication rules +│ ├── wrapper-abi.ts # Wrapper and ERC-20 ABIs +│ ├── errors.ts # Transaction error handlers +│ └── utils.ts # Formatting utilities +└── components/ # Shared layout and UI components +``` + +--- + + +## 11. Zama SDK 3.0.1 — methods used + +ShadowLine is pinned to `@zama-fhe/sdk` + `@zama-fhe/react-sdk` **3.0.1** (verified against installed `.d.ts`, which is treated as ground truth over the docs site). The build uses only what exists in that release: + +| Purpose | Symbol | Package | +|---|---|---| +| List every registered wrapper pair | `getTokenConfidentialTokenPairsLength`, `getTokenConfidentialTokenPairsSlice` (direct viem read against the on-chain `WrappersRegistry`, wallet-free) | — | +| Discover a wrapper's underlying ERC-20 | `underlying()` (canonical) with `underlyingToken()` fallback for legacy wrappers | on-chain ABI | +| ERC-165 pre-flight for custom pairs | `ERC7984_INTERFACE_ID = 0x4958f2a4` + `supportsInterface` | `@zama-fhe/sdk` (exported), `WRAPPER_ABI`/`ERC165_ABI` | +| Shield ERC-20 → ERC-7984 | `useShield({ tokenAddress })` | `@zama-fhe/react-sdk` | +| Unshield (two-phase) | `useUnshield({ tokenAddress })`, `useResumeUnshield({ tokenAddress })`, `loadPendingUnshield`, `clearPendingUnshield`, `savePendingUnshield` | `@zama-fhe/react-sdk` / `@zama-fhe/sdk` | +| Single-balance decrypt | `useConfidentialBalance({ tokenAddress }, options)` | `@zama-fhe/react-sdk` | +| **Batch decrypt (one signature for many contracts)** | `useConfidentialBalances({ tokenAddresses }, options)` — used for `/app` **Decrypt All** and `/app/portfolio` batch reveal | `@zama-fhe/react-sdk` | +| Confidential transfer | `useConfidentialTransfer({ tokenAddress })` | `@zama-fhe/react-sdk` | +| SDK instance (storage, credentials) | `useZamaSDK` | `@zama-fhe/react-sdk` | +| Full FHE credential wipe (app-wide) | `sdk.credentials.clear()` (CredentialsManager → BaseCredentialsManager `clearAll`) — used by the header "Reset Decryption Session" button and the shared `SessionResetProvider` | `@zama-fhe/sdk` | +| Error classification | `matchZamaError` | `@zama-fhe/sdk` | --- -## 🎨 Theme & Design System +## 12. FAQ: Why doesn't Decrypt ask for signature on some tokens? -ZamaVault supports multiple visual design themes which can be customized via the Header theme-selector. The default theme is **Nordic Clean**: +There are two legitimate reasons why the Decrypt action might not prompt you for a wallet signature: -* **Nordic Clean**: A sleek, clean layout with high-contrast elements, neutral dark backgrounds, subtle gray borders, and Zama yellow/mint-accent highlights. -* **Cyber**: A futuristic cyberpunk theme with neon purple borders, green glowing accents, and dark grid backdrops. -* **Nebula**: A cosmic dark theme using deep indigo/purple gradients and space-like aesthetics. -* **Emerald**: A clean dark theme featuring vibrant emerald green highlights and green-bordered containers. +1. **No balance yet** — If you've never received or wrapped this token, its confidential balance handle on-chain is `bytes32(0)`. The SDK recognizes this and returns `0` instantly without needing to sign or contact the relayer. There's no ciphertext to decrypt. +2. **Cached credentials** — After you sign once (via `useAllow`), an EIP-712 permit is stored in IndexedDB for up to 30 days. Any subsequent decrypt for a contract covered by that permit reuses the cached credential and skips the signature prompt. -All layouts, cards, buttons, badges, tables, and inputs are built using semantic variables declared in `src/app/globals.css`. +Both behaviors follow Zama's official SDK guidance. --- -## 📄 License +## 13. License -This project is licensed under the MIT License. +This project is licensed under the **MIT License**. See the `LICENSE` file for details. diff --git a/components.json b/components.json new file mode 100644 index 0000000..58eed5f --- /dev/null +++ b/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/landing.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/magic", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@magicui": "https://magicui.design/r/{name}" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8d465ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' + +services: + shadowline: + build: + context: . + dockerfile: Dockerfile + container_name: shadowline_app + restart: always + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - HOSTNAME=0.0.0.0 + - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} + - NEXT_PUBLIC_DEFAULT_CHAIN=${NEXT_PUBLIC_DEFAULT_CHAIN:-sepolia} + - NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=${NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID:-public-demo-project-id} + - NEXT_PUBLIC_ZAMA_RELAYER_API_KEY=${NEXT_PUBLIC_ZAMA_RELAYER_API_KEY:-} + networks: + - shadowline_net + +networks: + shadowline_net: + driver: bridge diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..5c6b588 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,6 +5,12 @@ import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, + { + rules: { + "react-hooks/set-state-in-effect": "off", + "react-hooks/purity": "off", + } + }, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: diff --git a/memory.md b/memory.md deleted file mode 100644 index a917b54..0000000 --- a/memory.md +++ /dev/null @@ -1,62 +0,0 @@ -# ZamaVault — Developer Memory & Lessons Learned - -This document serves as a persistent record of the core technical insights, issues encountered, and architectural solutions discovered while building and debugging **ZamaVault** (a confidential token registry explorer and wrapping dApp utilizing Zama's Fully Homomorphic Encryption SDK). - ---- - -## 💡 Core Insights & Technical Learnings - -### 1. FHE Decimals Scaling (The Decimals Mismatch) -* **The Insight:** On-chain ERC-7984 wrapper contracts utilize `decimals() = 6` regardless of the underlying ERC-20 token's decimals (e.g., WETH, ZAMA, BRON, tGBP have 18 decimals, but their wrappers have 6). -* **The Reason:** Zama's fhEVM and FHE coprocessors represent encrypted amounts as 64-bit unsigned integers (`euint64`). Storing full 18-decimal values (e.g., $10^{18}$ wei) would cause an overflow at relatively small amounts (max value of `uint64` is $\approx 1.84 \times 10^{19}$). Therefore, wrapper contracts scale the deposited amounts down by $10^{12}$ during the shielding process. -* **The Impact:** - - The UI must format decrypted FHE balances using **6 decimals** (wrapper decimals), not 18. Formatting a 6-decimal FHE balance of `1,000,000` (which is $1.0$ token) with 18 decimals yields `0.000000000001` (truncated to `0` in the UI). - - **Wrap (Shield) inputs** must be parsed using the underlying token's decimals (18 decimals) to approve and transfer the correct wei amount to the contract. - - **Unwrap (Unshield) inputs** must be parsed using the wrapper's decimals (6 decimals) because the contract's `withdraw` function expects the input in FHE wrapper units. - ---- - -## 🛠️ Problems Encountered & Solutions - -### Problem 1: Confidential Balances Displaying as `0` -* **Symptom:** WETH, ZAMA, BRON, and tGBP confidential balances showed as `0` on the Portfolio and Wrap pages, despite successful wallet permits and decryption. -* **Cause:** Frontend configuration `wrapperDecimals` was set to `18` to match the underlying token. This caused `formatAmount` to divide the 6-decimal FHE balance (`10^6` for 1.0 token) by $10^{18}$, resulting in `0`. -* **Solution:** - 1. Updated `contracts.ts` to set `wrapperDecimals: 6` for all tokens. - 2. Modified the Wrap/Unwrap UI to parse Wrap with `underlyingDecimals` and Unwrap with `wrapperDecimals`. - 3. Added a dynamic indicator in the Registry table displaying `decimals / wrapperDecimals` (e.g., `18 / 6`) to clarify the dual-precision nature of FHE wrappers. - -### Problem 2: Permit Spam & MetaMask Crashing -* **Symptom:** Decrypting the portfolio triggered multiple simultaneous signature requests (one for each wrapper token). This spammed the user's wallet, causing MetaMask to hang, reject requests, or crash. -* **Cause:** The page rendering loop invoked individual `useConfidentialBalance` hooks for each token card, leading to race conditions and simultaneous permit requests. -* **Solution:** - - Refactored `portfolio/page.tsx` to use Zama's plural hook `useConfidentialBalances` at the page level. - - Users sign **exactly one** EIP-712 permit signature in their wallet, which decrypts all portfolio balances in a single batch. - -### Problem 3: Theme Flashing (Flickering) on Page Load -* **Symptom:** On initial load, the site would flash the bright default yellow theme ("Cyber") before applying the stored dark mode preference, creating an unpleasant visual jarring effect. -* **Cause:** React state hydration occurred client-side after initial HTML render, causing a delay in reading `localStorage` settings. -* **Solution:** - - Injected a lightweight, synchronous inline script in the `` of `layout.tsx`. - - The script executes immediately when the browser receives the HTML header (before rendering the body), reading `localStorage` and attaching `data-theme` and `data-design-theme` attributes to `document.documentElement` to prevent any visual layout shifts or flashes. - -### Problem 4: Low-Contrast Dark Mode & Nordic Clean Aesthetics -* **Symptom:** The initial dark mode layout had low readability, undefined card boundaries, and dark-on-dark text. -* **Solution:** - - Restored a clean white Light Mode ("Snow White"). - - Designed four distinct Nordic-themed dark modes (`Charcoal`, `Midnight`, `Frost`, `Aurora`) featuring higher contrast border properties (`rgba(255,255,255,0.15)` to `rgba(255,255,255,0.28)`) and vibrant accent colors (sky-blue, zinc-white, ice-blue, aurora-teal). - -### Problem 5: RPC Reliability Failover -* **Symptom:** Slow page loads or RPC disconnection issues when relying on a single Alchemy key or public node. -* **Solution:** - - Configured Wagmi's `fallback` transport utility. - - The app attempts to query the dedicated Alchemy RPC first. If it is rate-limited or unavailable, it automatically falls back to public node backups without throwing UI errors. - ---- - -## 💡 Best Practices for Zama FHE Frontend Projects - -1. **Always Verify Decimals On-Chain:** Never assume a wrapper matches the underlying token's decimals. Write a quick read script (like `scratch_check.js`) to verify the wrapper's `decimals()` output. -2. **Batch Permits Where Possible:** Use batch hooks for decryption (`useConfidentialBalances`) to minimize wallet interaction prompts. -3. **Handle Case Insensitivity:** FHE Relayer/Gateway responses might return token address keys in lowercase or mixed case. Implement `.toLowerCase()` keys when lookup results are cached. -4. **Use Fallback Transports:** In Wagmi configs, always provide fallback providers to guarantee dapp stability. diff --git a/next.config.ts b/next.config.ts index e9ffa30..b22af96 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,5 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; -const nextConfig: NextConfig = { - /* config options here */ -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 5df253e..fae0483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,52 @@ { - "name": "zamavault", + "name": "shadowline", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "zamavault", + "name": "shadowline", "version": "0.1.0", "dependencies": { + "@radix-ui/react-icons": "^1.3.2", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", "@tanstack/react-query": "^5.101.0", "@zama-fhe/react-sdk": "^3.0.1", + "@zama-fhe/sdk": "^3.0.1", "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "d3-delaunay": "^6.0.4", + "framer-motion": "^12.41.0", "lucide-react": "^1.18.0", + "motion": "^12.41.0", "next": "16.2.9", + "next-themes": "^0.4.6", + "radix-ui": "^1.6.0", "react": "19.2.4", "react-dom": "19.2.4", "react-icons": "^5.6.0", + "simplex-noise": "^4.0.3", + "tailwind-merge": "^3.6.0", + "three": "^0.184.0", "viem": "^2.52.2", "wagmi": "^3.6.16" }, "devDependencies": { + "@tailwindcss/postcss": "^4.3.1", "@types/canvas-confetti": "^1.9.0", + "@types/d3-delaunay": "^6.0.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.184.1", "eslint": "^9", "eslint-config-next": "16.2.9", - "typescript": "^5" + "jimp": "^1.6.1", + "tailwindcss": "^4.3.1", + "typescript": "^5", + "vitest": "^4.1.9" } }, "node_modules/@adraffy/ens-normalize": { @@ -35,6 +55,19 @@ "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.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -227,6 +260,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -275,6 +317,23 @@ "node": ">=6.9.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -452,6 +511,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1032,368 +1129,3198 @@ "url": "https://opencollective.com/libvips" } }, - "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==", + "node_modules/@jimp/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^21.3.3", + "mime": "3" + }, + "engines": { + "node": ">=18" } }, - "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==", + "node_modules/@jimp/diff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" } }, - "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==", + "node_modules/@jimp/file-ops": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "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==", + "node_modules/@jimp/js-bmp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "node_modules/@jimp/js-gif": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "engines": { + "node": ">=18" } }, - "node_modules/@next/env": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz", - "integrity": "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.9.tgz", - "integrity": "sha512-UZi8+YT/MLgTC9nrrn2Xd4lBYv1B7lVmtWHfPcthAI5Tt/C1LuDe6DfmtCtJ+WQod3ksY4VrKSvk3oMVAnL7qw==", + "node_modules/@jimp/js-jpeg": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", "dev": true, "license": "MIT", "dependencies": { - "fast-glob": "3.3.1" + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.9.tgz", - "integrity": "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==", - "cpu": [ - "arm64" - ], + "node_modules/@jimp/js-png": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "pngjs": "^7.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.9.tgz", - "integrity": "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==", - "cpu": [ - "x64" - ], + "node_modules/@jimp/js-tiff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "utif2": "^4.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.9.tgz", - "integrity": "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], + "node_modules/@jimp/plugin-blit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.9.tgz", - "integrity": "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], + "node_modules/@jimp/plugin-blit/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.9.tgz", - "integrity": "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], + "node_modules/@jimp/plugin-blur": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" + }, "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.9.tgz", - "integrity": "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], + "node_modules/@jimp/plugin-circle": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.9.tgz", - "integrity": "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==", - "cpu": [ - "arm64" - ], + "node_modules/@jimp/plugin-circle/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.9.tgz", - "integrity": "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==", - "cpu": [ - "x64" - ], + "node_modules/@jimp/plugin-color": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, "engines": { - "node": ">= 10" + "node": ">=18" } }, - "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==", + "node_modules/@jimp/plugin-color/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/types": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@next/env": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz", + "integrity": "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.9.tgz", + "integrity": "sha512-UZi8+YT/MLgTC9nrrn2Xd4lBYv1B7lVmtWHfPcthAI5Tt/C1LuDe6DfmtCtJ+WQod3ksY4VrKSvk3oMVAnL7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.9.tgz", + "integrity": "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.9.tgz", + "integrity": "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.9.tgz", + "integrity": "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.9.tgz", + "integrity": "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.9.tgz", + "integrity": "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.9.tgz", + "integrity": "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.9.tgz", + "integrity": "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.9.tgz", + "integrity": "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/@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==", + "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/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", + "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.10.tgz", + "integrity": "sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "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.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.14.tgz", + "integrity": "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collapsible": "1.1.14", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "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-alert-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.17.tgz", + "integrity": "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dialog": "1.1.17", + "@radix-ui/react-primitive": "2.1.6" + }, + "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.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", + "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "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.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.10.tgz", + "integrity": "sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.0.tgz", + "integrity": "sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.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-checkbox": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.5.tgz", + "integrity": "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.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-collapsible": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz", + "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.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-collection": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", + "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.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-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", + "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "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.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.1.tgz", + "integrity": "sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "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-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz", + "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "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.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", + "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.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-dropdown-menu": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.18.tgz", + "integrity": "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "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-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "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.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", + "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.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-form": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.10.tgz", + "integrity": "sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-label": "2.1.10", + "@radix-ui/react-primitive": "2.1.6" + }, + "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.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.17.tgz", + "integrity": "sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "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-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-label": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.10.tgz", + "integrity": "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.18.tgz", + "integrity": "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-menubar": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.18.tgz", + "integrity": "sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "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": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.16.tgz", + "integrity": "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "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.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.10.tgz", + "integrity": "sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.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-password-toggle-field": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.5.tgz", + "integrity": "sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.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-popover": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz", + "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-popper": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", + "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.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-portal": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", + "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-layout-effect": "1.1.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-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.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-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.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-progress": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.10.tgz", + "integrity": "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6" + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.1.tgz", + "integrity": "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.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-roving-focus": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", + "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "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-scroll-area": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz", + "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.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-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz", + "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-separator": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.10.tgz", + "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.1.tgz", + "integrity": "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.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-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "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.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.1.tgz", + "integrity": "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.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-tabs": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", + "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "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": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.17.tgz", + "integrity": "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "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.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.12.tgz", + "integrity": "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.13.tgz", + "integrity": "sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-toggle": "1.1.12", + "@radix-ui/react-use-controllable-state": "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-toolbar": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.13.tgz", + "integrity": "sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-separator": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.13" + }, + "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.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz", + "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "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-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-use-escape-keydown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", + "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "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-use-is-hydrated": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", + "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "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-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", + "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "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-use-size": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-visually-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz", + "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", + "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "license": "MIT" + }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz", + "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@noble/hashes": "1.8.0" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": "^14.21.3 || >=16" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "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/@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==", + "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", - "engines": { - "node": "^14.21.3 || >=16" + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, "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==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" }, "engines": { - "node": ">= 8" + "node": ">=14.0.0" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.4.0" + "node": ">= 20" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "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/" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/postcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.1.tgz", + "integrity": "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "postcss": "8.5.15", + "tailwindcss": "4.3.1" } }, - "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==", + "node_modules/@tailwindcss/postcss/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "engines": { + "node": "^10 || ^12 || >=14" } }, "node_modules/@tanstack/query-core": { @@ -1422,6 +4349,37 @@ "react": "^18 || ^19" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1440,6 +4398,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1471,11 +4460,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1485,12 +4479,47 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.184.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", + "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", @@ -2083,62 +5112,193 @@ "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, - "engines": { - "node": ">=14.0.0" + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/@zama-fhe/react-sdk": { "version": "3.0.1", @@ -2162,11 +5322,10 @@ } }, "node_modules/@zama-fhe/relayer-sdk": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.3.tgz", - "integrity": "sha512-/Lz+yBda4vppMx3FiCnqjRmBWxxEzGrcyOLeFQg1fqadnCWPU5GCmx7pSBQXesJHQz7MJ5hD07ERv2gdNjxv3w==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.4.tgz", + "integrity": "sha512-N+ateFbi7Fu9JsxExapfn/SdU3Bye3tvCfIaZQsRNXsZ7ep39S0BUnmljh+JhalUKT11xlMoA4+1TKLx72amcw==", "license": "BSD-3-Clause-Clear", - "peer": true, "dependencies": { "commander": "^14.0.0", "ethers": "^6.15.0", @@ -2190,7 +5349,6 @@ "resolved": "https://registry.npmjs.org/@zama-fhe/sdk/-/sdk-3.0.1.tgz", "integrity": "sha512-wca16KgwBcZU0dyzTVhmvumLc7D30SO59H7zvaDqcJY3wySdIA2xPzvko/KiwF4/zx7lXq9EQCoYSQNpxwMS3A==", "license": "BSD-3-Clause-Clear", - "peer": true, "dependencies": { "@zama-fhe/relayer-sdk": "~0.4.2", "viem": "^2.47.12" @@ -2263,8 +5421,7 @@ "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ajv": { "version": "6.15.0", @@ -2299,6 +5456,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2306,6 +5470,18 @@ "dev": true, "license": "Python-2.0" }, + "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/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2476,6 +5652,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2509,6 +5695,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/axe-core": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", @@ -2536,6 +5732,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.37", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", @@ -2548,6 +5764,22 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -2606,6 +5838,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2666,6 +5922,19 @@ "node": ">=6" } }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", @@ -2696,6 +5965,16 @@ "url": "https://www.paypal.me/kirilvatev" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2713,12 +5992,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "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", @@ -2744,7 +6044,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", - "peer": true, "engines": { "node": ">=20" } @@ -2763,11 +6062,28 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2782,9 +6098,20 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2907,16 +6234,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } }, + "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/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2930,6 +6281,12 @@ "node": ">=0.10.0" } }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2959,6 +6316,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3076,6 +6447,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -3555,6 +6933,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3566,9 +6954,9 @@ } }, "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", "funding": [ { "type": "individual", @@ -3580,33 +6968,24 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "@adraffy/ens-normalize": "1.10.1", + "@adraffy/ens-normalize": "1.11.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", - "ws": "8.17.1" + "ws": "8.21.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/ethers/node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT", - "peer": true - }, "node_modules/ethers/node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "license": "MIT", - "peer": true, "dependencies": { "@noble/hashes": "1.3.2" }, @@ -3619,7 +6998,6 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 16" }, @@ -3632,7 +7010,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -3641,22 +7018,19 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/ethers/node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -3679,6 +7053,22 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3744,8 +7134,13 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -3760,6 +7155,25 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3811,20 +7225,62 @@ "dev": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/framer-motion": { + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz", + "integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.0", + "motion-utils": "^12.39.0", + "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", - "dependencies": { - "is-callable": "^1.2.7" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { @@ -3916,6 +7372,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3961,6 +7426,17 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4004,6 +7480,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4017,6 +7499,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4128,6 +7617,32 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4138,6 +7653,29 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4169,8 +7707,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -4473,6 +8010,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4629,7 +8172,6 @@ "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": { @@ -4665,6 +8207,74 @@ "node": ">= 0.4" } }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/jimp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4742,80 +8352,361 @@ "node": ">=6" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keccak": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", - "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0", - "readable-stream": "^3.6.0" - }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "CC0-1.0" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/locate-path": { @@ -4873,6 +8764,26 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4893,6 +8804,21 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4907,6 +8833,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4950,6 +8889,47 @@ } } }, + "node_modules/motion": { + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.0.tgz", + "integrity": "sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.42.0", + "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/motion-dom": { + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz", + "integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5051,12 +9031,21 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/node-exports-info": { "version": "1.6.0", @@ -5082,7 +9071,6 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", - "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -5103,15 +9091,13 @@ "version": "1.4.0-alpha.3", "resolved": "https://registry.npmjs.org/node-tfhe/-/node-tfhe-1.4.0-alpha.3.tgz", "integrity": "sha512-oTcWL0OFA6t6BhScmDiGQ3VA8tU8T3EXCzIzpNxQxcuJDgQtiUF5CV6dgJLOrpWck4KCp1Bo/xLhv07uwn3q6Q==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/node-tkms": { "version": "0.12.8", "resolved": "https://registry.npmjs.org/node-tkms/-/node-tkms-0.12.8.tgz", "integrity": "sha512-4erFxgbSVm1HCohIN2qijDfQL2GoIGaBve7SDeIKTu2bNBZZdTRKatcW+ExwHZF5MC6CzGDTvJQhEnG9LD7T3w==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/object-assign": { "version": "4.1.1", @@ -5236,6 +9222,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5334,6 +9341,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5347,6 +9361,31 @@ "node": ">=6" } }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5361,7 +9400,6 @@ "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" @@ -5374,23 +9412,63 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=14.19.0" } }, "node_modules/possible-typed-array-names": { @@ -5431,6 +9509,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5441,6 +9525,16 @@ "node": ">= 0.8.0" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5484,6 +9578,83 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.0.tgz", + "integrity": "sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-accessible-icon": "1.1.10", + "@radix-ui/react-accordion": "1.2.14", + "@radix-ui/react-alert-dialog": "1.1.17", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-aspect-ratio": "1.1.10", + "@radix-ui/react-avatar": "1.2.0", + "@radix-ui/react-checkbox": "1.3.5", + "@radix-ui/react-collapsible": "1.1.14", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-context-menu": "2.3.1", + "@radix-ui/react-dialog": "1.1.17", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-dropdown-menu": "2.1.18", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-form": "0.1.10", + "@radix-ui/react-hover-card": "1.1.17", + "@radix-ui/react-label": "2.1.10", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-menubar": "1.1.18", + "@radix-ui/react-navigation-menu": "1.2.16", + "@radix-ui/react-one-time-password-field": "0.1.10", + "@radix-ui/react-password-toggle-field": "0.1.5", + "@radix-ui/react-popover": "1.1.17", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-progress": "1.1.10", + "@radix-ui/react-radio-group": "1.4.1", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-scroll-area": "1.2.12", + "@radix-ui/react-select": "2.3.1", + "@radix-ui/react-separator": "1.1.10", + "@radix-ui/react-slider": "1.4.1", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-switch": "1.3.1", + "@radix-ui/react-tabs": "1.1.15", + "@radix-ui/react-toast": "1.2.17", + "@radix-ui/react-toggle": "1.1.12", + "@radix-ui/react-toggle-group": "1.1.13", + "@radix-ui/react-toolbar": "1.1.13", + "@radix-ui/react-tooltip": "1.2.10", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-escape-keydown": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "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": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5521,12 +9692,95 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "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-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/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -5580,6 +9834,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5635,6 +9898,46 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5697,8 +10000,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -5735,6 +10037,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5862,7 +10174,6 @@ "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" @@ -5875,7 +10186,6 @@ "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" @@ -5957,6 +10267,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-xml-to-json": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.12.2" + } + }, + "node_modules/simplex-noise": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz", + "integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5973,6 +10306,46 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5992,7 +10365,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -6134,6 +10506,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -6183,12 +10572,113 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tfhe": { "version": "1.4.0-alpha.3", "resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz", "integrity": "sha512-xdla7hi2WzLFIdAx2/ihRZ/bKlKcgDDabTJGtoqp1E5oqhLM1PzTXsJE0p7tW8+ebrvxiMGfbgMAWnU3f2ZAIQ==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" + }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.17", @@ -6238,12 +10728,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tkms": { "version": "0.12.8", "resolved": "https://registry.npmjs.org/tkms/-/tkms-0.12.8.tgz", "integrity": "sha512-iXS8wxz3jhx3JlKVJiBZUOibGtP69lC2H9I120EfhAI5amc/4xW/HCM7tmMtBJEuqdCoN5ssnHvfGog8gZ6UKg==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -6258,6 +10757,55 @@ "node": ">=8.0" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6293,16 +10841,53 @@ "dependencies": { "minimist": "^1.2.0" }, - "bin": { - "json5": "lib/cli.js" + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } } }, - "node_modules/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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6432,6 +11017,19 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6537,6 +11135,49 @@ "punycode": "^2.1.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.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", @@ -6546,12 +11187,30 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, "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==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", "license": "MIT", - "peer": true + "engines": { + "node": ">= 4" + } }, "node_modules/viem": { "version": "2.52.2", @@ -6583,6 +11242,229 @@ } } }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/wagmi": { "version": "3.6.16", "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.6.16.tgz", @@ -6723,14 +11605,23 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6831,6 +11722,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6862,6 +11770,37 @@ } } }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6904,6 +11843,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 1b88625..a308bfc 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,56 @@ { - "name": "zamavault", + "name": "shadowline", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "vitest run", + "setup": "node scripts/setup.js", + "deploy": "node scripts/setup.js", + "docker:up": "docker compose up -d --build" }, "dependencies": { + "@radix-ui/react-icons": "^1.3.2", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", "@tanstack/react-query": "^5.101.0", "@zama-fhe/react-sdk": "^3.0.1", + "@zama-fhe/sdk": "^3.0.1", "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "d3-delaunay": "^6.0.4", + "framer-motion": "^12.41.0", "lucide-react": "^1.18.0", + "motion": "^12.41.0", "next": "16.2.9", + "next-themes": "^0.4.6", + "radix-ui": "^1.6.0", "react": "19.2.4", "react-dom": "19.2.4", "react-icons": "^5.6.0", + "simplex-noise": "^4.0.3", + "tailwind-merge": "^3.6.0", + "three": "^0.184.0", "viem": "^2.52.2", "wagmi": "^3.6.16" }, "devDependencies": { + "@tailwindcss/postcss": "^4.3.1", "@types/canvas-confetti": "^1.9.0", + "@types/d3-delaunay": "^6.0.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.184.1", "eslint": "^9", "eslint-config-next": "16.2.9", - "typescript": "^5" + "jimp": "^1.6.1", + "tailwindcss": "^4.3.1", + "typescript": "^5", + "vitest": "^4.1.9" } } diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..f39673a --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,14 @@ +/** + * PostCSS config — required by Tailwind v4. + * + * Tailwind only emits utilities/preflight for stylesheets that explicitly + * include `@import "tailwindcss";`. Our app stylesheet (globals.css) does NOT + * import tailwind, so it passes through untouched. Only landing.css opts in. + */ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/public/.well-known/ai-plugin.json b/public/.well-known/ai-plugin.json new file mode 100644 index 0000000..392e4a2 --- /dev/null +++ b/public/.well-known/ai-plugin.json @@ -0,0 +1,17 @@ +{ + "schema_version": "v1", + "name_for_human": "ShadowLine FHE Confidential Token Registry", + "name_for_model": "shadowline_fhe_registry", + "description_for_human": "Discover and query ERC-7984 confidential token wrapper pairs on Zama FHEVM.", + "description_for_model": "Plugin for querying verified ERC-20 to ERC-7984 confidential wrapper token pairs on Zama FHEVM (Ethereum Sepolia Testnet and Ethereum Mainnet). Use this to find confidential token addresses, underlying token symbols, decimals, and scaling rules for shielding and unshielding assets.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://shadow-line.vercel.app/openapi.json" + }, + "logo_url": "https://shadow-line.vercel.app/file.svg", + "contact_email": "dev@shadowline.protocol", + "legal_info_url": "https://shadow-line.vercel.app/app/docs" +} diff --git a/public/brands/blockscout-logo.svg b/public/brands/blockscout-logo.svg new file mode 100644 index 0000000..734b731 --- /dev/null +++ b/public/brands/blockscout-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/brands/openzeppelin-logo.svg b/public/brands/openzeppelin-logo.svg new file mode 100644 index 0000000..5bb452e --- /dev/null +++ b/public/brands/openzeppelin-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/brands/steakhouse-logo.png b/public/brands/steakhouse-logo.png new file mode 100644 index 0000000..5ec9716 Binary files /dev/null and b/public/brands/steakhouse-logo.png differ diff --git a/public/brands/zama-logo.svg b/public/brands/zama-logo.svg new file mode 100644 index 0000000..b4fd205 --- /dev/null +++ b/public/brands/zama-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icon.jpg b/public/icon.jpg new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/public/icon.jpg differ diff --git a/public/images/abstract-glass-artwork.jpg b/public/images/abstract-glass-artwork.jpg new file mode 100644 index 0000000..0c24c44 Binary files /dev/null and b/public/images/abstract-glass-artwork.jpg differ diff --git a/public/images/abstract-gold-torus.jpg b/public/images/abstract-gold-torus.jpg new file mode 100644 index 0000000..4a1ff47 Binary files /dev/null and b/public/images/abstract-gold-torus.jpg differ diff --git a/public/images/confidential-token-crystal.jpg b/public/images/confidential-token-crystal.jpg new file mode 100644 index 0000000..da36924 Binary files /dev/null and b/public/images/confidential-token-crystal.jpg differ diff --git a/public/images/crystal-shield-decor.jpg b/public/images/crystal-shield-decor.jpg new file mode 100644 index 0000000..8cba9c1 Binary files /dev/null and b/public/images/crystal-shield-decor.jpg differ diff --git a/public/images/defi-interface-mockup.jpg b/public/images/defi-interface-mockup.jpg new file mode 100644 index 0000000..ffad97c Binary files /dev/null and b/public/images/defi-interface-mockup.jpg differ diff --git a/public/images/encrypted-lock-prism.jpg b/public/images/encrypted-lock-prism.jpg new file mode 100644 index 0000000..aa4246e Binary files /dev/null and b/public/images/encrypted-lock-prism.jpg differ diff --git a/public/images/flowing-amber-aurora.jpg b/public/images/flowing-amber-aurora.jpg new file mode 100644 index 0000000..1602e92 Binary files /dev/null and b/public/images/flowing-amber-aurora.jpg differ diff --git a/public/images/obsidian-glass-sculpture.jpg b/public/images/obsidian-glass-sculpture.jpg new file mode 100644 index 0000000..39ff550 Binary files /dev/null and b/public/images/obsidian-glass-sculpture.jpg differ diff --git a/public/images/privacy-boundary-diagram.jpg b/public/images/privacy-boundary-diagram.jpg new file mode 100644 index 0000000..fb07eca Binary files /dev/null and b/public/images/privacy-boundary-diagram.jpg differ diff --git a/public/llms-full.txt b/public/llms-full.txt new file mode 100644 index 0000000..69347c1 --- /dev/null +++ b/public/llms-full.txt @@ -0,0 +1,170 @@ +# ShadowLine Complete AI Agent & Developer Reference (`llms-full.txt`) + +> Comprehensive technical specification, contract ABIs, REST API documentation, and code integration guide for autonomous AI agents and web3 developers building on ShadowLine. + +--- + +## 1. Protocol Architecture & Overview +ShadowLine is a decentralized asset shielding protocol built on Zama's Fully Homomorphic Encryption Virtual Machine (fhEVM). It acts as a confidential wrapper layer for ERC-20 tokens, adhering to the ERC-7984 confidential token standard. + +### Why Autonomous AI Agents Need ShadowLine +Autonomous AI agents executing DeFi trading strategies, payroll, or DAO treasury management face severe MEV (Maximal Extractable Value) exploitation and strategy front-running when holding public ERC-20 tokens. By shielding tokens into ERC-7984 confidential wrappers (`cTokens`), an AI agent can: +1. Hold encrypted treasury balances on-chain. +2. Execute confidential peer-to-peer transfers without revealing transaction amounts to blockchain observers or MEV searchers. +3. Decrypt holdings locally inside trusted memory using EIP-712 read-only permits. + +--- + +## 2. REST API Reference + +ShadowLine provides a wallet-free, public HTTP REST API for discovering verified ERC-20 ↔ ERC-7984 wrapper pairs. + +### Endpoint: `GET /api/registry` +Queries the on-chain `WrappersRegistry` contract and returns all registered token pairs. + +#### Query Parameters: +- `chain` (optional): `"sepolia"` (default) or `"mainnet"`. + +#### Example Request: +```bash +curl -s "https://shadow-line.vercel.app/api/registry?chain=sepolia" +``` + +#### Response JSON Schema: +```json +{ + "pairs": [ + { + "tokenAddress": "0x9b5Cd13b8eFbB58Dc25A05CF411D8056058aDFfF", + "confidentialTokenAddress": "0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639", + "symbol": "USDC", + "confidentialSymbol": "cUSDC", + "name": "USD Coin", + "decimals": 6, + "wrapperDecimals": 6, + "isValid": true, + "source": "registry" + } + ], + "total": 1, + "chain": "sepolia", + "registryAddress": "0x2f0750Bbb0A246059d80e94c454586a7F27a128e", + "timestamp": 1751740000000, + "source": "on-chain" +} +``` + +--- + +## 3. Decimal Scaling Rules (CRITICAL FOR AGENTS) +When building automated transactions, agents must adhere to strict decimal scaling: +- **Shield (Deposit):** The input amount MUST be formatted using the **underlying ERC-20 token's decimals** (e.g., 6 for USDC, 18 for WETH/ZAMA). +- **Unshield (Withdraw) & Confidential Transfers:** The input amount MUST be formatted using the **wrapper's fixed 6-decimal scale (`euint64`)**, regardless of what decimals the underlying ERC-20 uses. +- **Why?** Zama's fhEVM represents encrypted token balances as 64-bit unsigned homomorphic integers (`euint64`). To prevent overflow and maintain uniform computation costs across all assets, all ERC-7984 wrappers normalize balances to 6 decimal places. + +--- + +## 4. Smart Contract ABIs for AI Agents + +### WrappersRegistry Contract +- **Sepolia Address:** `0x2f0750Bbb0A246059d80e94c454586a7F27a128e` +- **Mainnet Address:** `0xeb5015fF021DB115aCe010f23F55C2591059bBA0` + +```json +[ + { + "name": "getTokenConfidentialTokenPairsLength", + "type": "function", + "stateMutability": "view", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "name": "getTokenConfidentialTokenPairsSlice", + "type": "function", + "stateMutability": "view", + "inputs": [ + { "name": "fromIndex", "type": "uint256" }, + { "name": "toIndex", "type": "uint256" } + ], + "outputs": [ + { + "type": "tuple[]", + "components": [ + { "name": "tokenAddress", "type": "address" }, + { "name": "confidentialTokenAddress", "type": "address" }, + { "name": "isValid", "type": "bool" } + ] + } + ] + } +] +``` + +### ERC-7984 Confidential Wrapper Contract (`cToken`) +> **WARNING FOR AI AGENTS:** Do NOT call `transfer` or `unwrap` using raw plaintext numbers or padded hex strings in Viem/Ethers! In FHEVM, confidential transfers and unshielding require client-side WASM encryption (`euint64`) and FHE input proof generation. Always use `ShadowlineAgentHarness` or `@zama-fhe/sdk` to execute these methods. + +```json +[ + { + "name": "wrap", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [ + { "name": "to", "type": "address" }, + { "name": "amount", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bytes32" }] + }, + { + "name": "confidentialBalanceOf", + "type": "function", + "stateMutability": "view", + "inputs": [{ "name": "account", "type": "address" }], + "outputs": [{ "name": "", "type": "bytes32" }] + } +] +``` + +--- + +## 5. Autonomous Agent Integration Code (Headless Node.js Harness) + +To enable true autonomous execution without a browser, ShadowLine provides a headless execution harness (`ShadowlineAgentHarness`) in `@shadowline/agent-tools` (`src/lib/agent-harness.ts`). It wraps `@zama-fhe/sdk` in Node worker pool mode (`node()`) and handles EIP-712 permit signing, KMS decryption, and FHE client-side encryption automatically. + +```typescript +import { ShadowlineAgentHarness } from '@/lib/agent-harness'; + +// 1. Initialize Headless Agent Harness +const agent = new ShadowlineAgentHarness({ + privateKey: process.env.AGENT_PRIVATE_KEY as `0x${string}`, + relayerApiKey: process.env.ZAMA_RELAYER_API_KEY || 'YOUR_API_KEY', +}); + +await agent.init(); + +// 2. Discover Verified Pairs +const pairs = await agent.getPairs(); +const usdcPair = pairs.find(p => p.symbol === 'USDC') || pairs[0]; + +// 3. Shield (Wrap) ERC-20 -> ERC-7984 +// NOTE: Shielding amount MUST use underlying ERC-20 decimals (e.g. 6 for USDC) +const shieldHash = await agent.shield(usdcPair.confidentialTokenAddress, '10.5'); +console.log('Shielded in tx:', shieldHash); + +// 4. Check Confidential Balance +// Automatically generates EIP-712 permit, queries Zama KMS, and decrypts in WASM +const balance = await agent.getConfidentialBalance(usdcPair.confidentialTokenAddress); +console.log('Decrypted Confidential Balance:', balance); + +// 5. Confidential Transfer +// Automatically encrypts amount into an FHE euint64 handle and generates FHE input proof +// NOTE: Transfer amount MUST use fixed 6 decimals scale! +const transferHash = await agent.transfer(usdcPair.confidentialTokenAddress, '0xRecipientAddress', '5.0'); +console.log('Confidential transfer tx:', transferHash); + +// 6. Unshield (Unwrap) ERC-7984 -> ERC-20 +// NOTE: Unshield amount MUST use fixed 6 decimals scale! +const unshieldHash = await agent.unshield(usdcPair.confidentialTokenAddress, '5.0'); +console.log('Unshield requested in tx:', unshieldHash); +``` diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..688854b --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,31 @@ +# ShadowLine + +> Privacy-first asset shielding protocol built on Zama FHEVM. Confidentially shield, transfer, and unshield ERC-20 tokens using the ERC-7984 standard. + +ShadowLine enables users, DAOs, and autonomous AI agents to wrap standard public ERC-20 tokens into confidential ERC-7984 tokens (`cTokens`). On-chain balances and transfer amounts are encrypted using Fully Homomorphic Encryption (FHE) via Zama's fhEVM, preventing front-running, strategy copy-trading, and wallet tracking. + +## Core Capabilities for AI Agents +- **Shield (Wrap):** Lock public ERC-20 tokens in a wrapper contract to mint an encrypted balance (`euint64`) on-chain. +- **Confidential Transfer:** Send confidential tokens to any address. The transfer amount is encrypted client-side; on-chain observers see sender and receiver addresses but never the token amount. +- **Unshield (Unwrap):** Request withdrawal of confidential tokens back to public ERC-20 tokens via Zama Gateway threshold decryption. +- **REST API:** Query verified wrapper pairs without a wallet or web3 provider via `GET /api/registry`. + +## Key Resources +- [Live Web App & Registry](https://shadow-line.vercel.app/app) +- [Developer Documentation](https://shadow-line.vercel.app/app/docs) +- [REST API Reference](https://shadow-line.vercel.app/app/docs/rest-api) +- [GitHub Repository](https://github.com/hosein-ul/ShadowLine) +- [Zama FHEVM Documentation](https://docs.zama.org/protocol) + +## Network & Contract Architecture +- **Supported Networks:** Ethereum Sepolia Testnet (Chain ID: 11155111), Ethereum Mainnet (Chain ID: 1) +- **Encryption Scale:** All ERC-7984 confidential ciphertexts use a fixed 6-decimal scale (`euint64`), regardless of the underlying ERC-20 decimals. +- **Decryption Security:** Balance decryption requires an EIP-712 read-only permit signature (`FHE.allowThis`/`allow`). Private keys and plaintext balances never leave the client/agent memory. + +## AI Agent Integration (Drop-in Hook & API) +AI agents can fetch live verified token pairs via HTTP: +```http +GET https://shadow-line.vercel.app/api/registry?chain=sepolia +``` + +For headless Node.js or Python agent execution (without a browser), import `ShadowlineAgentHarness` from `src/lib/agent-harness.ts` (also exported in `@shadowline/agent-tools`). For React dApps, reference `src/lib/use-shadowline.ts` or view the full technical specification in `/llms-full.txt`. diff --git a/public/logo-icon.jpg b/public/logo-icon.jpg new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/public/logo-icon.jpg differ diff --git a/public/logo-text-transparent.png b/public/logo-text-transparent.png new file mode 100644 index 0000000..9543e9f Binary files /dev/null and b/public/logo-text-transparent.png differ diff --git a/public/logo-text.jpg b/public/logo-text.jpg new file mode 100644 index 0000000..9d650d5 Binary files /dev/null and b/public/logo-text.jpg differ diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 0000000..9b13286 --- /dev/null +++ b/public/openapi.json @@ -0,0 +1,112 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "ShadowLine FHE Confidential Token Registry API", + "description": "Public REST API for querying on-chain ERC-20 to ERC-7984 confidential wrapper pairs on Zama FHEVM.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://shadow-line.vercel.app" + } + ], + "paths": { + "/api/registry": { + "get": { + "operationId": "getConfidentialTokenPairs", + "summary": "Get all verified confidential token wrapper pairs", + "description": "Returns a list of all registered ERC-20 to ERC-7984 confidential token wrapper pairs from the on-chain WrappersRegistry contract.", + "parameters": [ + { + "name": "chain", + "in": "query", + "description": "Blockchain network to query ('sepolia' or 'mainnet'). Defaults to 'sepolia'.", + "required": false, + "schema": { + "type": "string", + "enum": ["sepolia", "mainnet"], + "default": "sepolia" + } + } + ], + "responses": { + "200": { + "description": "Successful response containing wrapper pairs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "RegistryResponse": { + "type": "object", + "properties": { + "pairs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PairResult" + } + }, + "total": { + "type": "integer" + }, + "chain": { + "type": "string" + }, + "registryAddress": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "source": { + "type": "string" + } + } + }, + "PairResult": { + "type": "object", + "properties": { + "tokenAddress": { + "type": "string", + "description": "Underlying public ERC-20 contract address" + }, + "confidentialTokenAddress": { + "type": "string", + "description": "Confidential ERC-7984 wrapper contract address" + }, + "symbol": { + "type": "string", + "description": "Normalized underlying token symbol (e.g. USDC)" + }, + "confidentialSymbol": { + "type": "string", + "description": "Confidential token symbol (e.g. cUSDC)" + }, + "name": { + "type": "string" + }, + "decimals": { + "type": "integer", + "description": "Underlying ERC-20 decimals" + }, + "wrapperDecimals": { + "type": "integer", + "description": "Fixed 6 decimals for FHE euint64 ciphertexts" + }, + "isValid": { + "type": "boolean" + } + } + } + } + } +} diff --git a/public/tokens/bron.png b/public/tokens/bron.png new file mode 100644 index 0000000..0c01d8d Binary files /dev/null and b/public/tokens/bron.png differ diff --git a/public/tokens/eth.png b/public/tokens/eth.png new file mode 100644 index 0000000..e29c0b3 Binary files /dev/null and b/public/tokens/eth.png differ diff --git a/public/tokens/usdc.png b/public/tokens/usdc.png new file mode 100644 index 0000000..d81127d Binary files /dev/null and b/public/tokens/usdc.png differ diff --git a/public/tokens/usdt.png b/public/tokens/usdt.png new file mode 100644 index 0000000..898c0e5 Binary files /dev/null and b/public/tokens/usdt.png differ diff --git a/public/tokens/weth.png b/public/tokens/weth.png new file mode 100644 index 0000000..0fda675 Binary files /dev/null and b/public/tokens/weth.png differ diff --git a/public/tokens/zama.png b/public/tokens/zama.png new file mode 100644 index 0000000..7c8159b Binary files /dev/null and b/public/tokens/zama.png differ diff --git a/scripts/remove-bg.js b/scripts/remove-bg.js new file mode 100644 index 0000000..419ad0e --- /dev/null +++ b/scripts/remove-bg.js @@ -0,0 +1,48 @@ +const jimpLib = require("jimp"); + +async function run() { + const JimpClass = jimpLib.Jimp || jimpLib; + console.log("Reading image..."); + const image = await JimpClass.read("public/logo-text.jpg"); + const width = image.bitmap.width; + const height = image.bitmap.height; + console.log("Image loaded:", width, "x", height); + + // Sample top-left corner as background color reference + const bgR = image.bitmap.data[0]; + const bgG = image.bitmap.data[1]; + const bgB = image.bitmap.data[2]; + console.log("Background color sample:", bgR, bgG, bgB); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = (width * y + x) << 2; + const r = image.bitmap.data[idx + 0]; + const g = image.bitmap.data[idx + 1]; + const b = image.bitmap.data[idx + 2]; + + // Euclidean distance from background color + const dist = Math.sqrt((r - bgR)**2 + (g - bgG)**2 + (b - bgB)**2); + + if (dist < 40) { + image.bitmap.data[idx + 3] = 0; // 100% transparent + } else if (dist < 80) { + // Smooth alpha edge transition + image.bitmap.data[idx + 3] = Math.floor(((dist - 40) / 40) * 255); + } + } + } + + console.log("Saving transparent PNG..."); + if (typeof image.writeAsync === "function") { + await image.writeAsync("public/logo-text-transparent.png"); + } else if (typeof image.write === "function") { + await image.write("public/logo-text-transparent.png"); + } + console.log("Successfully created public/logo-text-transparent.png!"); +} + +run().catch(err => { + console.error("Error processing image:", err); + process.exit(1); +}); diff --git a/scripts/setup.js b/scripts/setup.js new file mode 100644 index 0000000..69e909b --- /dev/null +++ b/scripts/setup.js @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/** + * ShadowLine 0-to-100 Automated Setup & Launcher + * + * Cross-platform CLI wizard for Linux Ubuntu, Windows, and macOS. + * Handles environment configuration, dependency installation, and + * launching local dev/prod servers or cloud deployments. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const readline = require('readline'); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const ENV_LOCAL_PATH = path.join(ROOT_DIR, '.env.local'); + +// ANSI Color codes for terminal formatting +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + cyan: '\x1b[36m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + magenta: '\x1b[35m', +}; + +function printBanner() { + console.clear(); + console.log(`${colors.cyan}${colors.bold}`); + console.log(' ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗███████╗'); + console.log(' ██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║██║ ██║████╗ ██║██╔════╝'); + console.log(' ███████╗███████║███████║██║ ██║██║ ██║██║ █╗ ██║██║ ██║██╔██╗ ██║█████╗ '); + console.log(' ╚════██║██╔══██║██╔══██║██║ ██║██║ ██║██║███╗██║██║ ██║██║╚██╗██║██╔══╝ '); + console.log(' ███████║██║ ██║██║ ██║██████╔╝╚██████╔╝╚███╔███╔╝███████╗██║██║ ╚████║███████╗'); + console.log(' ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝'); + console.log(`${colors.reset}`); + console.log(`${colors.green}${colors.bold} 🔒 Confidential Asset Shielding Protocol | ⚡ Powered by Zama FHEVM${colors.reset}`); + console.log(`${colors.magenta} 🌐 Open-Source Protocol (MIT License) | 💎 Powered by x.com/andy1eth${colors.reset}`); + console.log(`${colors.cyan} ────────────────────────────────────────────────────────────────────────────────────${colors.reset}\n`); +} + +function checkNodeVersion() { + const version = process.version.replace('v', '').split('.')[0]; + if (parseInt(version, 10) < 18) { + console.error(`${colors.red}[ERROR] Node.js version 18 or higher is required. You are running ${process.version}${colors.reset}`); + process.exit(1); + } + console.log(`${colors.green}✔ Node.js version check passed (${process.version})${colors.reset}`); +} + +// Single shared readline interface — created once, reused across all questions +let _rl = null; +function getRl() { + if (!_rl || _rl.closed) { + _rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + } + return _rl; +} + +function closeRl() { + if (_rl && !_rl.closed) { + _rl.close(); + _rl = null; + } +} + +function question(query) { + return new Promise((resolve) => getRl().question(query, resolve)); +} + +async function setupEnvironment() { + console.log(`\n${colors.bold}─── Step 1: Environment Configuration (.env.local) ───${colors.reset}`); + + if (fs.existsSync(ENV_LOCAL_PATH)) { + console.log(`${colors.green}✔ .env.local already exists. Using existing configuration.${colors.reset}`); + return; + } + + console.log(`${colors.yellow}ℹ .env.local not found. Generating default public configuration...${colors.reset}`); + + const customize = await question(`${colors.cyan}? Do you want to configure custom API keys (WalletConnect / Relayer)? [y/N]: ${colors.reset}`); + + let wcId = 'public-demo-project-id'; + let relayerKey = ''; + + if (customize.trim().toLowerCase() === 'y' || customize.trim().toLowerCase() === 'yes') { + const inputWc = await question(`${colors.cyan}? Enter WalletConnect Project ID (leave blank for public fallback): ${colors.reset}`); + if (inputWc.trim()) wcId = inputWc.trim(); + + const inputRelayer = await question(`${colors.cyan}? Enter Zama Relayer API Key (leave blank for public testnet mode): ${colors.reset}`); + if (inputRelayer.trim()) relayerKey = inputRelayer.trim(); + } + + const envContent = [ + '# ShadowLine Automated Configuration', + 'NEXT_PUBLIC_APP_URL="http://localhost:3000"', + 'NEXT_PUBLIC_DEFAULT_CHAIN="sepolia"', + `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="${wcId}"`, + `NEXT_PUBLIC_ZAMA_RELAYER_API_KEY="${relayerKey}"`, + '', + ].join('\n'); + + fs.writeFileSync(ENV_LOCAL_PATH, envContent, 'utf8'); + console.log(`${colors.green}✔ Created .env.local successfully!${colors.reset}`); +} + +function installDependencies() { + console.log(`\n${colors.bold}─── Step 2: Installing Dependencies ───${colors.reset}`); + const nodeModulesPath = path.join(ROOT_DIR, 'node_modules'); + if (fs.existsSync(nodeModulesPath)) { + console.log(`${colors.green}✔ node_modules found. Verifying packages are up to date...${colors.reset}`); + } else { + console.log(`${colors.yellow}ℹ node_modules not found. Installing project dependencies...${colors.reset}`); + } + try { + execSync('npm install', { stdio: 'inherit', cwd: ROOT_DIR }); + console.log(`${colors.green}✔ Project dependencies verified and ready.${colors.reset}`); + } catch (error) { + console.error(`${colors.red}[ERROR] Failed to install dependencies.${colors.reset}`); + process.exit(1); + } +} + +async function presentMenu() { + console.log(`\n${colors.bold}${colors.magenta}═══════════════════════════════════════════════════════════════════════`); + console.log(`🎉 SETUP COMPLETE! What would you like to do next?`); + console.log(`═══════════════════════════════════════════════════════════════════════${colors.reset}`); + console.log(`${colors.green}[1] 🚀 Start Local Development Server (npm run dev) [RECOMMENDED]${colors.reset}`); + console.log(`[2] 🌐 Start Production Server (npm run start)`); + console.log(`[3] ☁️ Deploy to Netlify (via Netlify CLI)`); + console.log(`[4] ☁️ Deploy to Vercel (via Vercel CLI)`); + console.log(`[5] 🐳 Launch Docker Container (docker compose up -d)`); + console.log(`[0] ❌ Exit`); + console.log(`${colors.magenta}───────────────────────────────────────────────────────────────────────${colors.reset}`); + + const choice = await question(`${colors.cyan}? Select an option [0-5]: ${colors.reset}`); + // Pause readline (do NOT close/destroy it) — closing destroys stdin fd which causes EINVAL + // when execSync tries to inherit stdio from this process. + getRl().pause(); + + switch (choice.trim()) { + case '1': + console.log(`\n${colors.green}🚀 Launching local development server on http://localhost:3000 ...${colors.reset}\n`); + try { + execSync('npm run dev', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + // Ctrl+C or dev server exit — not a fatal error, just exit cleanly + process.exit(0); + } + break; + case '2': + console.log(`\n${colors.green}🌐 Launching production server on http://localhost:3000 ...${colors.reset}\n`); + try { + execSync('npm run start', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + process.exit(0); + } + break; + case '3': + console.log(`\n${colors.cyan}☁️ Deploying to Netlify...${colors.reset}`); + console.log(`${colors.yellow}ℹ Make sure you are logged in. If not, run: npx netlify login${colors.reset}\n`); + try { + execSync('npx netlify-cli deploy --prod', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + console.error(`${colors.red}[ERROR] Netlify deployment failed.${colors.reset}`); + } + break; + case '4': + console.log(`\n${colors.cyan}☁️ Deploying to Vercel...${colors.reset}`); + console.log(`${colors.yellow}ℹ Make sure you are logged in. If not, run: npx vercel login${colors.reset}\n`); + try { + execSync('npx vercel --prod', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + console.error(`${colors.red}[ERROR] Vercel deployment failed.${colors.reset}`); + } + break; + case '5': + console.log(`\n${colors.cyan}🐳 Launching Docker container...${colors.reset}\n`); + try { + execSync('docker compose up -d --build', { stdio: 'inherit', cwd: ROOT_DIR }); + console.log(`${colors.green}✔ Docker container running on http://localhost:3000${colors.reset}`); + } catch (e) { + console.error(`${colors.red}[ERROR] Docker command failed. Is Docker running?${colors.reset}`); + } + break; + case '0': + console.log(`\n${colors.yellow}Goodbye! Re-run anytime with: npm run setup${colors.reset}\n`); + process.exit(0); + break; + default: + console.log(`${colors.red}Invalid option. Exiting.${colors.reset}`); + process.exit(0); + } +} + +async function main() { + printBanner(); + checkNodeVersion(); + await setupEnvironment(); + installDependencies(); + await presentMenu(); +} + +main().catch((err) => { + console.error(`${colors.red}[FATAL ERROR]`, err.message || err, colors.reset); + closeRl(); + process.exit(1); +}); diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 0000000..f06fc14 --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,121 @@ +# ShadowLine 0-to-100 Quick Installer and Prerequisite Auto-Installer for Windows PowerShell +# +# Usage (one-liner in PowerShell): +# irm https://raw.githubusercontent.com/hosein-ul/ShadowLine/main/scripts/setup.ps1 | iex +# +# Or locally: +# .\scripts\setup.ps1 + +$ErrorActionPreference = "Stop" + +Write-Host ' ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗███████╗' -ForegroundColor Cyan +Write-Host ' ██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║██║ ██║████╗ ██║██╔════╝' -ForegroundColor Cyan +Write-Host ' ███████╗███████║███████║██║ ██║██║ ██║██║ █╗ ██║██║ ██║██╔██╗ ██║█████╗ ' -ForegroundColor Cyan +Write-Host ' ╚════██║██╔══██║██╔══██║██║ ██║██║ ██║██║███╗██║██║ ██║██║╚██╗██║██╔══╝ ' -ForegroundColor Cyan +Write-Host ' ███████║██║ ██║██║ ██║██████╔╝╚██████╔╝╚███╔███╔╝███████╗██║██║ ╚████║███████╗' -ForegroundColor Cyan +Write-Host ' ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝' -ForegroundColor Cyan +Write-Host ' 🔒 Confidential Asset Shielding Protocol | ⚡ Powered by Zama FHEVM' -ForegroundColor Green +Write-Host ' 🌐 Open-Source Protocol (MIT License) | 💎 Powered by x.com/andy1eth' -ForegroundColor Magenta +Write-Host ' ────────────────────────────────────────────────────────────────────────────────────' -ForegroundColor Cyan +Write-Host "" + +# Helper to refresh PATH in current session after winget/choco installs +function Refresh-Path { + $machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + $userPath = [System.Environment]::GetEnvironmentVariable("Path", "User") + $env:Path = "$machinePath;$userPath" +} + +# 1. Check and Auto-Install Git +if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Host "ℹ Git not found. Attempting automatic installation..." -ForegroundColor Yellow + if (Get-Command winget -ErrorAction SilentlyContinue) { + winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements + Refresh-Path + } elseif (Get-Command choco -ErrorAction SilentlyContinue) { + choco install git -y + Refresh-Path + } else { + Write-Host "[ERROR] Git is not installed and no package manager (winget/choco) was found." -ForegroundColor Red + Write-Host "Please install Git from https://git-scm.com/ and re-run this script." -ForegroundColor Red + exit 1 + } +} else { + Write-Host "✔ Git is already installed ($(git --version))." -ForegroundColor Green +} + +# 2. Check and Auto-Install Node.js (v18+) +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Host "ℹ Node.js not found. Attempting automatic installation of Node.js LTS..." -ForegroundColor Yellow + if (Get-Command winget -ErrorAction SilentlyContinue) { + winget install --id OpenJS.NodeJS.LTS -e --source winget --accept-package-agreements --accept-source-agreements + Refresh-Path + } elseif (Get-Command choco -ErrorAction SilentlyContinue) { + choco install nodejs-lts -y + Refresh-Path + } else { + Write-Host "[ERROR] Node.js is not installed and no package manager was found." -ForegroundColor Red + Write-Host "Please install Node.js v18+ from https://nodejs.org/ and re-run this script." -ForegroundColor Red + exit 1 + } +} else { + try { + $nodeVer = (node -v) -replace 'v','' -split '\.' | Select-Object -First 1 + if ([int]$nodeVer -lt 18) { + Write-Host "[ERROR] Node.js version 18 or higher is required. Found v$nodeVer. Please upgrade." -ForegroundColor Red + exit 1 + } + Write-Host "✔ Node.js version check passed ($(node -v))." -ForegroundColor Green + } catch { + Write-Host "[ERROR] Could not determine Node.js version. Please ensure Node.js v18+ is installed." -ForegroundColor Red + exit 1 + } +} + +# 3. Clone or locate the repository +# Bug fix: track $RepoDir explicitly rather than relying on Set-Location side-effects +$RepoDir = $null + +# Bug fix: safely check package.json content (avoid null reference if file doesn't exist) +$InRepo = $false +if (Test-Path "package.json") { + $pkgContent = Get-Content "package.json" -Raw -ErrorAction SilentlyContinue + if ($pkgContent -and ($pkgContent | Select-String '"name": "shadowline"' -Quiet)) { + $InRepo = $true + } +} + +if ($InRepo) { + $RepoDir = (Get-Location).Path + Write-Host "✔ ShadowLine repository detected in current directory." -ForegroundColor Green +} elseif ((Test-Path "shadowline\package.json") -and (Test-Path "shadowline\.git")) { + Write-Host "ℹ Directory 'shadowline' found. Updating..." -ForegroundColor Yellow + Set-Location "shadowline" + git pull origin main + $RepoDir = (Get-Location).Path +} elseif ((Test-Path "ShadowLine\package.json") -and (Test-Path "ShadowLine\.git")) { + Write-Host "ℹ Directory 'ShadowLine' found. Updating..." -ForegroundColor Yellow + Set-Location "ShadowLine" + git pull origin main + $RepoDir = (Get-Location).Path +} else { + Write-Host "Cloning ShadowLine from GitHub..." -ForegroundColor Cyan + $currName = Split-Path -Leaf (Get-Location).Path + if (($currName -ieq "shadowline") -and -not (Get-ChildItem -Force | Where-Object { $_.Name -ne "." -and $_.Name -ne ".." })) { + git clone https://github.com/hosein-ul/ShadowLine.git . + } else { + git clone https://github.com/hosein-ul/ShadowLine.git shadowline + Set-Location "shadowline" + } + $RepoDir = (Get-Location).Path +} + +Write-Host "✔ Working directory: $RepoDir" -ForegroundColor Green +Write-Host "" + +# 4. Launch interactive setup wizard +# Bug fix: When run via irm | iex, node.exe stdin is connected to the PowerShell pipe. +# We must pass the script path explicitly and rely on setup.js's /dev/tty equivalent (CONIN$). +Write-Host "✔ Environment ready! Launching interactive setup wizard..." -ForegroundColor Green +$SetupScript = Join-Path $RepoDir "scripts\setup.js" +node $SetupScript diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..a3554b2 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# ShadowLine 0-to-100 Quick Installer for Ubuntu Linux, Debian & macOS +# +# Usage (one-liner): +# curl -sSL https://raw.githubusercontent.com/hosein-ul/ShadowLine/main/scripts/setup.sh | bash +# +# Or locally inside the repo: +# bash scripts/setup.sh + +# Bug fix: use set -euo pipefail for robust error handling +# -e: exit on error, -u: treat unset variables as error, -o pipefail: catch pipe failures +set -euo pipefail + +GREEN='\033[0;32m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${CYAN}" +cat << "EOF" + ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗███████╗ + ██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║██║ ██║████╗ ██║██╔════╝ + ███████╗███████║███████║██║ ██║██║ ██║██║ █╗ ██║██║ ██║██╔██╗ ██║█████╗ + ╚════██║██╔══██║██╔══██║██║ ██║██║ ██║██║███╗██║██║ ██║██║╚██╗██║██╔══╝ + ███████║██║ ██║██║ ██║██████╔╝╚██████╔╝╚███╔███╔╝███████╗██║██║ ╚████║███████╗ + ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ +EOF +echo -e "${NC}" +echo -e "${GREEN} 🔒 Confidential Asset Shielding Protocol | ⚡ Powered by Zama FHEVM${NC}" +echo -e "${YELLOW} 🌐 Open-Source Protocol (MIT License) | 💎 Powered by x.com/andy1eth${NC}" +echo -e "${CYAN} ────────────────────────────────────────────────────────────────────────────────────${NC}\n" + +# Detect root / sudo availability +SUDO="" +if [ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo &> /dev/null; then + SUDO="sudo" +elif [ "${EUID:-$(id -u)}" -ne 0 ]; then + echo -e "${YELLOW}Warning: Running without root privileges and sudo is not installed. Package installs may fail.${NC}" +fi + +# 0. Ensure curl is installed (required for NodeSource) +if ! command -v curl &> /dev/null; then + echo -e "${YELLOW}ℹ curl not found. Attempting to install...${NC}" + if command -v apt-get &> /dev/null; then + $SUDO apt-get update -y && $SUDO apt-get install -y curl ca-certificates + elif command -v brew &> /dev/null; then + brew install curl + else + echo -e "${RED}[ERROR] curl is required but could not be installed. Please install curl manually.${NC}" + exit 1 + fi +else + echo -e "${GREEN}✔ curl is available.${NC}" +fi + +# 1. Check & Auto-Install Git +if ! command -v git &> /dev/null; then + echo -e "${YELLOW}ℹ Git not found. Attempting to install...${NC}" + if command -v apt-get &> /dev/null; then + echo -e "${CYAN}Installing git via apt-get (Ubuntu/Debian)...${NC}" + $SUDO apt-get update -y && $SUDO apt-get install -y git + elif command -v brew &> /dev/null; then + echo -e "${CYAN}Installing git via Homebrew (macOS)...${NC}" + brew install git + else + echo -e "${RED}[ERROR] Git is required. Please install git and try again.${NC}" + exit 1 + fi +else + echo -e "${GREEN}✔ Git is already installed ($(git --version)).${NC}" +fi + +# 2. Check & Auto-Install Node.js (v18+) +if ! command -v node &> /dev/null; then + echo -e "${YELLOW}ℹ Node.js not found. Installing Node.js v20 (LTS)...${NC}" + if command -v apt-get &> /dev/null; then + echo -e "${CYAN}Installing Node.js via NodeSource (Ubuntu/Debian)...${NC}" + $SUDO apt-get update -y && $SUDO apt-get install -y ca-certificates gnupg + curl -fsSL https://deb.nodesource.com/setup_20.x | $SUDO -E bash - + $SUDO apt-get install -y nodejs + elif command -v brew &> /dev/null; then + echo -e "${CYAN}Installing Node.js via Homebrew (macOS)...${NC}" + brew install node + else + echo -e "${RED}[ERROR] Node.js is required. Please install Node.js v18+ and try again.${NC}" + exit 1 + fi +else + NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$NODE_VER" -lt 18 ]; then + echo -e "${YELLOW}ℹ Node.js v$NODE_VER is older than required v18. Upgrading to v20 LTS...${NC}" + if command -v apt-get &> /dev/null; then + curl -fsSL https://deb.nodesource.com/setup_20.x | $SUDO -E bash - + $SUDO apt-get install -y nodejs + elif command -v brew &> /dev/null; then + brew upgrade node + else + echo -e "${RED}[ERROR] Node.js version 18 or higher is required. Found v$NODE_VER.${NC}" + exit 1 + fi + else + echo -e "${GREEN}✔ Node.js version check passed ($(node -v)).${NC}" + fi +fi + +# 3. Clone or locate the repository +# Bug fix: track the repo directory explicitly instead of relying on cd side-effects +REPO_DIR="" + +if [ -f "package.json" ] && grep -q '"name": "shadowline"' package.json 2>/dev/null; then + # Already inside the repo + REPO_DIR="$(pwd)" + echo -e "${GREEN}✔ ShadowLine repository detected in current directory.${NC}" +elif [ -f "shadowline/package.json" ] && [ -d "shadowline/.git" ]; then + echo -e "${YELLOW}ℹ Directory 'shadowline' found. Updating...${NC}" + cd shadowline + # Bug fix: don't fail on git pull errors (e.g. already up to date, detached HEAD) + git pull origin main || echo -e "${YELLOW}Warning: git pull had an issue; continuing with existing code.${NC}" + REPO_DIR="$(pwd)" +elif [ -f "ShadowLine/package.json" ] && [ -d "ShadowLine/.git" ]; then + echo -e "${YELLOW}ℹ Directory 'ShadowLine' found. Updating...${NC}" + cd ShadowLine + git pull origin main || echo -e "${YELLOW}Warning: git pull had an issue; continuing with existing code.${NC}" + REPO_DIR="$(pwd)" +else + echo -e "${CYAN}Cloning ShadowLine from GitHub...${NC}" + CURR_DIR_NAME=$(basename "$(pwd)" | tr '[:upper:]' '[:lower:]') + if [ "$CURR_DIR_NAME" = "shadowline" ] && [ -z "$(ls -A 2>/dev/null)" ]; then + git clone https://github.com/hosein-ul/ShadowLine.git . + else + git clone https://github.com/hosein-ul/ShadowLine.git shadowline + cd shadowline + fi + REPO_DIR="$(pwd)" +fi + +echo -e "${GREEN}✔ Working directory: $REPO_DIR${NC}\n" + +# 4. Launch the interactive setup wizard +# Bug fix: redirect stdin from /dev/tty so readline works even when piped via curl | bash +if [ -t 0 ]; then + # stdin is already a terminal — run normally + node "$REPO_DIR/scripts/setup.js" +elif [ -e /dev/tty ]; then + # stdin is a pipe (curl | bash) — redirect from the real terminal + node "$REPO_DIR/scripts/setup.js" < /dev/tty +else + echo -e "${RED}[ERROR] No interactive terminal available. Please run the script directly (not via pipe):${NC}" + echo -e "${CYAN} bash scripts/setup.sh${NC}" + exit 1 +fi diff --git a/src/app/ClientLayout.tsx b/src/app/ClientLayout.tsx index 79b2ecf..ea326a2 100644 --- a/src/app/ClientLayout.tsx +++ b/src/app/ClientLayout.tsx @@ -8,6 +8,7 @@ import Footer from '@/components/layout/Footer'; import { useAccount } from 'wagmi'; import { sepolia, mainnet } from 'wagmi/chains'; import { type SupportedChainId } from '@/config/chains'; +import { SessionResetProvider } from '@/lib/reset-session'; type Theme = 'dark' | 'light'; export type DesignTheme = 'charcoal' | 'midnight' | 'frost' | 'aurora'; @@ -53,55 +54,49 @@ export function useActiveNetwork() { function LayoutContent({ children }: { children: React.ReactNode }) { const [isTestnet, setIsTestnet] = useState(true); - // Load network preference on mount + // Load network preference on mount — force Sepolia while Mainnet relayer API key is pending useEffect(() => { - const savedNetwork = localStorage.getItem('network-preference'); - if (savedNetwork) { - setIsTestnet(savedNetwork === 'testnet'); - } + setIsTestnet(true); + localStorage.setItem('network-preference', 'testnet'); }, []); const handleSetIsTestnet = (val: boolean) => { + if (!val) return; // Mainnet is temporarily disabled setIsTestnet(val); - localStorage.setItem('network-preference', val ? 'testnet' : 'mainnet'); + localStorage.setItem('network-preference', 'testnet'); }; // Determine active chain ID dynamically (safe now that we are within Providers) - const { chain, isConnected } = useAccount(); - const activeChainId = (isConnected && chain && (chain.id === sepolia.id || chain.id === mainnet.id) - ? chain.id - : (isTestnet ? sepolia.id : mainnet.id)) as SupportedChainId; - - // Keep isTestnet in sync with connected wallet chain - useEffect(() => { - if (isConnected && chain) { - setIsTestnet(chain.id === sepolia.id); - } - }, [chain, isConnected]); + // While Mainnet relayer API key is pending, force activeChainId to Sepolia (11155111). + // If the user connects a wallet on Mainnet (chain 1), activeChainId stays Sepolia, + // prompting all UI actions (like Wrap/Transfer/Portfolio) to show "Switch to Sepolia". + const activeChainId = sepolia.id as SupportedChainId; return ( -
-
-
- {children} -
-
-
+ +
+
+
+ {children} +
+
+
+
); } export default function ClientLayout({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = useState('dark'); + const [theme, setTheme] = useState('light'); const [designTheme, setDesignThemeState] = useState('charcoal'); // Load theme and design direction from localStorage on mount useEffect(() => { const savedTheme = localStorage.getItem('theme') as Theme | null; - const initialTheme = savedTheme || 'dark'; + const initialTheme = savedTheme || 'light'; setTheme(initialTheme); document.documentElement.setAttribute('data-theme', initialTheme); diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts new file mode 100644 index 0000000..8116c55 --- /dev/null +++ b/src/app/api/registry/route.ts @@ -0,0 +1,249 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createPublicClient, http, type PublicClient } from 'viem'; +import { sepolia, mainnet } from 'viem/chains'; +import { REGISTRY_ADDRESSES, KNOWN_WRAPPERS } from '@/config/contracts'; + +/** + * GET /api/registry?chain=sepolia|mainnet + * + * Public REST API for querying the Zama WrappersRegistry. + * + * Returns every registered ERC-20 ↔ ERC-7984 wrapper pair with metadata. + * Falls back to the hardcoded snapshot only when the on-chain read fails. + * + * Usage: + * fetch("https://YOUR_DEPLOYMENT_URL/api/registry?chain=sepolia") + * .then(r => r.json()) + * .then(data => console.log(data.pairs)) + */ + +/** + * Real WrappersRegistry ABI — the two view functions we need to paginate the + * pair list. Verified against the on-chain contract source at + * https://github.com/zama-ai/protocol-apps/tree/main/contracts/confidential-token-wrappers-registry + * + * Note: the previous version of this route called a non-existent `listPairs` + * function which always reverted, causing every request to fall through to + * the cached snapshot. That is now fixed. + */ +const REGISTRY_ABI = [ + { + name: 'getTokenConfidentialTokenPairsLength', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ name: '', type: 'uint256' }], + }, + { + name: 'getTokenConfidentialTokenPairsSlice', + type: 'function', + stateMutability: 'view', + inputs: [ + { name: 'fromIndex', type: 'uint256' }, + { name: 'toIndex', type: 'uint256' }, + ], + outputs: [ + { + type: 'tuple[]', + components: [ + { name: 'tokenAddress', type: 'address' }, + { name: 'confidentialTokenAddress', type: 'address' }, + { name: 'isValid', type: 'bool' }, + ], + }, + ], + }, +] as const; + +// ERC-20 metadata ABI for enrichment +const ERC20_META_ABI = [ + { name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] }, + { name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] }, + { name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint8' }] }, +] as const; + +const CHAIN_MAP: Record = { + sepolia, + mainnet, +}; + +const RPC_URLS: Record = { + [sepolia.id]: process.env.NEXT_PUBLIC_SEPOLIA_RPC || 'https://ethereum-sepolia-rpc.publicnode.com', + [mainnet.id]: process.env.NEXT_PUBLIC_MAINNET_RPC || 'https://ethereum-rpc.publicnode.com', +}; + +/** + * Registry entries that are on-chain but that our review flags as unverified + * (suspected test/placeholder deployments). Instead of hiding them — + * which would silently drop official registry coverage — we surface them + * with an `unverified` flag + rationale so consumers can display a warning. + * + * Keep in sync with `BLOCKLISTED_WRAPPERS` in `src/lib/registry.ts`. + */ +const UNVERIFIED_WRAPPERS: Record = { + '0xba4cff6ed6f7cb2a58776deca4e984b498446762': + 'Suspected test/placeholder entry (cbbqTGBP). Underlying uses a vanity address (0xbeeff…) and the asset name has no known referent. See docs.zama.org mainnet addresses page.', +}; + +interface PairResult { + tokenAddress: string; + confidentialTokenAddress: string; + symbol: string; + confidentialSymbol: string; + name: string; + decimals: number; + wrapperDecimals: number; + isValid: boolean; + unverified?: boolean; + unverifiedReason?: string; +} + +async function readTokenMeta( + client: PublicClient, + address: `0x${string}`, +): Promise<{ name: string; symbol: string; decimals: number }> { + try { + const [name, symbol, decimals] = await Promise.all([ + client.readContract({ address, abi: ERC20_META_ABI, functionName: 'name' }), + client.readContract({ address, abi: ERC20_META_ABI, functionName: 'symbol' }), + client.readContract({ address, abi: ERC20_META_ABI, functionName: 'decimals' }), + ]); + return { + name: name as string, + symbol: (symbol as string).replace(/Mock$/i, ''), + decimals: Number(decimals), + }; + } catch { + return { name: 'Unknown', symbol: 'UNKNOWN', decimals: 18 }; + } +} + +export async function GET(request: NextRequest) { + const chainParam = request.nextUrl.searchParams.get('chain') ?? 'sepolia'; + const chain = CHAIN_MAP[chainParam.toLowerCase()]; + + if (!chain) { + return NextResponse.json( + { error: `Invalid chain "${chainParam}". Use "sepolia" or "mainnet".` }, + { status: 400 }, + ); + } + + const registryAddress = REGISTRY_ADDRESSES[chain.id as keyof typeof REGISTRY_ADDRESSES]; + if (!registryAddress) { + return NextResponse.json( + { error: `No registry address configured for chain ${chain.id}.` }, + { status: 400 }, + ); + } + + const client = createPublicClient({ + chain, + transport: http(RPC_URLS[chain.id]), + }); + + try { + // 1. Get pair count + const totalBig = (await client.readContract({ + address: registryAddress, + abi: REGISTRY_ABI, + functionName: 'getTokenConfidentialTokenPairsLength', + })) as bigint; + const total = Number(totalBig); + + if (total === 0) { + return NextResponse.json( + { pairs: [], total: 0, chain: chainParam, registryAddress, timestamp: Date.now(), source: 'on-chain' }, + { headers: cacheHeaders() }, + ); + } + + // 2. Fetch all pairs in one call (fromIndex inclusive, toIndex exclusive) + const slice = (await client.readContract({ + address: registryAddress, + abi: REGISTRY_ABI, + functionName: 'getTokenConfidentialTokenPairsSlice', + args: [0n, totalBig], + })) as readonly { + tokenAddress: `0x${string}`; + confidentialTokenAddress: `0x${string}`; + isValid: boolean; + }[]; + + // 3. Enrich with ERC-20 metadata (parallel) + const metaPromises = slice.map(async (pair): Promise => { + const [underlyingMeta, wrapperMeta] = await Promise.all([ + readTokenMeta(client, pair.tokenAddress), + readTokenMeta(client, pair.confidentialTokenAddress), + ]); + + const wrapperKey = pair.confidentialTokenAddress.toLowerCase(); + const unverifiedReason = UNVERIFIED_WRAPPERS[wrapperKey]; + + return { + tokenAddress: pair.tokenAddress, + confidentialTokenAddress: pair.confidentialTokenAddress, + symbol: underlyingMeta.symbol, + confidentialSymbol: `c${underlyingMeta.symbol}`, + name: underlyingMeta.name, + decimals: underlyingMeta.decimals, + wrapperDecimals: wrapperMeta.decimals, + isValid: pair.isValid, + ...(unverifiedReason ? { unverified: true, unverifiedReason } : {}), + }; + }); + + const pairs = await Promise.all(metaPromises); + + return NextResponse.json( + { + pairs, + total: pairs.length, + chain: chainParam, + registryAddress, + timestamp: Date.now(), + source: 'on-chain', + }, + { headers: cacheHeaders() }, + ); + } catch (err) { + // Fallback to hardcoded snapshot only when the RPC read genuinely fails. + console.error('Registry on-chain read failed, falling back to snapshot:', err); + const fallback = (KNOWN_WRAPPERS[chain.id as keyof typeof KNOWN_WRAPPERS] ?? []).map((p): PairResult => { + const wrapperKey = p.erc7984Address.toLowerCase(); + const unverifiedReason = UNVERIFIED_WRAPPERS[wrapperKey]; + return { + tokenAddress: p.erc20Address, + confidentialTokenAddress: p.erc7984Address, + symbol: p.symbol, + confidentialSymbol: `c${p.symbol}`, + name: p.name, + decimals: p.decimals, + wrapperDecimals: p.wrapperDecimals, + isValid: p.isValid ?? true, + ...(unverifiedReason ? { unverified: true, unverifiedReason } : {}), + }; + }); + + return NextResponse.json( + { + pairs: fallback, + total: fallback.length, + chain: chainParam, + registryAddress, + timestamp: Date.now(), + source: 'cached-snapshot', + warning: 'On-chain read failed. Showing cached snapshot which may be incomplete.', + }, + { headers: cacheHeaders() }, + ); + } +} + +function cacheHeaders(): HeadersInit { + return { + 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + }; +} diff --git a/src/app/app/analytics/page.tsx b/src/app/app/analytics/page.tsx new file mode 100644 index 0000000..628f4e5 --- /dev/null +++ b/src/app/app/analytics/page.tsx @@ -0,0 +1,713 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { usePublicClient } from 'wagmi'; +import { parseAbiItem, formatUnits } from 'viem'; +import Card from '@/components/ui/Card'; +import Badge from '@/components/ui/Badge'; +import Button from '@/components/ui/Button'; +import Skeleton from '@/components/ui/Skeleton'; +import TokenIcon from '@/components/ui/TokenIcon'; +import BlurIn from '@/components/ui/BlurIn'; +import { useActiveNetwork } from '@/app/ClientLayout'; +import { useRegistryPairs } from '@/lib/registry'; +import { formatAddress } from '@/lib/utils'; +import { + BarChart2, + TrendingUp, + Shield, + Unlock, + Users, + RefreshCw, + ExternalLink, + Clock, + ArrowUpRight, + ArrowDownLeft, + Scale, + Zap, +} from 'lucide-react'; + +/* ─── Types ──────────────────────────────────────────────────────────────────── */ + +interface TokenTVL { + symbol: string; + tvlRaw: bigint; + tvlCompact: string; // "23.0M", "5.1K", "412" + tvlHuman: number; // normalized float (for sorting & bar width) + decimals: number; + erc20Address: string; + wrapperAddress: string; + shieldCount: number; + unshieldCount: number; + shieldVolume: bigint; // total amount shielded (underlying decimals) + unshieldVolume: bigint; // total amount unshielded +} + +interface ActivityEvent { + type: 'shield' | 'unshield'; + symbol: string; + amount: bigint; + decimals: number; + from: string; + to: string; + txHash: string; + blockNumber: bigint; + timeAgo: string; // pre-computed +} + +const TRANSFER_ABI = parseAbiItem( + 'event Transfer(address indexed from, address indexed to, uint256 value)', +); + +const ERC20_BALANCE_ABI = [ + { + name: 'balanceOf', + type: 'function', + stateMutability: 'view', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint256' }], + }, +] as const; + +/* ─── Helpers ─────────────────────────────────────────────────────────────────── */ + +/** Format a token amount into compact notation: "23.0M", "5.1K", "412.35" */ +function formatTVLCompact(raw: bigint, decimals: number): string { + if (raw === 0n) return '0'; + const divisor = 10n ** BigInt(decimals); + const whole = Number(raw / divisor); + const frac = Number(raw % divisor) / Math.pow(10, decimals); + const value = whole + frac; + + if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(2)}B`; + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`; + if (value >= 1_000) return `${(value / 1_000).toFixed(2)}K`; + return value.toFixed(2).replace(/\.00$/, ''); +} + +/** + * Estimate how long ago an event happened without extra RPC calls. + * Uses: fetchTimestamp - (latestBlock - eventBlock) × blockTimeMs + */ +function estimateTimeAgo( + eventBlock: bigint, + latestBlock: bigint, + fetchTimestamp: number, + blockTimeMs = 12_000, +): string { + const blocksDiff = Number(latestBlock - eventBlock); + const msAgo = Date.now() - (fetchTimestamp - blocksDiff * blockTimeMs); + const sec = Math.max(0, Math.floor(msAgo / 1000)); + + if (sec < 60) return `${sec}s ago`; + const min = Math.floor(sec / 60); + if (min < 60) return `${min}m ago`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr}h ago`; + return `${Math.floor(hr / 24)}d ago`; +} + +/* ─── Stat card ───────────────────────────────────────────────────────────────── */ +function StatCard({ + icon, + label, + value, + sub, + color = 'var(--accent)', + loading, +}: { + icon: React.ReactNode; + label: string; + value: string; + sub?: string; + color?: string; + loading?: boolean; +}) { + return ( + +
+
+ {icon} +
+
+
{label}
+ {loading ? ( + + ) : ( +
+ {value} +
+ )} + {sub && !loading && ( +
{sub}
+ )} +
+
+
+ ); +} + +/* ─── TVL bar ────────────────────────────────────────────────────────────────── */ +function TVLBar({ + token, + maxTvlHuman, + explorerBase, +}: { + token: TokenTVL; + maxTvlHuman: number; + explorerBase: string; +}) { + const pct = maxTvlHuman > 0 + ? Math.max((token.tvlHuman / maxTvlHuman) * 100, 0.5) + : 0; + + const volCompact = formatTVLCompact(token.shieldVolume, token.decimals); + + return ( +
+
+ +
+
{token.symbol}
+
c{token.symbol}
+
+
+ +
+
+
+ +
+
+ {token.tvlCompact} +
+
+ {token.shieldCount}↑{' '} + {token.unshieldCount}↓ + {token.shieldVolume > 0n && ( + + · vol {volCompact} + + )} +
+
+ + + + +
+ ); +} + +/* ─── Activity row ───────────────────────────────────────────────────────────── */ +function ActivityRow({ + event, + explorerBase, +}: { + event: ActivityEvent; + explorerBase: string; +}) { + const isShield = event.type === 'shield'; + const color = isShield ? 'var(--success)' : 'var(--warning)'; + const Icon = isShield ? ArrowUpRight : ArrowDownLeft; + const amountStr = Number(formatUnits(event.amount, event.decimals)).toLocaleString(undefined, { + maximumFractionDigits: 4, + }); + + return ( +
+
+ +
+ +
+
+ + {isShield ? 'Shield' : 'Unshield'} + + + {amountStr} {event.symbol} + +
+
+ + {isShield ? 'from' : 'to'}{' '} + {formatAddress(isShield ? event.from : event.to)} + + + {event.timeAgo} + +
+
+ + + Tx + +
+ ); +} + +/* ─── Wrap/Unwrap ratio bar ──────────────────────────────────────────────────── */ +function RatioBar({ shields, unshields }: { shields: number; unshields: number }) { + const total = shields + unshields; + if (total === 0) return
No events yet
; + const shieldPct = Math.round((shields / total) * 100); + + return ( +
+
+ + Shields {shieldPct}% + + + {100 - shieldPct}% Unshields + +
+
+
+ {unshields} txs + {shields} txs +
+
+ ); +} + +/* ─── Main page ──────────────────────────────────────────────────────────────── */ + +const BLOCK_TIME_MS = 12_000; // ~12 seconds per block + +function getBlocksSinceUtcMidnight(): bigint { + const now = Date.now(); + const midnightUTC = new Date(); + midnightUTC.setUTCHours(0, 0, 0, 0); + const msSinceMidnight = now - midnightUTC.getTime(); + const blocksSinceMidnight = Math.ceil(msSinceMidnight / BLOCK_TIME_MS); + // Enforce a minimum of 7200 blocks (~24h) so the page always has meaningful + // data even at 00:01 UTC when only a handful of blocks have elapsed. + return BigInt(Math.max(blocksSinceMidnight, 7200)); +} + +export default function AnalyticsPage() { + const { activeChainId, isTestnet } = useActiveNetwork(); + const { pairs } = useRegistryPairs(activeChainId); + const client = usePublicClient({ chainId: activeChainId }); + const explorerBase = isTestnet + ? 'https://eth-sepolia.blockscout.com' + : 'https://eth.blockscout.com'; + + const [tvlData, setTvlData] = useState([]); + const [activity, setActivity] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [lastUpdated, setLastUpdated] = useState(null); + const [error, setError] = useState(null); + + const fetchAnalytics = useCallback(async () => { + if (!client || pairs.length === 0) return; + setIsLoading(true); + setError(null); + + try { + const latestBlock = await client.getBlockNumber(); + const fetchTimestamp = Date.now(); + const blockLookback = getBlocksSinceUtcMidnight(); + const fromBlock = latestBlock > blockLookback + ? latestBlock - blockLookback + : 0n; + + const tokenResults = await Promise.all( + pairs + .filter((p) => p.isValid !== false) + .map(async (pair) => { + try { + // TVL + const tvlRaw = await client.readContract({ + address: pair.erc20Address, + abi: ERC20_BALANCE_ABI, + functionName: 'balanceOf', + args: [pair.erc7984Address], + }) as bigint; + + // Shield events + const shieldLogs = await client.getLogs({ + address: pair.erc20Address, + event: TRANSFER_ABI, + args: { to: pair.erc7984Address }, + fromBlock, + toBlock: latestBlock, + }); + + // Unshield events + const unshieldLogs = await client.getLogs({ + address: pair.erc20Address, + event: TRANSFER_ABI, + args: { from: pair.erc7984Address }, + fromBlock, + toBlock: latestBlock, + }); + + const shieldVolume = shieldLogs.reduce( + (sum, log) => sum + ((log.args?.value as bigint) ?? 0n), 0n, + ); + const unshieldVolume = unshieldLogs.reduce( + (sum, log) => sum + ((log.args?.value as bigint) ?? 0n), 0n, + ); + + // Compute human-readable TVL (float) for normalized sorting & bar width. + // Using raw bigint directly for sort is WRONG because decimals differ: + // 22.98M ZAMA (18 dec) raw >> 22.89M USDC (6 dec) raw, despite similar value. + const divisor = 10n ** BigInt(pair.decimals); + const tvlHuman = Number(tvlRaw / divisor) + Number(tvlRaw % divisor) / Math.pow(10, pair.decimals); + + const tokenTvl: TokenTVL = { + symbol: pair.symbol, + tvlRaw, + tvlCompact: formatTVLCompact(tvlRaw, pair.decimals), + tvlHuman, + decimals: pair.decimals, + erc20Address: pair.erc20Address, + wrapperAddress: pair.erc7984Address, + shieldCount: shieldLogs.length, + unshieldCount: unshieldLogs.length, + shieldVolume, + unshieldVolume, + }; + + // Build activity events (latest 8 per token) + const makeEvents = ( + logs: typeof shieldLogs, + type: 'shield' | 'unshield', + ): ActivityEvent[] => + [...logs] + .sort((a, b) => Number((b.blockNumber ?? 0n) - (a.blockNumber ?? 0n))) + .slice(0, 8) + .map((log) => ({ + type, + symbol: pair.symbol, + amount: (log.args?.value as bigint) ?? 0n, + decimals: pair.decimals, + from: (log.args?.from as string) ?? '', + to: (log.args?.to as string) ?? '', + txHash: log.transactionHash ?? '', + blockNumber: log.blockNumber ?? 0n, + timeAgo: estimateTimeAgo( + log.blockNumber ?? latestBlock, + latestBlock, + fetchTimestamp, + BLOCK_TIME_MS, + ), + })); + + return { + tokenTvl, + events: [ + ...makeEvents(shieldLogs, 'shield'), + ...makeEvents(unshieldLogs, 'unshield'), + ], + }; + } catch { + return null; + } + }), + ); + + const validResults = tokenResults.filter((r): r is NonNullable => r !== null); + const allTvl = validResults.map((r) => r.tokenTvl); + const allEvents = validResults + .flatMap((r) => r.events) + .filter((e) => e.txHash && e.amount > 0n) + .sort((a, b) => Number(b.blockNumber - a.blockNumber)) + .slice(0, 25); + + // Sort by human-readable value (normalized by decimals), not raw bigint. + setTvlData(allTvl.sort((a, b) => b.tvlHuman - a.tvlHuman)); + setActivity(allEvents); + setLastUpdated(new Date()); + } catch (err) { + console.error('Analytics fetch failed:', err); + setError('Failed to load analytics data. Check your RPC connection.'); + } finally { + setIsLoading(false); + } + }, [client, pairs]); + + useEffect(() => { + fetchAnalytics(); + }, [fetchAnalytics]); + + // Derived stats + const totalShields = tvlData.reduce((s, t) => s + t.shieldCount, 0); + const totalUnshields = tvlData.reduce((s, t) => s + t.unshieldCount, 0); + const uniqueShielders = new Set( + activity.filter((e) => e.type === 'shield').map((e) => e.from.toLowerCase()), + ).size; + const maxTvlHuman = tvlData.reduce((m, t) => (t.tvlHuman > m ? t.tvlHuman : m), 0); + const activePairs = tvlData.filter((t) => t.tvlRaw > 0n).length; + + // Most active token by tx count + const mostActive = tvlData.length > 0 + ? [...tvlData].sort((a, b) => (b.shieldCount + b.unshieldCount) - (a.shieldCount + a.unshieldCount))[0] + : null; + + return ( +
+ {/* ── Header ── */} +
+ + + {isTestnet ? 'Sepolia' : 'Mainnet'} · Live + +

+ +

+

+ On-chain metrics for all registered ERC-7984 confidential wrappers. + Sourced directly from Transfer events — no indexer required. +

+
+ + {/* ── Controls ── */} +
+
+ + {lastUpdated + ? `Updated ${lastUpdated.toLocaleTimeString()}` + : 'Loading…'} +  · Since 00:00 UTC today +  · Showing up to 25 recent events +
+ +
+ + {error && ( + + {error} + + )} + + {/* ── Stat Cards ── */} +
+ } + label="Active Pairs" + value={isLoading ? '…' : `${activePairs} / ${tvlData.length}`} + sub="pairs with TVL > 0" + loading={isLoading && tvlData.length === 0} + /> + } + label="Shields (last 24h)" + value={isLoading && tvlData.length === 0 ? '…' : totalShields.toLocaleString()} + color="var(--success)" + loading={isLoading && tvlData.length === 0} + /> + } + label="Unshields (last 24h)" + value={isLoading && tvlData.length === 0 ? '…' : totalUnshields.toLocaleString()} + color="var(--warning)" + loading={isLoading && tvlData.length === 0} + /> + } + label="Unique Shielders" + value={isLoading && tvlData.length === 0 ? '…' : uniqueShielders.toString()} + sub="distinct addresses (period)" + color="#a78bfa" + loading={isLoading && tvlData.length === 0} + /> +
+ + {/* ── Extra insight row ── */} + {!isLoading && tvlData.length > 0 && ( +
+ {/* Wrap/Unshield ratio */} + +

+ + Shield vs Unshield Ratio +

+ +
+ + {/* Most active token */} + +

+ + Most Active Token +

+ {mostActive ? ( +
+ +
+
{mostActive.symbol}
+
+ {(mostActive.shieldCount + mostActive.unshieldCount).toLocaleString()} txs ·{' '} + TVS {mostActive.tvlCompact} +
+
+ + #{tvlData.indexOf(mostActive) + 1} TVS rank + +
+ ) : ( + No data + )} +
+
+ )} + + {/* ── Main grid: TVL + Activity ── */} +
+ {/* TVL by token */} + +

+ + TVS by Token +

+ + {isLoading && tvlData.length === 0 ? ( +
+ {[1, 2, 3, 4].map((i) => )} +
+ ) : tvlData.length === 0 ? ( +

+ No data yet — connect wallet or wait for registry to load. +

+ ) : ( +
+ {tvlData.map((t) => ( + + ))} +
+ )} + +

+ TVS (Total Value Shielded) = underlying ERC-20 held by wrapper. Bar = relative share. + shield count ·{' '} + unshield count · vol = period volume +

+
+ + {/* Recent Activity */} + +

+ + Recent Activity + {activity.length > 0 && ( + + {activity.length} events + + )} +

+

+ Latest shield & unshield events across all tokens · since 00:00 UTC · up to 25 shown +

+ + {isLoading && activity.length === 0 ? ( +
+ {[1, 2, 3, 4].map((i) => )} +
+ ) : activity.length === 0 ? ( +

+ No shield/unshield events since 00:00 UTC today. +

+ ) : ( +
+ {activity.map((event, i) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/app/app/docs/[slug]/page.tsx b/src/app/app/docs/[slug]/page.tsx new file mode 100644 index 0000000..11e5424 --- /dev/null +++ b/src/app/app/docs/[slug]/page.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import DocRenderer from '../_docs/DocRenderer'; +import { SUBPAGE_SLUGS } from '../_docs/nav'; + +/** Pre-render every known subpage; reject anything else with a 404. */ +export const dynamicParams = false; + +export function generateStaticParams() { + return SUBPAGE_SLUGS.map((slug) => ({ slug })); +} + +export default async function DocSlugPage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + return ; +} diff --git a/src/app/app/docs/_docs/DocRenderer.tsx b/src/app/app/docs/_docs/DocRenderer.tsx new file mode 100644 index 0000000..f86dce2 --- /dev/null +++ b/src/app/app/docs/_docs/DocRenderer.tsx @@ -0,0 +1,22 @@ +'use client'; + +/** + * Client-side resolver for a docs subpage. The `[slug]` route is a server + * component (so it can own generateStaticParams + dynamicParams), but the + * slug→component map lives in a 'use client' module and must be read on the + * client — hence this thin wrapper. + */ + +import React from 'react'; +import { DocPage } from './components'; +import { DOC_CONTENT } from './content'; + +export default function DocRenderer({ slug }: { slug: string }) { + const Body = DOC_CONTENT[slug]; + if (!Body) return null; // unknown slugs are already 404'd by dynamicParams=false + return ( + + + + ); +} diff --git a/src/app/app/docs/_docs/Sidebar.tsx b/src/app/app/docs/_docs/Sidebar.tsx new file mode 100644 index 0000000..95b9183 --- /dev/null +++ b/src/app/app/docs/_docs/Sidebar.tsx @@ -0,0 +1,81 @@ +'use client'; + +/** + * Docs sidebar — grouped, route-aware navigation. Lives in the docs layout so it + * persists across page transitions (only the content re-animates). Collapses into + * a slide-over drawer on mobile. + */ + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { BookOpen, ExternalLink, Menu, X } from 'lucide-react'; +import { DOC_ENTRIES, DOC_GROUPS, hrefForSlug } from './nav'; + +export default function Sidebar() { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + + return ( + <> + + + + + {open &&
setOpen(false)} />} + + ); +} diff --git a/src/app/app/docs/_docs/components.tsx b/src/app/app/docs/_docs/components.tsx new file mode 100644 index 0000000..9274cbd --- /dev/null +++ b/src/app/app/docs/_docs/components.tsx @@ -0,0 +1,577 @@ +'use client'; + +/** + * Shared building blocks for every docs subpage: page shell, prose helpers, + * tables, code blocks, the prev/next pager, motion reveals, and a small set of + * hand-built SVG diagrams. All styling comes from the existing `.docs-*` design + * tokens in globals.css — no new palette. + */ + +import React from 'react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import Badge from '@/components/ui/Badge'; +import CopyButton from '@/components/ui/CopyButton'; +import { ArrowLeft, ArrowRight, ExternalLink } from 'lucide-react'; +import { getEntry, getNeighbours, hrefForSlug } from './nav'; + +/* ─── Motion reveal ──────────────────────────────────────────────────────── */ + +/** Fades + lifts its children in on mount. Respects the template's page-level + * entrance by staggering slightly after it. */ +export function Reveal({ + children, + delay = 0, + className, +}: { + children: React.ReactNode; + delay?: number; + className?: string; +}) { + return ( + + {children} + + ); +} + +/* ─── Page shell (header + body + pager) ─────────────────────────────────── */ + +export function DocPage({ slug, children }: { slug: string; children: React.ReactNode }) { + const entry = getEntry(slug); + return ( +
+ {entry && ( + + {entry.eyebrow} +

{entry.label}

+

{entry.description}

+
+ )} + {children} + +
+ ); +} + +/* ─── Prev / Next pager ──────────────────────────────────────────────────── */ + +export function DocsPager({ slug }: { slug: string }) { + const { prev, next } = getNeighbours(slug); + return ( + + ); +} + +/* ─── Prose helpers ──────────────────────────────────────────────────────── */ + +export function Lead({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function P({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function H2({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function H4({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function UL({ children }: { children: React.ReactNode }) { + return
    {children}
; +} + +/* ─── Code block ─────────────────────────────────────────────────────────── */ + +export function CodeBlock({ + code, + lang = 'ts', + filename, +}: { + code: string; + lang?: string; + filename?: string; +}) { + return ( +
+
+ {filename ?? lang} + +
+
+        {code}
+      
+
+ ); +} + +/* ─── Callouts & info boxes ──────────────────────────────────────────────── */ + +export function Callout({ + variant = 'info', + children, +}: { + variant?: 'info' | 'warning' | 'error' | 'success'; + children: React.ReactNode; +}) { + const cls = + variant === 'warning' + ? 'docs-callout docs-callout-warning' + : variant === 'error' + ? 'docs-callout docs-callout-error' + : variant === 'success' + ? 'docs-callout docs-callout-success' + : 'docs-info-box'; + return
{children}
; +} + +/* ─── Feature grid ───────────────────────────────────────────────────────── */ + +export function FeatureGrid({ + items, +}: { + items: { icon: string; title: string; desc: string }[]; +}) { + return ( +
+ {items.map((f) => ( +
+
{f.icon}
+
+ {f.title} +

+ {f.desc} +

+
+
+ ))} +
+ ); +} + +/* ─── Numbered steps ─────────────────────────────────────────────────────── */ + +export function StepList({ steps }: { steps: { t: string; d: React.ReactNode }[] }) { + return ( +
+ {steps.map((s, i) => ( +
+
{i + 1}
+
+ {s.t} +

+ {s.d} +

+
+
+ ))} +
+ ); +} + +/* ─── Tables ─────────────────────────────────────────────────────────────── */ + +export function PropTable({ + columns = ['Field', 'Type', 'Description'], + children, +}: { + columns?: string[]; + children: React.ReactNode; +}) { + return ( + + + + {columns.map((c) => ( + + ))} + + + {children} +
{c}
+ ); +} + +export function PropRow({ + name, + type, + required, + description, +}: { + name: string; + type: string; + required?: boolean; + description: string; +}) { + return ( + + + {name} + {required && required} + + + {type} + + {description} + + ); +} + +export function EndpointBadge({ method, path }: { method: string; path: string }) { + return ( +
+ {method} + {path} +
+ ); +} + +export function HookCard({ + name, + pkg, + description, + signature, + example, +}: { + name: string; + pkg: string; + description: string; + signature: string; + example: string; +}) { + return ( +
+
+
+
+ {name} + + {pkg} + +
+

{description}

+
+
+ + +
+ ); +} + +export function ErrorRow({ + code, + title, + description, + retryable, +}: { + code: string; + title: string; + description: string; + retryable: boolean; +}) { + return ( + + + + {code} + + + {title} + {description} + + + {retryable ? 'Retryable' : 'Terminal'} + + + + ); +} + +export function AddressTable({ + network, + registry, + pairs, +}: { + network: string; + registry: string; + pairs: { symbol: string; erc20: string; wrapper: string; decimals: number }[]; +}) { + const explorerBase = + network === 'Sepolia' + ? 'https://eth-sepolia.blockscout.com/address' + : 'https://eth.blockscout.com/address'; + + return ( +
+
+ WrappersRegistry +
+ {registry} + + + +
+
+ + + + + + + + + + + {pairs.map((p) => ( + + + + + + + ))} + +
TokenDecimalsERC-20 AddressERC-7984 Wrapper
+ {p.symbol} + + c{p.symbol} + + {p.decimals} / 6 +
+ + {p.erc20.slice(0, 10)}…{p.erc20.slice(-6)} + + + + +
+
+
+ + {p.wrapper.slice(0, 10)}…{p.wrapper.slice(-6)} + + + + +
+
+
+ ); +} + +/* ─── SVG diagrams ───────────────────────────────────────────────────────── */ +/* Themed via CSS variables so they track light/dark automatically. Each is a + labelled box-and-arrow schematic — no external assets. */ + +function DiagramFrame({ + title, + viewBox, + children, +}: { + title: string; + viewBox: string; + children: React.ReactNode; +}) { + return ( +
+ + + {children} + + +
{title}
+
+ ); +} + +/** Reusable rounded node. */ +function Node({ + x, + y, + w, + h, + label, + sub, + accent, +}: { + x: number; + y: number; + w: number; + h: number; + label: string; + sub?: string; + accent?: boolean; +}) { + return ( + + + + {label} + + {sub && ( + + {sub} + + )} + + ); +} + +const ARROW = 'var(--text-muted)'; + +export function ArchitectureDiagram() { + return ( + + + + + + + + + + + {/* Two backend lanes */} + + + + + + + {/* Arrows */} + + + + + + {/* Gateway settles to fhEVM */} + + settles + + ); +} + +export function ShieldFlowDiagram() { + return ( + + + + + + + + SHIELD (wrap) + + + + + + + UNSHIELD (unwrap) + + + + + + + ); +} + +export function PermitFlowDiagram() { + return ( + + + + + + + + + + + + {[140, 310, 480, 650].map((x, i) => ( + + ))} + + ); +} + +export function FheConceptDiagram() { + return ( + + + Public ERC-20 + balanceOf = 1000 + uint256 · readable by anyone + + + Confidential ERC-7984 + 0x9f3a…e1c7 + euint64 handle · only you can decrypt + + ); +} diff --git a/src/app/app/docs/_docs/content/addresses.tsx b/src/app/app/docs/_docs/content/addresses.tsx new file mode 100644 index 0000000..36b02b9 --- /dev/null +++ b/src/app/app/docs/_docs/content/addresses.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, AddressTable } from '../components'; +import { useRegistryPairs } from '@/lib/registry'; +import { sepolia, mainnet } from 'viem/chains'; +import { REGISTRY_ADDRESSES } from '@/config/contracts'; + +export default function Addresses() { + const { pairs: sepoliaPairs } = useRegistryPairs(sepolia.id); + const { pairs: mainnetPairs } = useRegistryPairs(mainnet.id); + + const sepoliaFormatted = sepoliaPairs + .filter((p) => p.source !== 'custom' && !p.unverified) + .map((p) => ({ + symbol: p.symbol, + erc20: p.erc20Address, + wrapper: p.erc7984Address, + decimals: p.decimals, + })); + + const mainnetFormatted = mainnetPairs + .filter((p) => p.source !== 'custom' && !p.unverified) + .map((p) => ({ + symbol: p.symbol, + erc20: p.erc20Address, + wrapper: p.erc7984Address, + decimals: p.decimals, + })); + + return ( + <> + + All addresses below are sourced directly from the official on-chain WrappersRegistry and verified + in real-time. This ensures every registered pair is always present and up-to-date. + + +

Sepolia Testnet

+ +

+ The pairs above are mock tokens with a public mint() — grab + free test tokens from the Faucet page. Addresses read live from the on-chain registry. +

+ +

Ethereum Mainnet

+ +

+ These pairs reflect the live on-chain registry on Ethereum Mainnet. You can also query this list + programmatically via the REST API or view live analytics on the{' '} + Registry page. +

+ + ); +} + diff --git a/src/app/app/docs/_docs/content/ai-agents.tsx b/src/app/app/docs/_docs/content/ai-agents.tsx new file mode 100644 index 0000000..088ce64 --- /dev/null +++ b/src/app/app/docs/_docs/content/ai-agents.tsx @@ -0,0 +1,116 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, H4, CodeBlock, EndpointBadge, UL, Callout } from '../components'; + +export default function AiAgents() { + return ( + <> + + ShadowLine is built from the ground up for autonomous AI agents, LLMs, and programmatic + wallets. Discover verified asset pairs via standard AI manifests and execute MEV-resistant + confidential DeFi operations. + + +

Why Autonomous AI Agents Need ShadowLine

+

+ AI agents executing on-chain trading strategies, DAO treasury management, or automated payroll + face critical vulnerabilities when holding public ERC-20 tokens: +

+
    +
  • + MEV Exploitation & Sandwiche Attacks: Searchers monitor public mempools and agent balances to front-run automated trades. +
  • +
  • + Strategy Copy-Trading: Observers can copy or counter-trade an AI agent's portfolio rebalancing in real time. +
  • +
  • + Treasury Exposure: DAO and agent operational wallets reveal sensitive cash flow and runway data. +
  • +
+

+ By wrapping public tokens into ERC-7984 confidential wrappers (cTokens) on Zama's + fhEVM, AI agents hold encrypted balances (euint64) and execute confidential transfers + completely hidden from public scrutiny. +

+ +

1. llms.txt — AI Discovery Standard

+

+ ShadowLine adheres to the emerging llmstxt.org specification. LLMs and autonomous coding agents can read our structured summaries directly: +

+
    +
  • + /llms.txt — High-level protocol summary, capabilities, and core concepts. +
  • +
  • + /llms-full.txt — Complete developer reference including smart contract ABIs, Viem code patterns, and decimal scaling rules. +
  • +
+ +

2. OpenAI & Universal AI Plugin Manifests

+

+ ShadowLine hosts standard discovery manifests, allowing AI frameworks (ChatGPT plugins, LangChain, Vercel AI SDK, Eliza) to auto-discover our REST API and query confidential asset pairs without manual schema configuration: +

+
    +
  • + /.well-known/ai-plugin.json — Plugin metadata and authentication spec. +
  • +
  • + /openapi.json — OpenAPI 3.0 specification for the /api/registry endpoint. +
  • +
+ +

3. Agent Tools SDK (`@shadowline/agent-tools`)

+

+ For developers building AI agents with TypeScript, we provide pre-built tool definitions in{' '} + src/lib/agent-tools.ts. These tools can be plugged directly into LangChain or Vercel AI SDK: +

+ + +

4. Headless Execution Harness (`ShadowlineAgentHarness`)

+

+ To enable autonomous AI agents to execute real FHE asset shielding, confidential transfers, and balance decryption in a backend or script environment (without a web browser), use the headless execution harness in src/lib/agent-harness.ts: +

+ p.symbol === 'USDC')?.confidentialTokenAddress || pairs[0].confidentialTokenAddress; + +// Shield (wrap) public USDC into confidential cUSDC +const shieldHash = await agent.shield(usdcWrapper, '10.0'); + +// Decrypt balance via EIP-712 permit & WASM decryption +const balance = await agent.getConfidentialBalance(usdcWrapper); + +// Send confidential cUSDC (client-side WASM encryption + FHE input proof) +const transferHash = await agent.transfer(usdcWrapper, '0xRecipient', '5.0');`} + /> + + + Critical Rule for AI Agents: When shielding (depositing), input amounts MUST use the underlying token's decimals (e.g. 6 for USDC, 18 for WETH). When unshielding (withdrawing) or transferring, amounts MUST always use the wrapper's fixed euint64 6-decimal scale. + + + ); +} diff --git a/src/app/app/docs/_docs/content/architecture.tsx b/src/app/app/docs/_docs/content/architecture.tsx new file mode 100644 index 0000000..c408cdd --- /dev/null +++ b/src/app/app/docs/_docs/content/architecture.tsx @@ -0,0 +1,70 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, Callout, ArchitectureDiagram, Reveal } from '../components'; + +export default function Architecture() { + return ( + <> + + ShadowLine is a non-custodial frontend. There is no backend that holds keys or funds — + everything happens between your browser, your wallet, the public RPC, and Zama's FHE + infrastructure. + + + + + + +

The four moving parts

+

+ Every action in the app resolves to a combination of these four lanes. Reads and writes to + public state go through Wagmi; anything involving an encrypted value goes through the Zama + SDK. +

+
    +
  • + ShadowLine UI — a Next.js app. It renders the registry, forms, and + balances. It never sees your private key and stores no secrets server-side. +
  • +
  • + Wagmi + viem — public RPC for standard EVM reads (allowances, ERC-20 + balances, registry pairs) and for sending transactions your wallet signs. +
  • +
  • + Zama React SDK — client-side FHE: it encrypts inputs before they hit the + chain, requests EIP-712 permits, and asks the Gateway to decrypt values that belong to + you. +
  • +
  • + Relayer / Gateway (KMS) — Zama's off-chain coprocessor. It performs + the heavy FHE work and produces the decryption proofs the fhEVM contracts verify on-chain. +
  • +
+ +

A shield, traced end-to-end

+

+ When you shield 100 USDC: the UI reads your allowance via Wagmi, sends an{' '} + approve() if needed, waits for the receipt, then calls the wrapper's{' '} + wrap(). The wrapper locks the ERC-20 and mints an encrypted euint64{' '} + balance to you. Nothing about the amount is readable on-chain afterward — only a ciphertext + handle exists. +

+ +

A decrypt, traced end-to-end

+

+ When you reveal a balance: the SDK asks your wallet for an off-chain EIP-712 signature (no + gas), derives a session key scoped to your address and that contract, and hands it to the + Gateway. The Gateway decrypts only your ciphertext and returns the plaintext to the + browser session. See EIP-712 Permits for the full sequence. +

+ + + Non-custodial by construction: there is no ShadowLine server in any of these + paths. If this site disappeared tomorrow, your tokens and wrappers would remain fully usable + directly against the on-chain contracts. + + + ); +} diff --git a/src/app/app/docs/_docs/content/decimal-scaling.tsx b/src/app/app/docs/_docs/content/decimal-scaling.tsx new file mode 100644 index 0000000..a2d95bd --- /dev/null +++ b/src/app/app/docs/_docs/content/decimal-scaling.tsx @@ -0,0 +1,85 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, CodeBlock, Callout, PropTable } from '../components'; + +export default function DecimalScaling() { + return ( + <> + + This is the most common source of bugs when integrating Zama FHE tokens. Read it carefully — + it is short. + + +

+ + FHE operates on euint64 + {' '} + — a 64-bit unsigned integer with a maximum of ~1.84 × 10¹⁹. A standard 18-decimal ERC-20 + represents 1.0 token as 10¹⁸; multiplied by any meaningful amount that overflows 64 bits + quickly. +

+

+ Therefore all ERC-7984 wrapper tokens use 6 decimals, regardless of the + underlying token's precision. The wrapper scales amounts automatically during shielding + and unshielding. +

+ + + Critical rule: when calling useShield, parse the amount with + the underlying token's decimals. When calling useUnshield, always + use 6 decimals (the wrapper decimals). When displaying a confidential + balance, always format with 6. + + +

Decision table

+ + + Shield (wrap) + + parseUnits(amount, underlyingDecimals) + + + {"parseUnits('1', 18)"} → 10¹⁸ + + + + Unshield (unwrap) + + parseUnits(amount, 6) + + + {"parseUnits('1', 6)"} → 10⁶ + + + + Display balance + + formatUnits(balance, 6) + + + {"formatUnits(1_000_000n, 6)"} → '1.0' + + + + + + + ); +} diff --git a/src/app/app/docs/_docs/content/errors.tsx b/src/app/app/docs/_docs/content/errors.tsx new file mode 100644 index 0000000..50995af --- /dev/null +++ b/src/app/app/docs/_docs/content/errors.tsx @@ -0,0 +1,69 @@ +'use client'; + +import React from 'react'; +import { Lead, CodeBlock, Callout, ErrorRow } from '../components'; + +export default function Errors() { + return ( + <> + + Use matchZamaError from @zama-fhe/sdk to classify SDK errors into + user-friendly messages. ShadowLine re-exports this via the classifyError(err){' '} + utility in src/lib/errors.ts. + + + ({ title: 'Declined', message: 'You cancelled the signature.' }), + INSUFFICIENT_ERC20_BALANCE: () => ({ title: 'Low Balance', message: 'Not enough tokens.' }), + _: (e) => ({ title: 'Error', message: e.message }), + }); + showToast(result); +}`} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error CodeTitleDescriptionRetry?
+ + + Wallet errors (non-SDK): common rejection strings like{' '} + user rejected, User denied, ACTION_REJECTED, and{' '} + user cancelled are caught by the fallback in classifyError() and + mapped to "Request Cancelled." + + + ); +} diff --git a/src/app/app/docs/_docs/content/faq.tsx b/src/app/app/docs/_docs/content/faq.tsx new file mode 100644 index 0000000..8a1dac1 --- /dev/null +++ b/src/app/app/docs/_docs/content/faq.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2 } from '../components'; + +function QA({ q, children }: { q: string; children: React.ReactNode }) { + return ( +
+

{q}

+

{children}

+
+ ); +} + +export default function Faq() { + return ( + <> + Short answers to the questions people ask most about confidential tokens. + + + Yes — on-chain it exists only as an encrypted euint64 handle. Validators, + indexers, and explorers see ciphertext, not a number. Only you, after signing a permit, can + decrypt it. What stays public is the interaction graph: that your address touched a wrapper, + and when. + + + + No. Decryption uses an off-chain EIP-712 signature. There is no transaction and no gas — + you're just proving to the Gateway that the ciphertext is yours. + + + + FHE works on 64-bit integers (euint64), which would overflow with 18-decimal + amounts. Wrappers standardize on 6 decimals and scale automatically. See{' '} + Decimal Scaling for the exact shield/unshield rule. + + + + Nothing is lost. The unwrap request is on-chain and the pending tx hash is saved locally. + Next visit, ShadowLine detects it and offers a Resume action to finalize. See{' '} + Shield & Unshield. + + + + Yes. It's non-custodial and open-source — every operation maps to public contract calls. + The REST API and the documented{' '} + SDK hooks let you build your own interface against the same + registry. + + + + Sepolia registry pairs are Zama-deployed mock tokens with a public mint(). Grab + free ones from the Faucet page, then shield them to try the full flow. + + + + Likely a USDT-style token that rejects changing a non-zero allowance directly to another + non-zero value. Zero the allowance first, then approve the real amount. ShadowLine does this + automatically; details are on Shield & Unshield. + + + ); +} diff --git a/src/app/app/docs/_docs/content/fhe.tsx b/src/app/app/docs/_docs/content/fhe.tsx new file mode 100644 index 0000000..008fbe5 --- /dev/null +++ b/src/app/app/docs/_docs/content/fhe.tsx @@ -0,0 +1,58 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, Callout, FheConceptDiagram, Reveal } from '../components'; + +export default function Fhe() { + return ( + <> + + The whole system rests on two ideas: a way to compute on encrypted data (FHE), and a token + standard that stores balances as ciphertext (ERC-7984). + + +

Fully Homomorphic Encryption

+

+ Fully Homomorphic Encryption (FHE) is a cryptographic scheme that allows + arbitrary computation on encrypted data without decrypting it first. Zama's{' '} + fhEVM is a modified Ethereum Virtual Machine that supports FHE operations + natively in Solidity — a contract can add two encrypted balances and get an encrypted sum, + never seeing either plaintext. +

+ +

The ERC-7984 standard

+

+ ERC-7984 is the confidential token standard built on the fhEVM. Instead of + storing balances as a public uint256, a wrapper contract stores them as{' '} + euint64 — an encrypted 64-bit integer. The plaintext is never on-chain; only the + token owner can decrypt it. +

+ + + + + + + Key properties of ERC-7984 tokens: +
    +
  • + Balances are on-chain ciphertexts — unreadable by validators, indexers, or block + explorers. +
  • +
  • Transfer amounts are encrypted — confidential even from recipients until decrypted.
  • +
  • Decryption requires the owner's EIP-712 permit.
  • +
  • The underlying ERC-20 is always 1:1 collateralized inside the wrapper contract.
  • +
+
+ +

What FHE does and does not hide

+

+ FHE hides values — balances and transfer amounts. It does not hide the{' '} + graph: the fact that address A interacted with a given wrapper contract, and + when, is still public, because transactions and their senders are public on Ethereum. See the{' '} + Security Model for the precise trust boundaries. +

+ + ); +} diff --git a/src/app/app/docs/_docs/content/index.tsx b/src/app/app/docs/_docs/content/index.tsx new file mode 100644 index 0000000..173b301 --- /dev/null +++ b/src/app/app/docs/_docs/content/index.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React from 'react'; +import Overview from './overview'; +import QuickStart from './quickstart'; +import Architecture from './architecture'; +import Fhe from './fhe'; +import DecimalScaling from './decimal-scaling'; +import Permits from './permits'; +import Shield from './shield'; +import Transfer from './transfer'; +import Registry from './registry'; +import Portfolio from './portfolio'; +import RestApi from './rest-api'; +import AiAgents from './ai-agents'; +import UseCases from './use-cases'; +import Addresses from './addresses'; +import Errors from './errors'; +import Security from './security'; +import Faq from './faq'; + +/** slug → body component. Keys must match `slug` values in nav.ts. */ +export const DOC_CONTENT: Record = { + overview: Overview, + quickstart: QuickStart, + architecture: Architecture, + fhe: Fhe, + 'decimal-scaling': DecimalScaling, + permits: Permits, + shield: Shield, + transfer: Transfer, + registry: Registry, + portfolio: Portfolio, + 'rest-api': RestApi, + 'ai-agents': AiAgents, + 'use-cases': UseCases, + addresses: Addresses, + errors: Errors, + security: Security, + faq: Faq, +}; diff --git a/src/app/app/docs/_docs/content/overview.tsx b/src/app/app/docs/_docs/content/overview.tsx new file mode 100644 index 0000000..77f45df --- /dev/null +++ b/src/app/app/docs/_docs/content/overview.tsx @@ -0,0 +1,65 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, Callout, FeatureGrid, Reveal } from '../components'; + +export default function Overview() { + return ( + <> + + ShadowLine is the canonical interface and developer toolkit for Zama's confidential + token ecosystem. It lets users and developers discover, wrap, unwrap, transfer, and decrypt + ERC-20 tokens that have been converted into confidential ERC-7984 wrappers using{' '} + Fully Homomorphic Encryption (FHE). + + + + + + +

Who this is for

+

+ Users get a simple UI to privatize on-chain balances: shield a token, send + it confidentially, and reveal balances only to yourself. Developers get a + public REST API, a documented set of React SDK hooks, and verified contract addresses so + they can build on the same registry ShadowLine does. +

+ +

How to read these docs

+

+ Start with the Quick Start if you want to ship + something today, or the Architecture and{' '} + FHE & ERC-7984 pages if you want to understand the + model first. The Guides walk through each product feature; the{' '} + Developers and Reference sections are the lookup material + you'll come back to. Use the Next button at the bottom of any page to read + straight through. +

+ + + ); +} diff --git a/src/app/app/docs/_docs/content/permits.tsx b/src/app/app/docs/_docs/content/permits.tsx new file mode 100644 index 0000000..1138727 --- /dev/null +++ b/src/app/app/docs/_docs/content/permits.tsx @@ -0,0 +1,87 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, CodeBlock, Callout, StepList, PermitFlowDiagram, Reveal } from '../components'; + +export default function Permits() { + return ( + <> + + Reading a confidential balance requires an EIP-712 typed-data signature from the token + owner's wallet. This signature authorizes the Zama Gateway to decrypt the ciphertext and + return the plaintext to the frontend session. It is off-chain — no gas, no transaction. + + + + Security rule — never auto-fire permits. Every call to{' '} + useConfidentialBalance or useConfidentialBalances with{' '} + enabled: true immediately requests a wallet signature. Always gate it behind an + explicit decryptRequested boolean that is only set true on a user + click. + + + + + + +

How it works

+ + + + Reset on token change: when the user switches the selected token, reset{' '} + decryptRequested synchronously in the onChange handler — not only + in a useEffect. A one-frame delay in the effect can let the old true{' '} + combine with the new token address and auto-fire a permit. + + + { + setSelectedToken(newToken); + setDecryptRequested(false); // ← same handler, not a useEffect +}; + +const { data: balance } = useConfidentialBalance({ + tokenAddress: selectedToken.erc7984Address, + enabled: decryptRequested && !!address, // ← explicit gate +});`} + /> + +

Rejections are terminal — do not retry

+

+ If the user declines the signature, treat it as done: re-arm the button and wait for another + click. Do not re-fire the query on error, on window focus, or on remount — that produces the + "wallet keeps popping up" loop. ShadowLine disables the query after any decrypt + error and only re-enables it on a fresh click. +

+ + ); +} diff --git a/src/app/app/docs/_docs/content/portfolio.tsx b/src/app/app/docs/_docs/content/portfolio.tsx new file mode 100644 index 0000000..a5b17aa --- /dev/null +++ b/src/app/app/docs/_docs/content/portfolio.tsx @@ -0,0 +1,70 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, CodeBlock, Callout } from '../components'; + +export default function Portfolio() { + return ( + <> + + The Portfolio shows everything you hold across the registry and + lets you reveal every confidential balance with a single signature. + + +

Batch decryption

+

+ Decrypting balances one-by-one would prompt your wallet once per token. Instead, the + portfolio uses useConfidentialBalances to cover many wrappers under a single + EIP-712 permit — one click, one signature, every official balance revealed. +

+ setDecryptRequested(true)}>Decrypt All; + } + return ( +
    + {wrappers.map((w) => ( +
  • {formatUnits(balances?.[w.toLowerCase()] ?? 0n, 6)}
  • + ))} +
+ ); +}`} + /> + +

Three kinds of holdings

+
    +
  • + Official wrappers — verified registry pairs, batch-decrypted together. +
  • +
  • + Custom wrappers — your locally-added pairs that support shield/unshield. +
  • +
  • + Custom decrypt-only — confidential tokens with no ERC-20 underlying; each + card runs its own per-row decrypt. +
  • +
+ + + Session reset: the wallet menu's "Reset Decryption Session" + wipes cached FHE permits app-wide. The next decrypt then prompts for a fresh wallet + signature — useful if you switch accounts or want to re-arm every gate at once. + + + ); +} diff --git a/src/app/app/docs/_docs/content/quickstart.tsx b/src/app/app/docs/_docs/content/quickstart.tsx new file mode 100644 index 0000000..c690dfe --- /dev/null +++ b/src/app/app/docs/_docs/content/quickstart.tsx @@ -0,0 +1,161 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, H2, P, UL, Callout, StepList, CodeBlock } from '../components'; + +export default function QuickStart() { + return ( + <> + + ShadowLine is ready to use — no installation required. Connect a wallet, pick a token pair, + and your first confidential balance is on-chain in under a minute. + + +

1. Connect your wallet

+

+ Open the Registry and click Connect in the top + right. ShadowLine works with any EIP-1193 wallet (MetaMask, Rabby, WalletConnect). Switch + the network toggle to Sepolia to use free test tokens, or{' '} + Mainnet for real assets. +

+ + First time on Sepolia? Head to the Faucet page and mint free + mock tokens — no ETH required, one click per token. + + +

2. Browse the Registry and pick a pair

+

+ The Registry lists every verified ERC-20 ↔ ERC-7984 wrapper pair + registered on-chain. Each card shows the public token on top and its confidential wrapper + below. Use the search bar to find a token by symbol or address. +

+

+ Click Shield on any public-token row to go directly to that pair on the{' '} + Wrapper page. +

+ +

3. Shield — wrap ERC-20 into a confidential token

+

+ On the Wrapper page, enter an amount and click{' '} + Shield. Two wallet prompts follow: +

+ +

+ The Zama Gateway finalizes the ciphertext in a few seconds. Your confidential balance appears + in the Registry after you click Decrypt and sign a + read-only EIP-712 permit. +

+ +

4. Transfer or decrypt

+

+ Once shielded, you have two options: +

+
    +
  • + Confidential Transfer — go to Transfer, + pick the confidential token, enter a recipient and amount. The amount is encrypted + client-side before submission. On-chain observers see the addresses but never the value. +
  • +
  • + Decrypt balance — on the Registry or Portfolio page, click{' '} + Decrypt next to any confidential row. Sign the EIP-712 permit in your + wallet. No tokens move; the balance is decrypted only inside your browser session. +
  • +
+ +

5. Unshield when you are done

+

+ To recover your original ERC-20, go to the Wrapper page, + switch to Unshield, and enter the amount. The Zama Gateway decrypts + on-chain and releases the underlying ERC-20 back to your wallet (typically 30–60 seconds). + If the page closes mid-flow, the Resume banner re-appears automatically on + your next visit. +

+ + + Decimal note: shield amounts use the underlying token's + decimals (e.g. 6 for USDC, 18 for WETH). Unshield always uses the wrapper's fixed{' '} + 6-decimal scale. See Decimal Scaling for the + full rule. + + +

For Developers: Drop-in SDK Hook

+

+ Want to integrate confidential asset shielding into your own dApp without writing boilerplate contract or relayer code? We created a zero-boilerplate drop-in React hook: useShadowline(). +

+

+ Simply copy src/lib/use-shadowline.ts into your React, Next.js, or Wagmi project to get instant access to verified contract pairs, automatic ERC-20 allowances, and one-click shielding/unshielding: +

+ +

Available Confidential Assets ({pairs.length})

+ {pairs.map((pair) => ( +
+ {pair.symbol} ↔ c{pair.symbol} + +
+ ))} +
+ ); +}`} + /> +

0-to-100 Automated Setup & Deployment

+

+ Want to run ShadowLine locally or deploy to a cloud node / VPS in under 1 minute? We built an automated cross-platform wizard that handles dependency checking, environment configuration (.env.local), production build verification, and server launching. +

+

+ Linux Ubuntu & macOS (Bash / Zsh): +

+ +

+ Windows (PowerShell & CMD): +

+ +

+ 1-Line Auto-Installers (with Automatic Prerequisite Installation): +

+ +

+ Docker & VPS Self-Hosting: +

+ + + ); +} diff --git a/src/app/app/docs/_docs/content/registry.tsx b/src/app/app/docs/_docs/content/registry.tsx new file mode 100644 index 0000000..999a200 --- /dev/null +++ b/src/app/app/docs/_docs/content/registry.tsx @@ -0,0 +1,55 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, Callout } from '../components'; + +export default function Registry() { + return ( + <> + + The Registry is the front door: a live view of every ERC-20 ↔ ERC-7984 + pair, read straight from the on-chain WrappersRegistry for the selected network. + + +

Official vs. custom pairs

+

+ ShadowLine keeps two kinds of pairs strictly separated so a token you added locally can never + be mistaken for a verified one. +

+
    +
  • + Official Registry — pairs verified on-chain by the WrappersRegistry + contract. On Sepolia these are Zama-deployed mock tokens (each carries a{' '} + Mock badge and has a public mint() you can use from the Faucet). +
  • +
  • + Custom / dev-only — pairs you add yourself, stored locally in your + browser and scoped per chain. They never mix into the official list. +
  • +
+ +

Network scoping

+

+ The Testnet/Mainnet switch in the header controls which registry is shown. Sepolia pairs and + Mainnet pairs are never displayed together, and only addresses for the active network appear + on each row — no cross-network placeholders. +

+ +

Adding a custom token

+

+ You can register any ERC-7984 token by address. ShadowLine validates it is genuinely + confidential: it tries ERC-165 first, and falls back to a behavioral probe of{' '} + confidentialBalanceOf() for tokens that don't implement ERC-165. A wrapper + (one with an underlying()) gets Shield/Unshield actions; a decrypt-only + confidential token gets its own per-row decrypt. +

+ + + Prefer to build against the registry programmatically? The{' '} + REST API returns the same official pairs as JSON with no + wallet required, and useListPairs gives you the live list inside a React app. + + + ); +} diff --git a/src/app/app/docs/_docs/content/rest-api.tsx b/src/app/app/docs/_docs/content/rest-api.tsx new file mode 100644 index 0000000..8a6d2b0 --- /dev/null +++ b/src/app/app/docs/_docs/content/rest-api.tsx @@ -0,0 +1,113 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Lead, P, H2, H4, CodeBlock, EndpointBadge, PropTable, PropRow } from '../components'; + +function useAppUrl() { + const [url, setUrl] = useState(process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? ''); + useEffect(() => { setUrl(window.location.origin); }, []); + return url; +} + +export default function RestApi() { + const APP_URL = useAppUrl(); + return ( + <> + + ShadowLine exposes a public REST API for querying the on-chain registry. No SDK, no wallet, + no authentication — just a fetch(). + + +

GET /api/registry

+ +

+ Returns all registered ERC-20 ↔ ERC-7984 wrapper pairs for the specified chain. Data is read + directly from the on-chain WrappersRegistry and cached for 60 seconds + (stale-while-revalidate 300s). +

+ +

Query parameters

+ + + + +

Response schema

+ + + + + + + + + + +

PairResult object

+ + + + + + + + + + +

Examples

+ + + c{pair['symbol']:8} | decimals: {pair['decimals']}/{pair['wrapperDecimals']}")`} + /> + +

HTTP headers

+ + + + Cache-Control + + public, s-maxage=60, stale-while-revalidate=300 + + + + Access-Control-Allow-Origin + + * (CORS open) + + + + ); +} diff --git a/src/app/app/docs/_docs/content/security.tsx b/src/app/app/docs/_docs/content/security.tsx new file mode 100644 index 0000000..4f2cbcb --- /dev/null +++ b/src/app/app/docs/_docs/content/security.tsx @@ -0,0 +1,87 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, H2, UL, Callout, PropTable } from '../components'; + +export default function Security() { + return ( + <> + + ShadowLine is a non-custodial interface over audited, open-source contracts. Understanding + exactly what is private, what is public, and who you trust is the point of this page. + + +

What stays private

+
    +
  • + Balances — stored on-chain as euint64 ciphertext. Not + readable by validators, indexers, or explorers. +
  • +
  • + Transfer amounts — encrypted before submission; the transaction carries a + ciphertext, not a number. +
  • +
+ +

What is public

+
    +
  • + Addresses and the interaction graph — that your address interacted with a + given wrapper, and when. FHE hides values, not the fact that a transaction happened. +
  • +
  • + The underlying ERC-20 movements at shield/unshield boundaries: the moment + you wrap or unwrap, the public leg (the ERC-20 lock or release) is a normal, visible + transfer. +
  • +
+ +

Trust boundaries

+ + + ShadowLine frontend + Only after you sign a permit, in your session + No — every transfer is signed by your wallet + + + Zama Gateway / Relayer + Decrypts only ciphertext your session key authorizes + No custody of funds + + + Public RPC / validators + No — only ciphertext handles are on-chain + No + + + +

Design guarantees

+
    +
  • + Non-custodial: no ShadowLine server holds keys or funds. Tokens are locked + inside the open-source ERC-7984 wrapper contracts. +
  • +
  • + 1:1 collateralization: every confidential unit is backed by an underlying + ERC-20 held in the wrapper. +
  • +
  • + Explicit decryption: balances are only revealed via an EIP-712 permit you + sign — the app never auto-decrypts. +
  • +
  • + Private keys never leave your wallet: the frontend requests signatures; it + never sees your key. +
  • +
+ + + What ShadowLine does not claim: it is an interface, not a new protocol. + Confidentiality guarantees come from Zama's fhEVM and the ERC-7984 contracts. Always + verify contract addresses (see Contract Addresses) and + never enter seed phrases or private keys into any website. + + + ); +} diff --git a/src/app/app/docs/_docs/content/shield.tsx b/src/app/app/docs/_docs/content/shield.tsx new file mode 100644 index 0000000..0e1b236 --- /dev/null +++ b/src/app/app/docs/_docs/content/shield.tsx @@ -0,0 +1,78 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, CodeBlock, Callout, ShieldFlowDiagram, Reveal } from '../components'; + +export default function Shield() { + return ( + <> + + Shielding wraps a public ERC-20 into its confidential ERC-7984 form; unshielding does the + reverse. In the app both live on the Wrap page. + + + + + + +

Shield (wrap)

+

+ A shield is a two-transaction dance: an ERC-20 approve() so the wrapper can pull + your tokens, then wrap(), which locks the ERC-20 and mints you an encrypted{' '} + euint64 balance. ShadowLine runs the approval, waits for its receipt, refreshes + the allowance, then wraps — passing approvalStrategy: 'skip' so the SDK + doesn't try to approve again. +

+ + + + USDT-style tokens: some ERC-20s (real USDT, and this app's USDTMock + which replicates it) revert an approve() that changes a non-zero allowance + straight to another non-zero value. If an allowance is already outstanding, zero it first + (approve(spender, 0), await the receipt), then approve the real amount. Standard + tokens with a zero allowance are unaffected. + + +

Unshield (unwrap)

+

+ Unshielding is two phases. First an on-chain unwrap() request burns your + ciphertext and registers the intent. Then Zama's Gateway produces a decryption proof and + finalizes the unwrap — typically ~30–60 seconds later — releasing the underlying ERC-20 back + to your address. +

+
    +
  • Amounts for unshield always use 6 decimals (wrapper decimals).
  • +
  • + Because finalization is asynchronous, the pending unwrap tx hash is persisted so the + operation can be resumed if you close the tab. +
  • +
+ +

Resuming an interrupted unshield

+

+ The SDK does not auto-persist the pending unwrap. ShadowLine saves the unwrap tx hash the + moment it's submitted; on the next visit it reads it back with{' '} + loadPendingUnshield and offers a Resume action wired to{' '} + useResumeUnshield. See the SDK Hooks page for the + exact signatures. +

+ + + The activity feed on the Wrap page auto-refreshes a few seconds after a shield or unshield + confirms, so a fresh transaction appears without a manual reload. + + + ); +} diff --git a/src/app/app/docs/_docs/content/transfer.tsx b/src/app/app/docs/_docs/content/transfer.tsx new file mode 100644 index 0000000..09c8398 --- /dev/null +++ b/src/app/app/docs/_docs/content/transfer.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, CodeBlock, Callout } from '../components'; + +export default function Transfer() { + return ( + <> + + A confidential transfer moves ERC-7984 tokens where the amount is encrypted on-chain — hidden + from block explorers and even from the recipient until they decrypt it. In the app this is + the Transfer page. + + +

How it differs from a normal transfer

+
    +
  • + The amount is encrypted client-side before it's submitted, so the transaction carries + a ciphertext, not a number. +
  • +
  • + The sender and recipient addresses are still public — FHE hides the value, not the graph. +
  • +
  • + The recipient must run their own decrypt (an EIP-712 permit) to learn how much they + received. +
  • +
+ +

Using the SDK

+

+ useConfidentialTransfer takes the wrapper address and returns a mutation. The + SDK encrypts the amount, then submits — you get lifecycle callbacks for each phase. +

+ { + await transfer({ + to, + // wrapper decimals are always 6 + amount: parseUnits('25', 6), + onEncryptComplete: () => console.log('amount encrypted, submitting…'), + onTransferSubmitted: (hash) => console.log('submitted:', hash), + }); + }; + + return ; +}`} + /> + + + The Transfer page also supports a standard public ERC-20 transfer mode for the underlying + token, so you can move either the public or the confidential side from one place. + + + + You can only send what you hold confidentially. If your confidential balance is lower than + the amount, the transfer reverts with{' '} + INSUFFICIENT_CONFIDENTIAL_BALANCE — see the{' '} + Error Reference. + + + ); +} diff --git a/src/app/app/docs/_docs/content/use-cases.tsx b/src/app/app/docs/_docs/content/use-cases.tsx new file mode 100644 index 0000000..715336b --- /dev/null +++ b/src/app/app/docs/_docs/content/use-cases.tsx @@ -0,0 +1,114 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, H2, P, UL, Callout, FeatureGrid } from '../components'; + +export default function UseCases() { + return ( + <> + + ERC-7984 confidential tokens make on-chain amounts invisible to everyone except the holder. + Here are the real-world patterns ShadowLine is designed to enable. + + +

Private payroll & compensation

+

+ Companies paying contributors on-chain today expose every salary to public scrutiny — anyone + can track an address and reconstruct the full comp structure. Wrapping payroll tokens into + confidential wrappers keeps amounts encrypted on-chain. The recipient holds the ciphertext; + only they can decrypt the value with an EIP-712 permit. Attestations (hire date, role) can + remain on-chain without leaking the number itself. +

+ +

Sealed-bid auctions

+

+ Traditional on-chain auctions require bids to be public, enabling sniping and last-second + manipulation. With ERC-7984 wrappers, each bid is an encrypted amount submitted to a smart + contract. The contract performs comparisons on ciphertext — no participant learns another + bid until the auctioneer chooses to finalize. ShadowLine's shield flow handles the + ERC-20 → confidential conversion that feeds into such contracts. +

+ +

DAO treasury & budget privacy

+

+ DAOs frequently need to approve grants or operational spending without surfacing exact + numbers to competitors or exploiters before execution. Confidential token flows let a + multi-sig hold and transfer budget allocations as encrypted balances. The DAO's + governance rules stay on-chain; the amounts move privately until finalization. +

+ +

Front-run resistant DeFi

+

+ Any large swap, liquidity provision, or liquidation on a public mempool is visible before + it lands. Wrapping the input amount keeps MEV bots blind to the size of the upcoming + trade. The ciphertext is only decrypted inside the EVM at execution time — by then the + block is already sealed. +

+ +

Private P2P payments

+

+ Sending money between wallets reveals the amount to every block explorer, data aggregator, + and anyone who knows either address. A confidential transfer (see{' '} + Confidential Transfer) submits an encrypted amount — + the recipient must run their own decrypt to learn what they received, and observers see + only that a transaction occurred. +

+ +

Vesting & lockup schedules

+

+ Token vesting contracts that hold large allocations are targets for social engineering and + market manipulation once balances are known. Wrapping vested amounts as confidential tokens + removes the live balance signal. The cliff and linear schedule logic stays on-chain; only + the holder can reveal what has vested so far. +

+ + + All of these patterns share one foundation: ERC-20 tokens are locked inside the ERC-7984 + wrapper (1:1 collateralized) and the encrypted handle is what moves on-chain. ShadowLine is + the interface that makes shielding, unshielding, and transferring those handles easy. + + +

Building on ShadowLine

+ + +
    +
  • + REST API — wallet-free pair discovery +
  • +
  • + Contract Addresses — Sepolia and Mainnet +
  • +
  • + Registry & Discovery — adding custom pairs +
  • +
  • + Security Model — trust boundaries +
  • +
+ + ); +} diff --git a/src/app/app/docs/_docs/nav.ts b/src/app/app/docs/_docs/nav.ts new file mode 100644 index 0000000..0a53272 --- /dev/null +++ b/src/app/app/docs/_docs/nav.ts @@ -0,0 +1,215 @@ +/** + * Single source of truth for the docs navigation. + * + * Every doc "page" is one entry here. The sidebar renders them grouped; the + * prev/next pager walks the FLAT order below. Adding a page = adding one entry + * (plus its content component in `content/` and a case in the `[slug]` route). + * + * Routing: the first item (`overview`) is the index route `/app/docs`. Every + * other item is a real subpage at `/app/docs/`. + */ + +export type DocGroup = + | 'Getting Started' + | 'Core Concepts' + | 'Guides' + | 'Developers' + | 'Reference'; + +export interface DocEntry { + /** URL slug. `overview` maps to the index route `/app/docs`. */ + slug: string; + /** Sidebar + page title. */ + label: string; + /** Sidebar group heading. */ + group: DocGroup; + /** Small eyebrow shown above the page title. */ + eyebrow: string; + /** One-line page summary rendered under the title. */ + description: string; +} + +/** + * FLAT, ordered list — drives both the grouped sidebar and the prev/next pager. + * Order here IS the reading order. + */ +export const DOC_ENTRIES: DocEntry[] = [ + // ── Getting Started ───────────────────────────────────────────── + { + slug: 'overview', + label: 'Overview', + group: 'Getting Started', + eyebrow: 'Introduction', + description: + 'What ShadowLine is, who it is for, and how the confidential-token pieces fit together.', + }, + { + slug: 'quickstart', + label: 'Quick Start', + group: 'Getting Started', + eyebrow: 'Getting Started', + description: + 'Connect a wallet, browse the Registry, shield your first token, and transfer confidentially.', + }, + { + slug: 'architecture', + label: 'Architecture', + group: 'Getting Started', + eyebrow: 'Getting Started', + description: + 'How the browser, wallet, Zama Relayer/Gateway, and the fhEVM contracts talk to each other.', + }, + + // ── Core Concepts ─────────────────────────────────────────────── + { + slug: 'fhe', + label: 'FHE & ERC-7984', + group: 'Core Concepts', + eyebrow: 'Concept', + description: + 'Fully Homomorphic Encryption, the fhEVM, and the confidential-token standard ShadowLine is built on.', + }, + { + slug: 'decimal-scaling', + label: 'Decimal Scaling', + group: 'Core Concepts', + eyebrow: 'Concept', + description: + 'Why every confidential wrapper is 6 decimals, and the exact rule for shield vs. unshield amounts.', + }, + { + slug: 'permits', + label: 'EIP-712 Permits', + group: 'Core Concepts', + eyebrow: 'Concept', + description: + 'How a read-only signature lets only you decrypt your own balance — and how to never fire it by accident.', + }, + + // ── Guides ────────────────────────────────────────────────────── + { + slug: 'shield', + label: 'Shield & Unshield', + group: 'Guides', + eyebrow: 'Guide', + description: + 'The full wrap/unwrap lifecycle: approval, shielding, the two-phase unshield, and interrupted-op resume.', + }, + { + slug: 'transfer', + label: 'Confidential Transfer', + group: 'Guides', + eyebrow: 'Guide', + description: + 'Send confidential tokens where the amount is encrypted on-chain — hidden even from the recipient.', + }, + { + slug: 'registry', + label: 'Registry & Discovery', + group: 'Guides', + eyebrow: 'Guide', + description: + 'How pairs are discovered on-chain, official vs. custom tokens, and adding your own wrapper.', + }, + { + slug: 'portfolio', + label: 'Portfolio & Decryption', + group: 'Guides', + eyebrow: 'Guide', + description: + 'View your holdings and batch-decrypt every confidential balance with a single signature.', + }, + + // ── Developers ────────────────────────────────────────────────── + { + slug: 'ai-agents', + label: 'AI Agents & LLMs', + group: 'Developers', + eyebrow: 'Developers', + description: + 'Discover verified asset pairs via llms.txt and OpenAI manifests, and execute MEV-resistant confidential DeFi operations.', + }, + { + slug: 'rest-api', + label: 'REST API', + group: 'Developers', + eyebrow: 'Developers', + description: + 'A public, wallet-free GET endpoint that returns every registered wrapper pair as JSON.', + }, + { + slug: 'use-cases', + label: 'Use Cases', + group: 'Developers', + eyebrow: 'Developers', + description: + 'Real-world patterns enabled by ERC-7984 confidential tokens: payroll, auctions, DAO treasury, and more.', + }, + + // ── Reference ─────────────────────────────────────────────────── + { + slug: 'addresses', + label: 'Contract Addresses', + group: 'Reference', + eyebrow: 'Reference', + description: + 'Registry and wrapper-pair addresses for Sepolia and Ethereum Mainnet, with explorer links.', + }, + { + slug: 'errors', + label: 'Error Reference', + group: 'Reference', + eyebrow: 'Reference', + description: + 'Every Zama SDK error code, whether it is retryable, and how ShadowLine maps it to a message.', + }, + { + slug: 'security', + label: 'Security Model', + group: 'Reference', + eyebrow: 'Reference', + description: + 'Trust boundaries, what stays private, what is public, and the guarantees ShadowLine does — and does not — make.', + }, + { + slug: 'faq', + label: 'FAQ', + group: 'Reference', + eyebrow: 'Reference', + description: 'Short answers to the questions people ask most about confidential tokens.', + }, +]; + +/** Ordered list of group headings, matching first appearance in DOC_ENTRIES. */ +export const DOC_GROUPS: DocGroup[] = [ + 'Getting Started', + 'Core Concepts', + 'Guides', + 'Developers', + 'Reference', +]; + +/** Route href for a slug. `overview` is the index route. */ +export function hrefForSlug(slug: string): string { + return slug === 'overview' ? '/app/docs' : `/app/docs/${slug}`; +} + +/** Look up an entry by slug. */ +export function getEntry(slug: string): DocEntry | undefined { + return DOC_ENTRIES.find((e) => e.slug === slug); +} + +/** Prev/next neighbours in reading order, for the pager. */ +export function getNeighbours(slug: string): { prev?: DocEntry; next?: DocEntry } { + const i = DOC_ENTRIES.findIndex((e) => e.slug === slug); + if (i === -1) return {}; + return { + prev: i > 0 ? DOC_ENTRIES[i - 1] : undefined, + next: i < DOC_ENTRIES.length - 1 ? DOC_ENTRIES[i + 1] : undefined, + }; +} + +/** Every slug except the index — used to generate the [slug] subpages. */ +export const SUBPAGE_SLUGS: string[] = DOC_ENTRIES.filter((e) => e.slug !== 'overview').map( + (e) => e.slug, +); diff --git a/src/app/app/docs/layout.tsx b/src/app/app/docs/layout.tsx new file mode 100644 index 0000000..d8a93d6 --- /dev/null +++ b/src/app/app/docs/layout.tsx @@ -0,0 +1,19 @@ +'use client'; + +/** + * Persistent docs shell: a sticky grouped sidebar + the scrollable content + * column. `children` is the current subpage (wrapped by template.tsx, which + * handles the per-navigation entrance animation). + */ + +import React from 'react'; +import Sidebar from './_docs/Sidebar'; + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/src/app/app/docs/page.tsx b/src/app/app/docs/page.tsx new file mode 100644 index 0000000..b569166 --- /dev/null +++ b/src/app/app/docs/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { DocPage } from './_docs/components'; +import Overview from './_docs/content/overview'; + +/** Docs index → the Overview page. Real subpages live under `/app/docs/[slug]`. */ +export default function DocsIndexPage() { + return ( + + + + ); +} diff --git a/src/app/app/docs/template.tsx b/src/app/app/docs/template.tsx new file mode 100644 index 0000000..ed12b65 --- /dev/null +++ b/src/app/app/docs/template.tsx @@ -0,0 +1,26 @@ +'use client'; + +/** + * Next.js re-mounts a `template` on every navigation — perfect for a fresh + * entrance animation per docs page. We also reset scroll to the top so a long + * page doesn't open half-way down after clicking Next. + */ + +import React, { useEffect } from 'react'; +import { motion } from 'framer-motion'; + +export default function DocsTemplate({ children }: { children: React.ReactNode }) { + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'auto' }); + }, []); + + return ( + + {children} + + ); +} diff --git a/src/app/faucet/page.tsx b/src/app/app/faucet/page.tsx similarity index 92% rename from src/app/faucet/page.tsx rename to src/app/app/faucet/page.tsx index 2d3fac7..1769e93 100644 --- a/src/app/faucet/page.tsx +++ b/src/app/app/faucet/page.tsx @@ -1,13 +1,13 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Badge from '@/components/ui/Badge'; import Modal from '@/components/ui/Modal'; import TokenIcon from '@/components/ui/TokenIcon'; -import { KNOWN_WRAPPERS } from '@/config/contracts'; -import { useActiveNetwork } from '@/app/ClientLayout'; +import { useRegistryPairs, isMintablePair } from '@/lib/registry'; +import { classifyError } from '@/lib/errors'; import { useToast } from '@/components/ui/Toast'; import { useAccount, @@ -56,7 +56,17 @@ export default function FaucetPage() { const { switchChain } = useSwitchChain(); const { addToast } = useToast(); - const wrappers = KNOWN_WRAPPERS[sepolia.id] ?? []; + // Faucet is Sepolia-only by design. Read pairs from the on-chain registry + // (or the hardcoded fallback when no wallet is connected to Sepolia), + // then keep only the pairs whose underlying ERC-20 is a mock — only mocks + // expose a public `mint(address,uint256)`. Restricted pairs (e.g. the + // real `ctGBP` at `0x167D…A208` on Sepolia) are filtered out here so the + // user is never offered an action that will revert on-chain. + const { pairs: allPairs } = useRegistryPairs(sepolia.id); + const wrappers = useMemo( + () => allPairs.filter((p) => p.isValid !== false && isMintablePair(p)), + [allPairs], + ); const selectedWrapper = wrappers.find(w => w.symbol === selectedToken); // Wagmi contract writing hook @@ -126,13 +136,14 @@ export default function FaucetPage() { title: 'Faucet Request Submitted', message: 'Transaction sent to the network. Minting mock tokens...', }); - } catch (err: any) { + } catch (err: unknown) { console.error(err); setIsRequestPending(false); + const classified = classifyError(err); addToast({ - variant: 'error', - title: 'Faucet Request Failed', - message: err.message || 'The faucet mint transaction was rejected.', + variant: classified.retryable ? 'warning' : 'error', + title: classified.title, + message: classified.message, }); } }; @@ -291,7 +302,7 @@ export default function FaucetPage() { Transaction pending...
{children}; +} diff --git a/src/app/app/learn/page.tsx b/src/app/app/learn/page.tsx new file mode 100644 index 0000000..3061e5d --- /dev/null +++ b/src/app/app/learn/page.tsx @@ -0,0 +1,789 @@ +'use client'; + +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import Link from 'next/link'; +import Card from '@/components/ui/Card'; +import Badge from '@/components/ui/Badge'; +import Button from '@/components/ui/Button'; +import Tooltip from '@/components/ui/Tooltip'; +import BlurIn from '@/components/ui/BlurIn'; +import confetti from 'canvas-confetti'; +import { + BookOpen, + Shield, + Unlock, + Eye, + Droplets, + ChevronRight, + ChevronLeft, + CheckCircle2, + ExternalLink, + Lock, + Cpu, + ArrowRight, + Sparkles, + PartyPopper, +} from 'lucide-react'; + +/* ─── Step definitions ──────────────────────────────────────────────────────── */ + +interface Step { + id: number; + title: string; + subtitle: string; + icon: React.ReactNode; + accentColor: string; +} + +const STEPS: Step[] = [ + { + id: 1, + title: 'What is FHE?', + subtitle: 'Fully Homomorphic Encryption', + icon: , + accentColor: 'var(--accent)', + }, + { + id: 2, + title: 'Get Test Tokens', + subtitle: 'Free mock tokens on Sepolia', + icon: , + accentColor: 'var(--info)', + }, + { + id: 3, + title: 'Shield a Token', + subtitle: 'Wrap ERC-20 → ERC-7984', + icon: , + accentColor: 'var(--success)', + }, + { + id: 4, + title: 'Decrypt Balance', + subtitle: 'EIP-712 permit flow', + icon: , + accentColor: '#a78bfa', + }, + { + id: 5, + title: 'Unshield Back', + subtitle: 'Unwrap ERC-7984 → ERC-20', + icon: , + accentColor: 'var(--warning)', + }, +]; + +/* ─── Individual step content ───────────────────────────────────────────────── */ + +function StepContent({ stepId }: { stepId: number }) { + switch (stepId) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + default: + return null; + } +} + +function StepFHE() { + return ( +
+

+ Fully Homomorphic Encryption (FHE) allows computations + on encrypted data — without ever decrypting it. Zama's protocol + brings this to Ethereum: your token balances and transfers are encrypted + on-chain so that nobody, not even validators or block explorers, can see + your holdings. +

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

Key Terms

+
+ + + + +
+
+
+ ); +} + +function StepFaucet() { + return ( +
+

+ Before you can shield tokens, you need some test tokens. ShadowLine + includes a Faucet page that lets you mint free mock + tokens on the Sepolia testnet — no cost, no limits. +

+ +
+ + + + +
+ + +
+ Tip + + Mock tokens have a "Mock" badge in the UI. They behave identically to + real tokens for testing shielding and decryption flows. + +
+
+ +
+ + + +
+
+ ); +} + +function StepShield() { + return ( +
+

+ Shielding{' '} + (also called "wrapping") converts your public + ERC-20 tokens into confidential ERC-7984 tokens. Your balance becomes + encrypted on-chain — invisible to everyone except you. +

+ +
+ + + + Two transactions: first an ERC-20 approval (allows + the wrapper contract to spend your tokens), then the{' '} + shield transaction itself. + + The approval step uses the standard ERC-20 approve(){' '} + function. You only need to approve once per token unless you + revoke the allowance. + + } + /> + + } + /> + +
+ +
+

+ + Decimal Scaling +

+

+ All wrapper tokens use 6 decimals regardless of the + underlying token's precision (which may be 18). This is because FHE + operates on euint64 — a 64-bit integer that would overflow + at large 18-decimal values. The wrapper contract automatically scales + amounts during shield and unshield. +

+
+ +
+ + + +
+
+ ); +} + +function StepDecrypt() { + return ( +
+

+ Your confidential balance is encrypted on-chain. To view it, you need to{' '} + sign an EIP-712 permit — a typed off-chain signature + that authorizes the Zama Gateway to decrypt your balance and return the + plaintext to your browser. +

+ +
+ + + Your wallet will show a typed data signature request. This creates a{' '} + temporary session key that the Zama Gateway uses to + decrypt. Your private key never leaves your wallet. + + } + /> + +
+ +
+

+ + Privacy Guarantee +

+

+ The EIP-712 permit is off-chain — it is not a + transaction and costs no gas. The signature is scoped to your wallet + address and a specific contract, so it cannot be reused by anyone else. + The Zama Gateway decrypts the ciphertext using your session key and + returns the result exclusively to your browser session. +

+
+ + +
+ Important + + ShadowLine never auto-fires permit signatures. You + always click "Decrypt" first — your wallet only prompts when you + explicitly request it. + +
+
+
+ ); +} + +function StepUnshield() { + return ( +
+

+ Unshielding{' '} + (also called "unwrapping") converts your + confidential ERC-7984 tokens back into public ERC-20 tokens. This is a + two-step process: an on-chain request followed by finalization. +

+ +
+ + + + The unshield is a two-phase process: the unwrap + request goes on-chain, then the Zama Gateway processes it and triggers + finalization. This typically takes 30–60 seconds. + + The Zama Gateway needs to decrypt the encrypted amount to verify + you have sufficient balance, then sends a finalization transaction. + If you close the browser during this window, use the{' '} + "Resume Unshield" banner to complete it later. + + } + /> + + } + /> + +
+ +
+

+ + Interrupted Unshield? +

+

+ If you close your browser between the unwrap request and finalization, + don't worry — ShadowLine detects pending unshields automatically and + shows a yellow "Resume Unshield" banner. Click + "Resume" to complete the process. Your tokens are never lost. +

+
+ +
+ + + + + + +
+
+ ); +} + +/* ─── Reusable sub-components ───────────────────────────────────────────────── */ + +function DiagramBox({ + label, + description, + color, + icon, +}: { + label: string; + description: string; + color: string; + icon: React.ReactNode; +}) { + return ( +
+
{icon}
+
{label}
+
{description}
+
+ ); +} + +function HighlightCard({ title, description }: { title: string; description: string }) { + return ( + +

+ {title} +

+

+ {description} +

+
+ ); +} + +function TermDef({ term, definition }: { term: string; definition: string }) { + return ( +
+ {term} + {definition} +
+ ); +} + +function InstructionStep({ + number, + title, + description, +}: { + number: number; + title: string; + description: React.ReactNode; +}) { + return ( +
+
{number}
+
+
+ {title} +
+
+ {description} +
+
+
+ ); +} + +/* ─── Main page component ───────────────────────────────────────────────────── */ + +export default function LearnPage() { + const [activeStep, setActiveStep] = useState(1); + const [completedSteps, setCompletedSteps] = useState>(new Set()); + const hasConfettiFired = useRef(false); + + const currentStep = STEPS.find((s) => s.id === activeStep)!; + + const goTo = (id: number) => { + setActiveStep(id); + }; + + const goNext = () => { + if (activeStep < STEPS.length) { + setCompletedSteps((prev) => new Set([...prev, activeStep])); + setActiveStep(activeStep + 1); + } + }; + + const goPrev = () => { + if (activeStep > 1) setActiveStep(activeStep - 1); + }; + + const allComplete = completedSteps.size >= STEPS.length; + + const fireConfetti = useCallback(() => { + if (hasConfettiFired.current) return; + hasConfettiFired.current = true; + const end = Date.now() + 1500; + const colors = ['#ffd208', '#e6b800', '#10b981', '#a78bfa', '#f59e0b']; + (function frame() { + confetti({ + particleCount: 4, + angle: 60, + spread: 55, + origin: { x: 0 }, + colors, + }); + confetti({ + particleCount: 4, + angle: 120, + spread: 55, + origin: { x: 1 }, + colors, + }); + if (Date.now() < end) requestAnimationFrame(frame); + })(); + }, []); + + const markAllComplete = () => { + setCompletedSteps(new Set(STEPS.map((s) => s.id))); + }; + + useEffect(() => { + if (allComplete) fireConfetti(); + }, [allComplete, fireConfetti]); + + return ( +
+ {/* ── Header ── */} +
+ + Interactive Guide + +

+ +

+

+ A step-by-step walkthrough of Zama's FHE-powered confidential + token ecosystem. From test tokens to encrypted balances — in 5 minutes. +

+
+ + {/* ── Progress bar ── */} +
+ {STEPS.map((step) => { + const isActive = step.id === activeStep; + const isComplete = completedSteps.has(step.id); + + return ( + + ); + })} +
+ + {/* ── Completion banner ── */} + {allComplete && ( + +
+ +
+

+ Tutorial Complete! +

+

+ You now understand the full confidential token lifecycle. Ready to + try it for real? +

+
+
+ + + + + + + + + +
+
+
+ )} + + {/* ── Active step content ── */} + +
+
+
+ {currentStep.icon} +
+
+
+ Step {currentStep.id} of {STEPS.length} +
+

+ {currentStep.title} +

+

{currentStep.subtitle}

+
+
+
+ + + + {/* ── Navigation ── */} +
+ + +
+ {activeStep < STEPS.length ? ( + + ) : !allComplete ? ( + + ) : null} +
+
+
+ + {/* ── Resources footer ── */} +
+

+ Further Resources +

+
+ + + + +
+
+
+ ); +} + +function ResourceLink({ + href, + title, + description, +}: { + href: string; + title: string; + description: string; +}) { + const isExternal = href.startsWith('http'); + const Wrapper = isExternal ? 'a' : Link; + const extraProps = isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {}; + + return ( + + +
+ +
+
+ {title} +
+
+ {description} +
+
+
+
+
+ ); +} diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx new file mode 100644 index 0000000..c5980ee --- /dev/null +++ b/src/app/app/page.tsx @@ -0,0 +1,1598 @@ +'use client'; + +import React, { useState, useMemo, useEffect, useRef } from 'react'; +import Link from 'next/link'; +import Card from '@/components/ui/Card'; +import Badge from '@/components/ui/Badge'; +import Button from '@/components/ui/Button'; +import CopyButton from '@/components/ui/CopyButton'; +import TokenIcon from '@/components/ui/TokenIcon'; +import Skeleton from '@/components/ui/Skeleton'; +import Tooltip from '@/components/ui/Tooltip'; +import { formatAddress, formatAmount } from '@/lib/utils'; +import { useActiveNetwork } from '@/app/ClientLayout'; +import { + useRegistryPairs, + loadCustomPairs, + saveCustomPairs, + type RegistryPairsResult, + type CustomPairRecord, +} from '@/lib/registry'; +import { type WrapperPair } from '@/config/contracts'; +import { ERC20_ABI, WRAPPER_ABI, isErc7984Contract } from '@/lib/wrapper-abi'; +import BlurIn from '@/components/ui/BlurIn'; +import { useAccount, useReadContract, usePublicClient } from 'wagmi'; +import { useConfidentialBalance, useConfidentialBalances } from '@zama-fhe/react-sdk'; +import { useWalletErc7984Scan } from '@/lib/use-wallet-scan'; +import { useSessionReset } from '@/lib/reset-session'; +import { classifyError } from '@/lib/errors'; +import { useToast } from '@/components/ui/Toast'; +import { isAddress, getAddress } from 'viem'; +import { + Search, + Lock, + Unlock, + Shield, + ExternalLink, + Info, + AlertTriangle, + AlertCircle, + Database, + Settings2, + Loader2, + RefreshCw, + Plus, + Trash2, + Download, + Upload, +} from 'lucide-react'; + +// ─── Tooltip content constants ──────────────────────────────────────────────── +// Centralised here so copy can be revised without hunting through JSX. + +const TIP = { + erc7984: 'ERC-7984 wrapper stores your balance as on-chain ciphertext via FHE — unreadable by anyone without your cryptographic permit.', + confidentialBalance: 'Encrypted balance. Click Decrypt to sign a read-only EIP-712 permit — no tokens are spent, your private key stays in your wallet.', +}; + +// ─── Per-row component ──────────────────────────────────────────────────────── + +function shortName(name: string, maxLen = 24): string { + if (name.length <= maxLen) return name; + return name.slice(0, maxLen).trimEnd() + '…'; +} + +function RegistryTokenRow({ + wrapper, + explorerBase, + isTestnet, + batchedValue, + batchedError, +}: { + wrapper: WrapperPair; + explorerBase: string; + isTestnet: boolean; + /** Value from the parent's batched useConfidentialBalances (one signature for all). */ + batchedValue?: bigint; + /** Per-token error from the batched decrypt, if any. */ + batchedError?: Error; +}) { + const { address, isConnected } = useAccount(); + const [decryptRequested, setDecryptRequested] = useState(false); + // When true, the per-row Decrypt was clicked — bypass any batched value from + // Decrypt-All so this click is authoritative. `??` preserves 0n on the left, + // so a stale batched 0n would otherwise short-circuit the per-row query and + // the button would feel unresponsive. + const [preferSingle, setPreferSingle] = useState(false); + const decryptErrorRef = useRef(null); + const { resetToken } = useSessionReset(); + + // Public ERC-20 balance + const { data: rawPublicBalance } = useReadContract({ + abi: ERC20_ABI, + address: wrapper.erc20Address, + functionName: 'balanceOf', + args: address ? [address] : undefined, + query: { enabled: isConnected && !!address }, + }); + const publicBalance = rawPublicBalance as bigint | undefined; + + // Per-row single-token decrypt — only fires after explicit user click. + // retry: false — a rejected permit signature must NOT re-prompt the wallet. + const { + data: singleBalance, + isLoading: isSingleDecrypting, + error: singleError, + refetch: refetchConfidential, + } = useConfidentialBalance( + { tokenAddress: wrapper.erc7984Address }, + { + enabled: decryptRequested && isConnected && !!address && (preferSingle || batchedValue === undefined), + retry: false, + refetchOnWindowFocus: false, + }, + ); + + // preferSingle overrides the batched value once the user clicks the per-row + // Decrypt button. Otherwise, batched value from useConfidentialBalances wins + // (one Decrypt-All signature covers everything). + const confidentialBalance = preferSingle ? singleBalance : (batchedValue ?? singleBalance); + const isDecrypting = isSingleDecrypting; + const decryptError = preferSingle ? singleError : (batchedError ?? singleError); + + const isRevoked = wrapper.isValid === false; + const cleanName = shortName(wrapper.name.replace(/\s*\(Mock\)\s*/gi, '').trim()); + // Every Sepolia registry pair is a Zama-deployed testnet mock — real mainnet + // assets don't exist on Sepolia. (isMintablePair's on-chain symbol() check is + // kept for the Faucet's actual mint-button gating, a separate concern; it's + // unreliable as a *label* since some mocks' symbol() doesn't end in "Mock".) + const isMock = isTestnet && !wrapper.name.toLowerCase().includes('restricted') && !wrapper.symbol.toLowerCase().includes('restricted'); + const confidentialSymbol = `c${wrapper.symbol}`; + + // App-wide session reset — re-arm the button so the next click prompts for + // a fresh EIP-712 signature (IndexedDB is empty after reset). + useEffect(() => { + if (resetToken > 0) { + setDecryptRequested(false); + setPreferSingle(false); + } + }, [resetToken]); + + // Fire-once: on decrypt error (incl. signature rejection), disable the query + // so it can't re-fire on remount/focus. The Decrypt button re-arms itself. + const { addToast } = useToast(); + useEffect(() => { + if (!singleError) { + decryptErrorRef.current = null; + return; + } + const msg = singleError.message ?? ''; + if (decryptErrorRef.current === msg) return; + decryptErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(singleError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [singleError, addToast]); + + const handleDecrypt = () => { + setPreferSingle(true); + setDecryptRequested(true); + void refetchConfidential(); + }; + + const rowOpacity = isRevoked ? { opacity: 0.55 } : undefined; + + return ( +
+ {/* ── Public token row ──────────────────────────────────────────────── */} +
+
+ +
+
+ {wrapper.symbol} + {isMock && Mock} + {isRevoked && ( + + Revoked + + )} + {wrapper.unverified && ( + + Unverified + + )} +
+
+ {cleanName} +
+
+
+
+ {wrapper.decimals} +
+ {!isConnected ? ( + + ) : publicBalance !== undefined ? ( + + {formatAmount(publicBalance, wrapper.decimals)}{' '} + {wrapper.symbol} + + ) : ( + + )} +
+
+ {isRevoked ? ( + + Unavailable + + ) : ( +
+ + + +
+ )} +
+
+ + {/* ── Confidential wrapper row ──────────────────────────────────────── */} +
+
+ +
+
+ {confidentialSymbol} + {isMock && Mock} +
+
+ Confidential {cleanName} +
+
+
+ + + {wrapper.wrapperDecimals} + FHE + + +
+ {!isConnected ? ( + + ) : confidentialBalance !== undefined && confidentialBalance !== null ? ( + confidentialBalance === 0n ? ( + + + No confidential balance yet + + + + ) : ( + + {formatAmount(confidentialBalance, wrapper.wrapperDecimals)}{' '} + {confidentialSymbol} + + + ) + ) : isDecrypting ? ( + Awaiting signature… + ) : decryptError ? ( + + ) : ( + + )} +
+
+ {isRevoked ? ( + + Unavailable + + ) : ( +
+ + + +
+ )} +
+
+
+ ); +} + +// ─── Detected Token Row (For Wallet Scan Auto-Detection) ────────────────────── + +interface CustomTokenEntry { + address: `0x${string}`; + symbol: string; + name: string; + decimals: number; + isAutoDetected: boolean; +} + +function DetectedTokenRow({ + token, + explorerBase, + onRemove, + batchedValue, + batchedError, +}: { + token: CustomTokenEntry; + explorerBase: string; + onRemove?: () => void; + batchedValue?: bigint; + batchedError?: Error; +}) { + const { address, isConnected } = useAccount(); + const [decryptRequested, setDecryptRequested] = useState(false); + // Force the per-row query to win over a stale batched 0n — same rationale as + // in RegistryTokenRow. Clicking Decrypt is authoritative. + const [preferSingle, setPreferSingle] = useState(false); + const { resetToken } = useSessionReset(); + + // 1. Read the underlying ERC-20 via the canonical `underlying()` getter + // (OpenZeppelin IERC7984ERC20Wrapper — verified on-chain: real registry + // wrappers revert on the legacy `underlyingToken()` alias). + const { data: rawUnderlyingAddress } = useReadContract({ + abi: WRAPPER_ABI, + address: token.address, + functionName: 'underlying', + query: { enabled: isConnected && !!address }, + }); + const underlyingAddress = rawUnderlyingAddress as `0x${string}` | undefined; + const isWrapper = !!underlyingAddress && underlyingAddress !== '0x0000000000000000000000000000000000000000'; + + // 2. Query public balance of the underlying token (if it is a wrapper) + const { data: rawPublicBalance } = useReadContract({ + abi: ERC20_ABI, + address: isWrapper ? underlyingAddress : undefined, + functionName: 'balanceOf', + args: address ? [address] : undefined, + query: { enabled: isConnected && !!address && isWrapper }, + }); + const publicBalance = rawPublicBalance as bigint | undefined; + + // 3. Confidential balance — only decrypted on explicit user action. + // Prefer the parent's batched value (one signature covers everything). + // retry: false — a rejected permit signature must NOT re-prompt the wallet. + const { + data: singleBalance, + isLoading: isSingleDecrypting, + error: singleError, + } = useConfidentialBalance( + { tokenAddress: token.address }, + { + enabled: decryptRequested && isConnected && !!address && (preferSingle || batchedValue === undefined), + retry: false, + refetchOnWindowFocus: false, + }, + ); + const confidentialBalance = preferSingle ? singleBalance : (batchedValue ?? singleBalance); + const isDecrypting = isSingleDecrypting; + const decryptError = preferSingle ? singleError : (batchedError ?? singleError); + + const cleanName = shortName(token.name.replace(/\s*\(Mock\)\s*/gi, '').trim()); + const confidentialSymbol = isWrapper ? (token.symbol.startsWith('c') ? token.symbol : `c${token.symbol}`) : token.symbol; + + // App-wide reset — re-arm the button. + useEffect(() => { + if (resetToken > 0) { + setDecryptRequested(false); + setPreferSingle(false); + } + }, [resetToken]); + + // Fire-once: on decrypt error, disable the query so it can't re-fire. + const { addToast } = useToast(); + const decryptErrorRef = useRef(null); + useEffect(() => { + if (!singleError) { + decryptErrorRef.current = null; + return; + } + const msg = singleError.message ?? ''; + if (decryptErrorRef.current === msg) return; + decryptErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(singleError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [singleError, addToast]); + + const handleDecrypt = () => { + setPreferSingle(true); + setDecryptRequested(true); + // State transition alone enables the query; calling refetch() before the + // re-render means it fires against the still-disabled query and is a no-op. + }; + + return ( +
+ {/* ── Public token row (only shown if token wraps an underlying ERC-20) ── */} + {isWrapper && ( +
+ {isWrapper ? ( +
+ +
+
+ {token.symbol} + + {token.isAutoDetected ? 'Detected' : 'Custom'} + +
+
{cleanName}
+
+
+ ) : ( +
+ +
+
+ {token.symbol} + + Decrypt-only + +
+ Native FHE asset — no ERC-20 +
+
+ )} + + {isWrapper ? {token.decimals} : } +
+ {!isConnected ? ( + + ) : !isWrapper ? ( + + ) : publicBalance !== undefined ? ( + + {formatAmount(publicBalance, token.decimals)}{' '} + {token.symbol} + + ) : ( + + )} +
+
+ {isWrapper ? ( +
+ + + +
+ ) : ( + Direct Transfer Only + )} +
+
+ )} + + {/* ── Confidential wrapper row ──────────────────────────────────────── */} +
+
+ +
+
+ {confidentialSymbol} + {!isWrapper && ( + + {token.isAutoDetected ? 'Detected' : 'Custom'} + + )} +
+
{isWrapper ? `Confidential ${cleanName}` : `${cleanName} (Standalone ERC-7984)`}
+
+
+ + + 6 + FHE + + +
+ {!isConnected ? ( + + ) : confidentialBalance !== undefined && confidentialBalance !== null ? ( + confidentialBalance === 0n ? ( + + + No confidential balance yet + + + + ) : ( + + {/* wrapperDecimals (euint64 = 6) not token.decimals — the encrypted + balance is in wrapper units, and the underlying scale would + show 0 for high-decimal underlyings. */} + {formatAmount(confidentialBalance, 6)}{' '} + {confidentialSymbol} + + + ) + ) : isDecrypting ? ( + Awaiting signature… + ) : decryptError ? ( + + ) : ( + + )} +
+
+
+ {isWrapper && ( + + + + )} + {onRemove && ( + + )} +
+
+
+
+ ); +} + + +// ─── Page ───────────────────────────────────────────────────────────────────── + +export default function HomePage() { + const [searchQuery, setSearchQuery] = useState(''); + const [showRevoked, setShowRevoked] = useState(false); + const customTokenSectionRef = useRef(null); + const { isTestnet, activeChainId } = useActiveNetwork(); + const { addToast } = useToast(); + const { resetToken } = useSessionReset(); + + const { pairs, isLoading, isFromCache, officialTotal, customTotal }: RegistryPairsResult = + useRegistryPairs(activeChainId); + + const { address, isConnected } = useAccount(); + // Pin the client to the app's active network — without the explicit chainId, + // wagmi follows the wallet's chain, so validation reads could silently hit + // the wrong network (e.g. wallet on Mainnet while the UI shows Sepolia). + const client = usePublicClient({ chainId: activeChainId }); + + const registryAddresses = useMemo(() => { + return new Set(pairs.map((p) => p.erc7984Address.toLowerCase())); + }, [pairs]); + + const { + extra: detectedExtras, + status: scanStatus, + rescan, + } = useWalletErc7984Scan(address, client, registryAddresses); + + // === Persistent Local Custom Pairs (chain-scoped, versioned) === + const [customPairs, setCustomPairs] = useState([]); + + // Client-side load (SSR-safe — window is only touched in the effect). + useEffect(() => { + setCustomPairs(loadCustomPairs(activeChainId)); + }, [activeChainId]); + + // Adapt CustomPairRecord[] → CustomTokenEntry[] for the existing row UI. + const localCustomTokens = useMemo( + () => customPairs.map((p) => ({ + address: p.erc7984Address, + symbol: p.symbol, + name: p.name, + decimals: p.wrapperDecimals, + isAutoDetected: false, + })), + [customPairs], + ); + + // === Add Custom Pair — one input, on-chain validation === + const [inputAddress, setInputAddress] = useState(''); + const [addressError, setAddressError] = useState(''); + const [addressInfo, setAddressInfo] = useState(null); + const [previewPair, setPreviewPair] = useState(null); + const [isValidating, setIsValidating] = useState(false); + const inputRef = useRef(null); + const validationTokenRef = useRef(0); + + // Duplicate-check data is read through a ref so the validation effect below + // does NOT depend on `pairs` / `customPairs` / `detectedExtras` identities. + // With them in the dependency array, any unrelated re-render (balance + // refetch, scan status, decrypt state) re-ran the effect, cancelled the + // debounce timer, discarded the in-flight validation (token bump) and wiped + // the preview — the visible symptom was a stuck "Checking wrapper on-chain…" + // and a pair that could never be added. + const dedupDataRef = useRef({ pairs, customPairs, detectedExtras }); + useEffect(() => { + dedupDataRef.current = { pairs, customPairs, detectedExtras }; + }, [pairs, customPairs, detectedExtras]); + + // Debounced on-chain validation. Runs against a wallet-independent viem + // PublicClient — works even before a wallet is connected. + useEffect(() => { + setAddressError(''); + setAddressInfo(null); + setPreviewPair(null); + const paste = inputAddress.trim(); + if (!paste) return; + if (!isAddress(paste)) { + // Distinguish a checksum failure (right shape, wrong EIP-55 casing — + // viem's strict isAddress rejects it) from a malformed string, so the + // user gets an actionable message instead of a generic one. + setAddressError( + /^0x[0-9a-fA-F]{40}$/.test(paste) + ? 'Address checksum is invalid (EIP-55 mixed-case mismatch) — re-copy it from the explorer or paste it in all-lowercase.' + : 'Not a valid Ethereum address (0x-prefixed, 42 chars).', + ); + return; + } + const wrapperAddr = getAddress(paste) as `0x${string}`; + if (wrapperAddr === '0x0000000000000000000000000000000000000000') { + setAddressError('Cannot add the zero address.'); + return; + } + if (!client) { + setAddressError('No RPC client available for this network — try reloading.'); + return; + } + + const token = ++validationTokenRef.current; + const timer = setTimeout(async () => { + setIsValidating(true); + try { + // 1) Contract check. + const code = await client.getCode({ address: wrapperAddr }); + if (token !== validationTokenRef.current) return; + if (!code || code === '0x') { + setAddressError('Not a contract on this network.'); + return; + } + + // 2) ERC-7984 check — robust: ERC-165 fast path, then a behavioral + // `confidentialBalanceOf` probe so tokens that don't implement ERC-165 + // (but ARE real ERC-7984s) are still accepted. See isErc7984Contract. + const isErc7984 = await isErc7984Contract(client, wrapperAddr); + if (token !== validationTokenRef.current) return; + if (!isErc7984) { + setAddressError('Not an ERC-7984 confidential token (no ERC-165 support and no confidentialBalanceOf).'); + return; + } + + // 3) underlying() — canonical name, with the legacy underlyingToken() + // alias as a fallback. A token with NO underlying is a valid + // confidential-only ERC-7984 (not a wrapper): we still add it, as a + // decrypt-only token with no shield/unshield side. + let underlyingAddr: `0x${string}` | null = null; + try { + underlyingAddr = (await client.readContract({ + address: wrapperAddr, + abi: WRAPPER_ABI, + functionName: 'underlying', + })) as `0x${string}`; + } catch { + try { + underlyingAddr = (await client.readContract({ + address: wrapperAddr, + abi: WRAPPER_ABI, + functionName: 'underlyingToken', + })) as `0x${string}`; + } catch { underlyingAddr = null; } + } + if (token !== validationTokenRef.current) return; + + const isWrapper = + !!underlyingAddr && + underlyingAddr !== '0x0000000000000000000000000000000000000000' && + underlyingAddr.toLowerCase() !== wrapperAddr.toLowerCase(); + + // 4) Wrapper metadata is always read; underlying metadata only when it's + // an actual wrapper. + const [wSymRaw, wNameRaw, wDecRaw] = await Promise.all([ + client.readContract({ address: wrapperAddr, abi: WRAPPER_ABI, functionName: 'symbol' }).catch(() => null), + client.readContract({ address: wrapperAddr, abi: WRAPPER_ABI, functionName: 'name' }).catch(() => null), + client.readContract({ address: wrapperAddr, abi: WRAPPER_ABI, functionName: 'decimals' }).catch(() => null), + ]); + if (token !== validationTokenRef.current) return; + if (wSymRaw == null || wNameRaw == null || wDecRaw == null) { + setAddressError('Failed to read token metadata from the contract.'); + return; + } + const wrapperSymbol = String(wSymRaw); + const wrapperName = String(wNameRaw); + const wrapperDecimals = Number(wDecRaw); + + let underlyingSymbol = ''; + let underlyingName = ''; + let underlyingDecimals = wrapperDecimals; + if (isWrapper && underlyingAddr) { + const [uSymRaw, uNameRaw, uDecRaw] = await Promise.all([ + client.readContract({ address: underlyingAddr, abi: ERC20_ABI, functionName: 'symbol' }).catch(() => null), + client.readContract({ address: underlyingAddr, abi: ERC20_ABI, functionName: 'name' }).catch(() => null), + client.readContract({ address: underlyingAddr, abi: ERC20_ABI, functionName: 'decimals' }).catch(() => null), + ]); + if (token !== validationTokenRef.current) return; + if (uSymRaw == null || uNameRaw == null || uDecRaw == null) { + setAddressError('Failed to read underlying ERC-20 metadata.'); + return; + } + underlyingSymbol = String(uSymRaw); + underlyingName = String(uNameRaw); + underlyingDecimals = Number(uDecRaw); + } + + // 5) Duplicate check (latest data via ref — see above). + const { pairs: allPairs, customPairs: existingCustom } = dedupDataRef.current; + const wLower = wrapperAddr.toLowerCase(); + const uLower = underlyingAddr?.toLowerCase(); + const registryHitByWrapper = allPairs.find( + (p) => p.source !== 'custom' && p.erc7984Address.toLowerCase() === wLower, + ); + const registryHitByUnderlying = isWrapper + ? allPairs.find((p) => p.source !== 'custom' && p.erc20Address.toLowerCase() === uLower) + : undefined; + if (registryHitByWrapper) { + if (registryHitByWrapper.isValid === false) { + setAddressError('This wrapper is revoked in the on-chain registry.'); + return; + } + setAddressInfo(`This pair is already Official (${registryHitByWrapper.symbol}) — no need to add it.`); + setPreviewPair(null); + return; + } + if (registryHitByUnderlying) { + setAddressError(`The underlying ${underlyingSymbol} is already in the Official registry (paired with ${registryHitByUnderlying.symbol}).`); + return; + } + if (existingCustom.some((p) => { + if (p.erc7984Address.toLowerCase() === wLower) return true; + // erc20 collision check only when both sides have a non-zero underlying — + // avoids false collisions between distinct confidential-only tokens + // (both stored with erc20 = zero address). + if (!isWrapper || !uLower) return false; + const pu = p.erc20Address.toLowerCase(); + if (pu === '0x0000000000000000000000000000000000000000') return false; + return pu === uLower; + })) { + setAddressError('This token has already been added.'); + return; + } + // Scanner-detected tokens are NOT a blocker for adding — the user is + // promoting an auto-detected token to a first-class custom pair, which + // saves it to localStorage and gets it out of the "detected" bucket. + // (Duplicate row prevention lives in allCustomTokens dedup.) + + // All checks passed — build the preview. + setPreviewPair({ + erc7984Address: wrapperAddr, + erc20Address: isWrapper && underlyingAddr + ? (getAddress(underlyingAddr) as `0x${string}`) + : '0x0000000000000000000000000000000000000000', + symbol: wrapperSymbol, + name: wrapperName, + decimals: underlyingDecimals, + wrapperDecimals, + underlyingSymbol, + underlyingName, + addedAt: Date.now(), + source: 'custom', + isWrapper, + }); + } catch (err) { + if (token !== validationTokenRef.current) return; + const classified = classifyError(err); + setAddressError(classified.message || 'Validation failed.'); + } finally { + if (token === validationTokenRef.current) setIsValidating(false); + } + }, 500); + + return () => clearTimeout(timer); + // Dedup data intentionally read via dedupDataRef (kept in sync above) so + // identity churn on pairs/scan results can't cancel an in-flight check. + }, [inputAddress, client]); + + const handleAddCustomToken = () => { + if (!previewPair) return; + // Backstop dedup at add time — the validation snapshot may be stale if the + // registry/scan updated while the preview was showing. + // + // Comparing erc20Address only makes sense when it's non-zero. For a + // confidential-only ERC-7984 the underlying is the zero address, and + // comparing zero-vs-zero against ANY prior confidential-only entry would + // false-collide — that was the "This pair already exists" bug on a second + // confidential-only add. + const wLower = previewPair.erc7984Address.toLowerCase(); + const uLower = previewPair.erc20Address.toLowerCase(); + const hasUnderlying = uLower !== '0x0000000000000000000000000000000000000000'; + const collides = (p: { erc7984Address: string; erc20Address: string }) => { + if (p.erc7984Address.toLowerCase() === wLower) return true; + if (!hasUnderlying) return false; + const pu = p.erc20Address.toLowerCase(); + if (pu === '0x0000000000000000000000000000000000000000') return false; + return pu === uLower; + }; + if ( + customPairs.some(collides) || + pairs.some((p) => p.source !== 'custom' && collides(p)) + ) { + setPreviewPair(null); + setAddressError('This pair already exists in the registry or your custom list.'); + return; + } + const next = [...customPairs, previewPair]; + setCustomPairs(next); + saveCustomPairs(activeChainId, next); + setInputAddress(''); + setPreviewPair(null); + setAddressError(''); + setAddressInfo(null); + inputRef.current?.focus(); + addToast({ + variant: 'success', + title: previewPair.isWrapper === false ? 'Confidential Token Added' : 'Custom Pair Added', + message: previewPair.isWrapper === false + ? `${previewPair.symbol} is now available to decrypt in the Custom / Dev-only section.` + : `${previewPair.symbol} ↔ ${previewPair.underlyingSymbol} is now available for shield/unshield/decrypt.`, + }); + }; + + const handleRemoveCustomToken = (tokenAddress: string) => { + const next = customPairs.filter( + (p) => p.erc7984Address.toLowerCase() !== tokenAddress.toLowerCase(), + ); + setCustomPairs(next); + saveCustomPairs(activeChainId, next); + }; + + // ── Export / Import custom pairs (JSON) — survives a browser-cache wipe ── + const importFileRef = useRef(null); + + const handleExportCustomPairs = () => { + const payload = JSON.stringify( + { app: 'shadowline', kind: 'custom-pairs', version: 1, chainId: activeChainId, pairs: customPairs }, + null, + 2, + ); + const blob = new Blob([payload], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `shadowline-custom-pairs-${activeChainId}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleImportCustomPairs = (file: File) => { + const reader = new FileReader(); + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result)) as { + chainId?: number; + pairs?: CustomPairRecord[]; + }; + const incoming = Array.isArray(parsed.pairs) ? parsed.pairs : []; + // Schema check per record — drop anything malformed instead of crashing. + const valid = incoming.filter( + (p) => + p && + isAddress(p.erc7984Address ?? '') && + isAddress(p.erc20Address ?? '') && + typeof p.symbol === 'string' && + typeof p.decimals === 'number' && + typeof p.wrapperDecimals === 'number', + ); + if (parsed.chainId !== undefined && parsed.chainId !== activeChainId) { + addToast({ + variant: 'warning', + title: 'Chain Mismatch', + message: `This file was exported for chain ${parsed.chainId}, but the active network is ${activeChainId}. Import skipped.`, + }); + return; + } + const known = new Set([ + ...customPairs.map((p) => p.erc7984Address.toLowerCase()), + ...pairs.map((p) => p.erc7984Address.toLowerCase()), + ]); + const fresh = valid.filter((p) => !known.has(p.erc7984Address.toLowerCase())); + if (fresh.length === 0) { + addToast({ + variant: 'info', + title: 'Nothing to Import', + message: valid.length > 0 ? 'All pairs in the file already exist.' : 'No valid pair records found in the file.', + }); + return; + } + const next = [...customPairs, ...fresh]; + setCustomPairs(next); + saveCustomPairs(activeChainId, next); + addToast({ + variant: 'success', + title: 'Pairs Imported', + message: `${fresh.length} custom pair${fresh.length === 1 ? '' : 's'} restored from file.`, + }); + } catch { + addToast({ variant: 'error', title: 'Import Failed', message: 'The file is not valid ShadowLine custom-pairs JSON.' }); + } + }; + reader.readAsText(file); + }; + + // Combine auto-detected extras and manual custom tokens, deduplicating by address + const allCustomTokens = useMemo(() => { + const merged = localCustomTokens.map((t) => ({ ...t, isAutoDetected: false })); + const existing = new Set(merged.map((t) => t.address.toLowerCase())); + + for (const token of detectedExtras) { + if (!existing.has(token.address.toLowerCase())) { + merged.push({ + address: token.address, + symbol: token.symbol, + name: token.name, + decimals: token.decimals, + isAutoDetected: true, + }); + } + } + return merged; + }, [localCustomTokens, detectedExtras]); + + // The main table is the "Official — Zama Registry" section: custom pairs are + // excluded here and rendered exclusively in the "Custom / Dev-only" section + // below (they still flow to wrap/transfer/portfolio via useRegistryPairs). + const visibleWrappers = useMemo(() => { + const official = pairs.filter(p => p.source !== 'custom'); + return showRevoked ? official : official.filter(p => p.isValid !== false); + }, [pairs, showRevoked]); + const revokedCount = useMemo(() => pairs.filter(p => p.isValid === false).length, [pairs]); + + const filteredWrappers = useMemo(() => { + if (!searchQuery) return visibleWrappers; + const q = searchQuery.toLowerCase(); + return visibleWrappers.filter( + w => + w.name.toLowerCase().includes(q) || + w.symbol.toLowerCase().includes(q) || + w.erc20Address.toLowerCase().includes(q) || + w.erc7984Address.toLowerCase().includes(q), + ); + }, [visibleWrappers, searchQuery]); + + const explorerBase = isTestnet ? 'https://eth-sepolia.blockscout.com' : 'https://eth.blockscout.com'; + + // ── Batched Decrypt All (ONE EIP-712 signature covers every listed token) ── + // useConfidentialBalances (plural) calls credentials.allow(...addresses) once. + // Populate `batchAddresses` on click; empty array = disabled (no popup). + // Result gets distributed to both RegistryTokenRow and DetectedTokenRow via + // the `batchedValue` / `batchedError` props — rows fall back to their own + // single-token decrypt for the per-row "Decrypt" button. + const [batchAddresses, setBatchAddresses] = useState<`0x${string}`[]>([]); + const batchErrorRef = useRef(null); + + const { + data: batchResult, + isFetching: isBatchDecrypting, + error: batchError, + } = useConfidentialBalances( + { tokenAddresses: batchAddresses }, + { enabled: batchAddresses.length > 0, retry: false, refetchOnWindowFocus: false }, + ); + + // Reset on app-wide session reset — disarm the batch query. + useEffect(() => { + if (resetToken > 0) { + setBatchAddresses([]); + batchErrorRef.current = null; + } + }, [resetToken]); + + // Fire-once on batch failure (incl. rejected signature): disarm + one toast. + useEffect(() => { + if (!batchError) { + batchErrorRef.current = null; + return; + } + const msg = batchError.message ?? ''; + if (batchErrorRef.current === msg) return; + batchErrorRef.current = msg; + setBatchAddresses([]); + const classified = classifyError(batchError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [batchError, addToast]); + + // Fast address→bigint lookup for the row prop. + const batchValueByAddress = useMemo(() => { + const map = new Map(); + if (batchResult?.results instanceof Map) { + for (const [addr, val] of batchResult.results.entries()) { + if (typeof val === 'bigint') map.set(addr.toLowerCase(), val); + } + } + return map; + }, [batchResult]); + + const batchErrorByAddress = useMemo(() => { + const map = new Map(); + if (batchResult?.errors instanceof Map) { + for (const [addr, err] of batchResult.errors.entries()) { + if (err instanceof Error) map.set(addr.toLowerCase(), err); + } + } + return map; + }, [batchResult]); + + const handleDecryptAll = () => { + if (!isConnected) return; + const addresses: `0x${string}`[] = []; + for (const w of filteredWrappers) { + if (w.isValid !== false) addresses.push(w.erc7984Address); + } + for (const t of allCustomTokens) { + addresses.push(t.address); + } + if (addresses.length === 0) return; + setBatchAddresses(addresses); + }; + + return ( +
+ {/* Header */} +
+

+

+ +

+
+ + {/* Cached-snapshot banner */} + {isFromCache && ( + +
+ + + + + Showing a cached snapshot. Connect a wallet on{' '} + {isTestnet ? 'Sepolia' : 'Ethereum Mainnet'} to read the live on-chain + WrappersRegistry — the live list may include newer pairs. + +
+
+ )} + + {/* Stats Bar */} +
+ {/* Pairs */} +
+ +
+
+ {isLoading ? : officialTotal} +
+
+ Wrapper Pairs + {revokedCount > 0 && · {revokedCount} revoked} +
+
+
+ + {/* Network */} +
+
+
+
+ {isTestnet ? 'Sepolia Testnet' : 'Ethereum Mainnet'} +
+
Active Network
+
+
+ + {/* Standard */} +
+ +
+
+ ERC-7984 + +
+
Confidential Standard
+
+
+ + {/* Encryption layer */} +
+ +
+
fhEVM · Zama
+
Encryption Layer
+
+
+
+ + {/* Search + Actions */} +
+
+ + + + setSearchQuery(e.target.value)} + style={{ paddingLeft: 40 }} + aria-label="Search registered wrapper pairs" + /> +
+
+ {revokedCount > 0 && ( + + )} + {isConnected && filteredWrappers.length > 0 && ( + + )} + {isConnected && ( + + )} +
+
+ + {/* ── Section: Official — Zama Registry ─────────────────────────────── */} +
+

+ + Official Registry +

+

+ Verified on-chain by the Confidential Token Wrappers Registry. + {isTestnet && ' Most Sepolia pairs are Zama mock tokens with a public mint — grab free test tokens from the Faucet page.'} +

+
+ + {/* Pair list */} +
+
+ Token + Address + Decimals + + Balance + + + Actions +
+ + {isLoading && filteredWrappers.length === 0 ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ) : filteredWrappers.length === 0 ? ( +
+
+ +
+
+ {searchQuery ? 'No tokens match your search query' : 'No registered wrappers found on this network'} +
+
+ ) : ( +
+ {filteredWrappers.map(wrapper => ( + + ))} +
+ )} +
+ + {/* Auto-Detected & Custom Tokens Section */} + {isConnected && ( +
+
+
+

+ + Custom / Dev-only Tokens +

+

+ Added locally in this browser · not in the official registry. Includes ERC-7984 tokens auto-detected from your wallet history. +

+
+
+ {customPairs.length > 0 && ( + + )} + + { + const file = e.target.files?.[0]; + if (file) handleImportCustomPairs(file); + e.target.value = ''; + }} + /> + {scanStatus !== 'scanning' && ( + + )} +
+
+ + {/* Add-Custom-Pair form — one input, on-chain validated */} + +
+
+ +
+ + + + setInputAddress(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && previewPair && handleAddCustomToken()} + spellCheck={false} + autoComplete="off" + /> +
+ {isValidating && ( +
+ Checking wrapper on-chain… +
+ )} + {!isValidating && addressError && ( +
+ {addressError} +
+ )} + {!isValidating && addressInfo && ( +
+ {addressInfo} +
+ )} +
+
+
Add
+ +
+
+ + {previewPair && ( +
+
+ +
+
+ {previewPair.isWrapper === false ? 'Confidential' : 'Wrapper'} {previewPair.symbol} +
+
{formatAddress(previewPair.erc7984Address, 6)} · {previewPair.wrapperDecimals} dec
+
+
+ {previewPair.isWrapper === false ? ( + + Decrypt-only · no ERC-20 wrapper + + ) : ( + <> +
+
+ +
+
Underlying {previewPair.underlyingSymbol}
+
{formatAddress(previewPair.erc20Address, 6)} · {previewPair.decimals} dec
+
+
+ + )} +
+ )} + +
+ + Custom pairs are stored locally in this browser (chain-scoped) and go through the same shield / unshield / decrypt paths as Official pairs — just without the on-chain registry endorsement. +
+
+ + {/* Token List */} + {scanStatus === 'scanning' ? ( + +
+ + Scanning wallet transfer logs & Blockscout API for custom tokens... +
+
+ ) : allCustomTokens.length === 0 ? ( +
+
+ +
+

+ No additional custom tokens detected in wallet history. Use the form above to manually register a custom token address. +

+
+ ) : ( +
+
+ Token + Address + Decimals + Balance + Actions +
+
+ {allCustomTokens.map((token) => ( + handleRemoveCustomToken(token.address)} + batchedValue={batchValueByAddress.get(token.address.toLowerCase())} + batchedError={batchErrorByAddress.get(token.address.toLowerCase())} + /> + ))} +
+
+ )} +
+ )} + + {/* Info Banner */} + +
+
+ +
+
+
+ Underlying Mechanism + +
+
+ Confidential wrappers convert standard public tokens into ERC-7984 tokens + utilizing Fully Homomorphic Encryption (FHE) on the fhEVM. On-chain values + (like account balances and transfer amounts) are encrypted into cryptographic + handles — protecting transaction details from public ledger scraping. +
+
+
+
+
+ ); +} diff --git a/src/app/app/portfolio/page.tsx b/src/app/app/portfolio/page.tsx new file mode 100644 index 0000000..f60d4b4 --- /dev/null +++ b/src/app/app/portfolio/page.tsx @@ -0,0 +1,583 @@ +'use client'; + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import Card from '@/components/ui/Card'; +import Button from '@/components/ui/Button'; +import Badge from '@/components/ui/Badge'; +import Modal from '@/components/ui/Modal'; +import TokenIcon from '@/components/ui/TokenIcon'; +import WalletActivityFeed from '@/components/WalletActivityFeed'; +import { type WrapperPair } from '@/config/contracts'; +import { formatAmount, formatAddress } from '@/lib/utils'; +import { classifyError } from '@/lib/errors'; +import PendingUnshieldBanner from '@/components/PendingUnshieldBanner'; +import { useActiveNetwork } from '@/app/ClientLayout'; +import { useRegistryPairs } from '@/lib/registry'; +import { useAccount, useConnect } from 'wagmi'; +import { useConfidentialBalances, useConfidentialBalance } from '@zama-fhe/react-sdk'; +import { useQueryClient } from '@tanstack/react-query'; +import { useToast } from '@/components/ui/Toast'; +import { useSessionReset } from '@/lib/reset-session'; +import BlurIn from '@/components/ui/BlurIn'; +import { + Lock, + Unlock, + Info, + Shield, + Wallet, + RefreshCw, + AlertTriangle, + Settings2, +} from 'lucide-react'; + +// ── Official wrapper card ──────────────────────────────────────────────────── + +interface TokenPositionProps { + wrapper: WrapperPair; + isConnected: boolean; + isDecrypted: boolean; + isDecrypting: boolean; + decryptedBalance: bigint | undefined; + decryptError: Error | null; + onDecrypt: () => void; + /** When true, shows Unshield + Decrypt Again; when false only Decrypt. */ + isConfidentialOnly?: boolean; +} + +function TokenPositionCard({ + wrapper, + isConnected, + isDecrypted, + isDecrypting, + decryptedBalance, + decryptError, + onDecrypt, + isConfidentialOnly = false, +}: TokenPositionProps) { + return ( + +
+ +
+
{wrapper.name}
+
+ c{wrapper.symbol} · {formatAddress(wrapper.erc7984Address)} +
+
+
+ ERC-7984 + {isConfidentialOnly && ( + + Decrypt only + + )} +
+
+ + {/* Balance Display */} +
+
+ Confidential Balance +
+ {isDecrypting ? ( +
+
+ Awaiting permit… +
+ ) : decryptError ? ( +
+ {classifyError(decryptError).message} +
+ ) : isDecrypted ? ( +
+ + {formatAmount(decryptedBalance ?? 0n, wrapper.wrapperDecimals)} + + c{wrapper.symbol} +
+ ) : ( +
+
+ + •••••• + + + Encrypted + +
+ Click to decrypt +
+ )} +
+ + {/* Actions */} +
+ {!isDecrypted ? ( + + ) : ( + <> + {!isConfidentialOnly && ( + + )} + + + )} +
+ + ); +} + +// ── Confidential-only per-row decrypt (singular hook, one per card) ────────── + +function ConfidentialOnlyCard({ + wrapper, + isConnected, + resetToken, +}: { + wrapper: WrapperPair; + isConnected: boolean; + resetToken: number; +}) { + const [decryptRequested, setDecryptRequested] = useState(false); + + const { + data: balance, + isLoading, + error, + } = useConfidentialBalance( + { tokenAddress: wrapper.erc7984Address as `0x${string}` }, + { + enabled: decryptRequested && isConnected, + retry: false, + refetchOnWindowFocus: false, + }, + ); + + // Reset on app-wide session reset + useEffect(() => { + if (resetToken > 0) setDecryptRequested(false); + }, [resetToken]); + + const { addToast } = useToast(); + const lastErrorRef = useRef(null); + useEffect(() => { + if (!error) { + lastErrorRef.current = null; + return; + } + const msg = error.message ?? ''; + if (lastErrorRef.current === msg) return; + lastErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(error); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [error, addToast]); + + return ( + setDecryptRequested(true)} + isConfidentialOnly + /> + ); +} + +// ── Main page ──────────────────────────────────────────────────────────────── + +export default function PortfolioPage() { + const { activeChainId } = useActiveNetwork(); + const { address, isConnected, chain: walletChain } = useAccount(); + const { connect, connectors } = useConnect(); + const { addToast } = useToast(); + const queryClient = useQueryClient(); + const { reset: resetSession, isResetting: isRevoking, resetToken } = useSessionReset(); + + const { pairs, localRecords } = useRegistryPairs(activeChainId); + + // Official pairs = registry + config-file (never localStorage custom) + const officialWrappers = useMemo(() => pairs.filter((p) => p.source !== 'custom'), [pairs]); + // Custom wrapper pairs (isWrapper:true — has ERC-20 underlying, can unshield) + const customWrapperPairs = useMemo(() => pairs.filter((p) => p.source === 'custom' && p.isWrapper !== false), [pairs]); + // Confidential-only custom pairs (isWrapper:false — no underlying, decrypt only) + const customConfOnly = useMemo(() => pairs.filter((p) => p.source === 'custom' && p.isWrapper === false), [pairs]); + + // All pairs passed to the activity feed (official + custom wrappers) + const allWrappers = useMemo(() => pairs, [pairs]); + + const [requestedAddresses, setRequestedAddresses] = useState<`0x${string}`[]>([]); + const [resolvedBalances, setResolvedBalances] = useState>({}); + const [resolvedErrors, setResolvedErrors] = useState>({}); + const [isConnectModalOpen, setIsConnectModalOpen] = useState(false); + const [fheWorkerFailed, setFheWorkerFailed] = useState(false); + + const resolvedBalancesRef = useRef(resolvedBalances); + useEffect(() => { resolvedBalancesRef.current = resolvedBalances; }, [resolvedBalances]); + + const lastHandledErrorMsgRef = useRef(null); + const autoRevokedForMsgRef = useRef(null); + const revokeSessionRef = useRef<(() => void) | null>(null); + + const supportedChain = + !isConnected || !walletChain || walletChain.id === 11155111 || walletChain.id === 1; + + const { + data: decryptedBalances, + isLoading: isDecryptingAll, + error: globalError, + } = useConfidentialBalances( + { tokenAddresses: requestedAddresses }, + { + enabled: isConnected && supportedChain && requestedAddresses.length > 0, + retry: false, + refetchOnWindowFocus: false, + }, + ); + + useEffect(() => { + if (!decryptedBalances) return; + setResolvedBalances((prev) => { + const next = { ...prev }; + if (decryptedBalances.results instanceof Map) { + for (const [key, val] of decryptedBalances.results.entries()) { + if (val != null) next[key.toLowerCase()] = val; + } + } else if (decryptedBalances.results) { + for (const [key, val] of Object.entries(decryptedBalances.results)) { + if (val != null) next[key.toLowerCase()] = val as bigint; + } + } + return next; + }); + setResolvedErrors((prev) => { + const next = { ...prev }; + if (decryptedBalances.results instanceof Map) { + for (const [key] of decryptedBalances.results.entries()) delete next[key.toLowerCase()]; + } + if (decryptedBalances.errors instanceof Map) { + for (const [key, val] of decryptedBalances.errors.entries()) { + if (val) next[key.toLowerCase()] = val; + } + } + return next; + }); + }, [decryptedBalances]); + + useEffect(() => { + if (!globalError) { lastHandledErrorMsgRef.current = null; return; } + const msg = globalError.message ?? ''; + if (msg === lastHandledErrorMsgRef.current) return; + lastHandledErrorMsgRef.current = msg; + const classified = classifyError(globalError); + const isStalePermit = + classified.title === 'Decryption Failed' || + classified.title === 'Session Expired' || + classified.title === 'Session Key Rejected' || + classified.title === 'Balance Check Unavailable'; + + if (classified.title === 'Configuration Error' || classified.title === 'Relayer Unavailable') { + setFheWorkerFailed(true); + addToast({ variant: 'error', title: classified.title, message: classified.message }); + } else if (isStalePermit && autoRevokedForMsgRef.current !== msg) { + autoRevokedForMsgRef.current = msg; + try { revokeSessionRef.current?.(); } catch { /* best-effort */ } + addToast({ variant: 'info', title: 'Session Permit Refreshed', message: 'Cached permit was stale — cleared. Click Decrypt again.' }); + } else { + addToast({ variant: 'error', title: classified.title, message: classified.message }); + } + queryClient.removeQueries({ queryKey: ['zama.confidentialBalances'] }); + queryClient.removeQueries({ queryKey: ['zama.confidentialBalance'] }); + setRequestedAddresses((prev) => + prev.filter((addr) => resolvedBalancesRef.current[addr.toLowerCase()] !== undefined), + ); + }, [globalError, addToast, queryClient]); + + const handleDecryptToken = (tokenAddress: `0x${string}`, symbol: string) => { + const lower = tokenAddress.toLowerCase(); + setResolvedErrors((prev) => { const n = { ...prev }; delete n[lower]; return n; }); + setFheWorkerFailed(false); + lastHandledErrorMsgRef.current = null; + queryClient.resetQueries({ queryKey: ['zama.confidentialBalances'] }); + if (!requestedAddresses.some((a) => a.toLowerCase() === lower)) { + setRequestedAddresses((prev) => [...prev, tokenAddress]); + addToast({ variant: 'info', title: `Decrypting ${symbol}`, message: 'Sign the EIP-712 permit in your wallet.' }); + } + }; + + const handleDecryptAll = () => { + if (!address) return; + setResolvedErrors({}); + setFheWorkerFailed(false); + lastHandledErrorMsgRef.current = null; + queryClient.resetQueries({ queryKey: ['zama.confidentialBalances'] }); + // Batch includes official + custom wrapper pairs (conf-only have their own per-card hook) + const allAddresses = [...officialWrappers, ...customWrapperPairs].map((w) => w.erc7984Address); + setRequestedAddresses(allAddresses); + addToast({ variant: 'info', title: 'Decrypting Portfolio', message: 'One batch EIP-712 permit for all assets.' }); + }; + + useEffect(() => { + revokeSessionRef.current = () => { void resetSession({ silent: true }); }; + }, [resetSession]); + + useEffect(() => { + if (resetToken === 0) return; + setRequestedAddresses([]); + setResolvedBalances({}); + setResolvedErrors({}); + setFheWorkerFailed(false); + autoRevokedForMsgRef.current = null; + lastHandledErrorMsgRef.current = null; + }, [resetToken]); + + const totalDecrypted = Object.keys(resolvedBalances).length; + const batchableCount = officialWrappers.length + customWrapperPairs.length; + + function rowProps(wrapper: WrapperPair) { + const lower = wrapper.erc7984Address.toLowerCase(); + return { + isDecrypted: resolvedBalances[lower] !== undefined, + decryptError: resolvedErrors[lower] || null, + isDecrypting: + requestedAddresses.some((a) => a.toLowerCase() === lower) && + resolvedBalances[lower] === undefined && + !resolvedErrors[lower], + decryptedBalance: resolvedBalances[lower], + onDecrypt: () => handleDecryptToken(wrapper.erc7984Address, wrapper.symbol), + }; + } + + return ( +
+
+
+
+

+

+ +

+
+
+ {isConnected && ( + + )} + {isConnected && batchableCount > 0 && totalDecrypted < batchableCount && ( + + )} +
+
+
+ + {/* Summary */} + {isConnected && totalDecrypted > 0 && ( + +
Decrypted Balances
+
+ + {totalDecrypted} + + / {batchableCount} assets decrypted +
+
+ )} + + {/* Not connected */} + {!isConnected ? ( +
+
+ +
+

Connect Wallet

+

+ Connect your Web3 wallet to view and decrypt confidential balances. +

+ +
+ ) : ( + <> + {/* Pending unshield banners */} + {officialWrappers.map((w) => ( + + ))} + + {/* Unsupported chain */} + {!supportedChain && ( +
+ +
+
Unsupported Chain
+
Switch your wallet to Sepolia or Mainnet.
+
+
+ )} + + {/* FHE worker error */} + {fheWorkerFailed && ( +
+ +
+
Zama Relayer Unavailable
+
FHE network temporarily unreachable. Wait and try again.
+
+
+ )} + + {/* ── Official — Zama Registry ────────────────────────────────── */} + {officialWrappers.length > 0 && ( +
+
+

+ Official Registry +

+

+ Verified on-chain ERC-20 ↔ ERC-7984 wrapper pairs. Supports shield, unshield, and decrypt. +

+
+
+ {officialWrappers.map((wrapper) => ( + + ))} +
+
+ )} + + {/* ── Custom wrapper pairs ────────────────────────────────────── */} + {customWrapperPairs.length > 0 && ( +
+
+ +
+

+ Custom Wrappers +

+

+ Locally-added wrapper pairs. Supports shield, unshield, and decrypt. +

+
+
+
+ {customWrapperPairs.map((wrapper) => ( + + ))} +
+
+ )} + + {/* ── Custom confidential-only tokens ────────────────────────── */} + {customConfOnly.length > 0 && ( +
+
+ +
+

+ Custom Decrypt-Only +

+

+ Confidential tokens with no ERC-20 underlying. Decrypt only — no shield/unshield. +

+
+
+
+ {customConfOnly.map((wrapper) => ( + + ))} +
+
+ )} + + {/* No tokens at all */} + {officialWrappers.length === 0 && customWrapperPairs.length === 0 && customConfOnly.length === 0 && ( +
+
+ +
+

No Tokens

+

No registered tokens on this chain. Try switching to Sepolia.

+
+ )} + + )} + + {/* Activity feed */} + {isConnected && address && supportedChain && allWrappers.length > 0 && ( + + )} + + {/* Info */} + +
+
+ +
+ + Decrypting balances requires an EIP-712 permit — a read-only off-chain signature that authorises the Zama Gateway to decrypt your balance for this session. + Your private key never leaves your device. Use Reset Session to clear cached permits and force fresh signatures. + +
+
+ + {/* Connect modal */} + {isConnectModalOpen && ( + setIsConnectModalOpen(false)}> +
+
Select a wallet:
+ {connectors.map((c) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/src/app/app/transfer/page.tsx b/src/app/app/transfer/page.tsx new file mode 100644 index 0000000..c78e51d --- /dev/null +++ b/src/app/app/transfer/page.tsx @@ -0,0 +1,894 @@ +'use client'; + +/** + * Transfer page — /app/transfer + * + * Two modes: + * - Confidential: ERC-7984 transfer with FHE-encrypted amount (Zama SDK). + * - Standard: plain ERC-20 transfer via wagmi writeContract. + * + * Confidential path: docs.zama.org/protocol/sdk/api-references/react/useconfidentialtransfer + * Installed 3.0.1 uses { tokenAddress } config shape (verified against .d.ts). + */ + +import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react'; +import { useAccount, useConnect, useReadContract, useWriteContract, useWaitForTransactionReceipt, usePublicClient } from 'wagmi'; +import { useConfidentialTransfer, useConfidentialBalance } from '@zama-fhe/react-sdk'; +import { isAddress, formatUnits } from 'viem'; + +import Card from '@/components/ui/Card'; +import Button from '@/components/ui/Button'; +import Badge from '@/components/ui/Badge'; +import TokenIcon from '@/components/ui/TokenIcon'; +import TokenSelect, { type TokenSelectGroup } from '@/components/ui/TokenSelect'; +import { useToast } from '@/components/ui/Toast'; +import TransactionSuccessModal from '@/components/ui/TransactionSuccessModal'; +import { useActiveNetwork } from '@/app/ClientLayout'; +import { useRegistryPairs } from '@/lib/registry'; +import { useWalletErc7984Scan } from '@/lib/use-wallet-scan'; +import { useSessionReset } from '@/lib/reset-session'; +import { parseAmount, formatAmount, formatAddress } from '@/lib/utils'; +import { classifyError } from '@/lib/errors'; +import { ERC20_ABI } from '@/lib/wrapper-abi'; +import { CHAIN_CONFIG, type SupportedChainId } from '@/config/chains'; +import { + Send, + ShieldCheck, + Lock, + ArrowRight, + Wallet, + ExternalLink, + Zap, + Shield, + Unlock, + Clock, + AlertTriangle, + CheckCircle2, + Loader2, + X, +} from 'lucide-react'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const; +const RECENT_KEY = 'shadowline-recent-recipients'; +const MAX_RECENT = 5; + +type TransferMode = 'confidential' | 'standard'; +type Step = 'idle' | 'encrypting' | 'submitting' | 'confirming' | 'done'; + +function loadRecents(): string[] { + try { + return JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[]; + } catch { + return []; + } +} +function safeParse(value: string, dec: number): bigint { + if (!value) return 0n; + try { return parseAmount(value, dec); } catch { return 0n; } +} +function saveRecent(addr: string) { + try { + const prev = loadRecents().filter((a) => a.toLowerCase() !== addr.toLowerCase()); + localStorage.setItem(RECENT_KEY, JSON.stringify([addr, ...prev].slice(0, MAX_RECENT))); + } catch { /* best-effort */ } +} + +function StepIndicator({ step }: { step: Step }) { + const steps: { id: Step; label: string }[] = [ + { id: 'encrypting', label: 'Encrypt' }, + { id: 'submitting', label: 'Submit' }, + { id: 'confirming', label: 'Confirm' }, + ]; + const activeIdx = steps.findIndex((s) => s.id === step); + + if (step === 'idle' || step === 'done') return null; + + return ( +
+ {steps.map((s, i) => { + const isDone = i < activeIdx; + const isActive = i === activeIdx; + return ( + +
+ {isDone ? ( + + ) : isActive ? ( + + ) : ( +
+ )} + {s.label} +
+ {i < steps.length - 1 && ( +
+ )} + + ); + })} +
+ ); +} + +export default function TransferPage() { + const { address, isConnected } = useAccount(); + const { connect, connectors } = useConnect(); + const { addToast } = useToast(); + const { activeChainId } = useActiveNetwork(); + const chainConfig = CHAIN_CONFIG[activeChainId]; + const publicClient = usePublicClient({ chainId: activeChainId as SupportedChainId }); + + const { pairs, isLoading: isRegistryLoading } = useRegistryPairs(activeChainId); + + const transferablePairs = useMemo( + () => pairs.filter((p) => p.isValid !== false), + [pairs], + ); + + // Auto-detected ERC-7984 tokens outside the registry + const registryAddresses = useMemo( + () => new Set(transferablePairs.map((p) => p.erc7984Address.toLowerCase())), + [transferablePairs], + ); + const { extra: extraTokens } = useWalletErc7984Scan(address, publicClient, registryAddresses); + + const [mode, setMode] = useState('confidential'); + const [selectedSymbol, setSelectedSymbol] = useState(''); + const selectedPair = useMemo( + () => transferablePairs.find((p) => p.symbol === selectedSymbol), + [transferablePairs, selectedSymbol], + ); + + // For auto-detected extra tokens not in registry + const selectedExtra = useMemo( + () => extraTokens.find((t) => t.address === selectedSymbol), + [extraTokens, selectedSymbol], + ); + + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + const [step, setStep] = useState('idle'); + const [finalTxHash, setFinalTxHash] = useState(undefined); + const [isSuccessOpen, setIsSuccessOpen] = useState(false); + + // Recent recipients + const [recents, setRecents] = useState([]); + useEffect(() => { setRecents(loadRecents()); }, []); + + // Confidential balance decrypt gate + const [decryptRequested, setDecryptRequested] = useState(false); + + // App-wide session reset — re-arm the Reveal button so the next click + // prompts for a fresh EIP-712 signature (IndexedDB is empty after reset). + const { resetToken } = useSessionReset(); + useEffect(() => { + if (resetToken > 0) setDecryptRequested(false); + }, [resetToken]); + + // Reset on token/mode change + const handleModeSwitch = (next: TransferMode) => { + setMode(next); + setSelectedSymbol(''); + setAmount(''); + setRecipient(''); + setStep('idle'); + setDecryptRequested(false); + }; + const handleTokenChange = (sym: string) => { + setSelectedSymbol(sym); + setAmount(''); + setDecryptRequested(false); + }; + + // Which address are we transferring from/to? + const erc7984Addr = selectedPair?.erc7984Address ?? selectedExtra?.address ?? ZERO_ADDRESS; + const erc20Addr = selectedPair?.erc20Address ?? ZERO_ADDRESS; + const wrapperDecimals = selectedPair?.wrapperDecimals ?? selectedExtra?.decimals ?? 6; + const underlyingDecimals = selectedPair?.decimals ?? 18; + + const decimals = mode === 'confidential' ? wrapperDecimals : underlyingDecimals; + const symbolDisplay = mode === 'confidential' + ? `c${selectedPair?.symbol ?? selectedExtra?.symbol ?? '…'}` + : (selectedPair?.symbol ?? '…'); + + // ── Confidential balance (decrypt-gated) ──────────────────────────────────── + // retry: false — a rejected permit signature must NOT re-prompt the wallet. + const { data: confBalRaw, isLoading: isDecrypting, error: confBalError, refetch: refetchConfBalance } = useConfidentialBalance( + { tokenAddress: erc7984Addr as `0x${string}` }, + { + enabled: decryptRequested && !!address && erc7984Addr !== ZERO_ADDRESS, + retry: false, + refetchOnWindowFocus: false, + }, + ); + + // Fire-once: on decrypt error (incl. signature rejection), disable the query + // and show one toast. The "Reveal balance" button re-arms itself. + const confBalErrorRef = useRef(null); + useEffect(() => { + if (!confBalError) { + confBalErrorRef.current = null; + return; + } + const msg = confBalError.message ?? ''; + if (confBalErrorRef.current === msg) return; + confBalErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(confBalError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [confBalError, addToast]); + const confBalance = confBalRaw != null + ? formatUnits(BigInt(confBalRaw), wrapperDecimals) + : null; + + // ── ERC-20 balance (standard mode, no gating needed) ─────────────────────── + const { data: erc20BalanceRaw } = useReadContract({ + address: erc20Addr as `0x${string}`, + abi: ERC20_ABI, + functionName: 'balanceOf', + args: [address!], + query: { enabled: !!address && erc20Addr !== ZERO_ADDRESS && mode === 'standard' }, + }); + const erc20Balance = erc20BalanceRaw != null + ? formatAmount(erc20BalanceRaw as bigint, underlyingDecimals) + : null; + + // ── Recipient validation ───────────────────────────────────────────────────── + const recipientTrimmed = recipient.trim(); + const recipientIsValidAddr = isAddress(recipientTrimmed); + const recipientIsZero = recipientTrimmed === ZERO_ADDRESS; + const recipientIsSelf = recipientTrimmed.toLowerCase() === address?.toLowerCase(); + const [isContract, setIsContract] = useState(null); + const contractCheckRef = useRef | null>(null); + + useEffect(() => { + setIsContract(null); + if (!recipientIsValidAddr || !publicClient) return; + if (contractCheckRef.current) clearTimeout(contractCheckRef.current); + contractCheckRef.current = setTimeout(() => { + publicClient.getCode({ address: recipientTrimmed as `0x${string}` }).then((code) => { + setIsContract(code != null && code !== '0x' && code.length > 2); + }).catch(() => setIsContract(null)); + }, 500); + return () => { if (contractCheckRef.current) clearTimeout(contractCheckRef.current); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recipientTrimmed, recipientIsValidAddr]); + + const recipientError = + recipientTrimmed && !recipientIsValidAddr ? 'Not a valid Ethereum address.' : + recipientIsZero ? 'Cannot send to the zero address.' : + recipientIsSelf ? 'Cannot send to yourself.' : + null; + const recipientWarning = !recipientError && isContract + ? 'This looks like a contract. Confidential tokens sent to a contract without ERC-7984 support may be permanently locked.' + : null; + + const parsedAmount = safeParse(amount, decimals); + + // Insufficient balance checks + const confBalBigint = confBalance != null ? parseAmount(confBalance, wrapperDecimals) : null; + const erc20BalBigint = erc20BalanceRaw as bigint | null ?? null; + const isConfInsufficient = mode === 'confidential' && confBalBigint != null && parsedAmount > 0n && parsedAmount > confBalBigint; + const isStdInsufficient = mode === 'standard' && erc20BalBigint != null && parsedAmount > 0n && parsedAmount > erc20BalBigint; + const isInsufficient = isConfInsufficient || isStdInsufficient; + + const hasToken = !!selectedPair || !!selectedExtra; + const isRecipientValid = recipientIsValidAddr && !recipientIsZero && !recipientIsSelf; + const isAmountValid = parsedAmount > 0n; + const canSubmit = + isConnected && + hasToken && + isRecipientValid && + isAmountValid && + !isInsufficient && + step === 'idle'; + + // Readable reason for disabled button + const disabledReason: string | null = !isConnected + ? 'Connect a wallet' + : !hasToken + ? 'Select a token' + : !isRecipientValid + ? 'Enter a valid recipient' + : !isAmountValid + ? 'Enter an amount' + : isInsufficient + ? 'Insufficient balance' + : step !== 'idle' + ? 'Transfer in progress…' + : null; + + // ── Confidential transfer ─────────────────────────────────────────────────── + const isPending = step !== 'idle' && step !== 'done'; + const { mutateAsync: transfer } = useConfidentialTransfer({ + tokenAddress: erc7984Addr as `0x${string}`, + }); + + // ── Standard ERC-20 transfer ──────────────────────────────────────────────── + const { writeContractAsync } = useWriteContract(); + const [pendingTxHash, setPendingTxHash] = useState(undefined); + useWaitForTransactionReceipt({ + hash: pendingTxHash as `0x${string}` | undefined, + query: { enabled: !!pendingTxHash }, + }); + + const explorerBase = chainConfig?.explorerUrl ?? 'https://etherscan.io'; + + const handleConfidentialTransfer = useCallback(async () => { + if (!canSubmit || !hasToken) return; + try { + setStep('encrypting'); + const res = await transfer({ + to: recipientTrimmed as `0x${string}`, + amount: parsedAmount, + onEncryptComplete: () => { + setStep('submitting'); + addToast({ variant: 'info', title: 'Amount Encrypted', message: 'Submitting to the network…' }); + }, + onTransferSubmitted: (hash) => { + setStep('confirming'); + setPendingTxHash(hash); + addToast({ variant: 'info', title: 'Transfer Submitted', message: 'Waiting for on-chain confirmation…' }); + }, + }); + saveRecent(recipientTrimmed); + setRecents(loadRecents()); + setFinalTxHash(res.txHash); + setIsSuccessOpen(true); + setStep('done'); + addToast({ + variant: 'success', + title: 'Confidential Transfer Confirmed', + message: `Sent ${amount} ${symbolDisplay} to ${formatAddress(recipientTrimmed)}.`, + }); + // Note: amount/recipient are cleared in modal onClose so the modal + // can display the correct sent amount (React batches these state updates). + } catch (err: unknown) { + console.error('Confidential transfer failed:', err); + const classified = classifyError(err); + addToast({ variant: classified.retryable ? 'warning' : 'error', title: classified.title, message: classified.message }); + setStep('idle'); + } + }, [canSubmit, hasToken, transfer, recipientTrimmed, parsedAmount, amount, symbolDisplay, addToast]); + + const handleStandardTransfer = useCallback(async () => { + if (!canSubmit || !selectedPair) return; + try { + setStep('submitting'); + addToast({ variant: 'info', title: 'Confirm in wallet', message: 'Approve the ERC-20 transfer in your wallet.' }); + const hash = await writeContractAsync({ + address: selectedPair.erc20Address as `0x${string}`, + abi: ERC20_ABI, + functionName: 'transfer', + args: [recipientTrimmed as `0x${string}`, parsedAmount], + }); + setStep('confirming'); + setPendingTxHash(hash); + addToast({ variant: 'info', title: 'Transfer Submitted', message: 'Waiting for confirmation…' }); + saveRecent(recipientTrimmed); + setRecents(loadRecents()); + setFinalTxHash(hash); + setIsSuccessOpen(true); + setStep('done'); + addToast({ + variant: 'success', + title: 'ERC-20 Transfer Confirmed', + message: `Sent ${amount} ${selectedPair.symbol} to ${formatAddress(recipientTrimmed)}.`, + }); + // Note: amount/recipient are cleared in modal onClose (see above). + } catch (err: unknown) { + console.error('ERC-20 transfer failed:', err); + const classified = classifyError(err); + addToast({ variant: classified.retryable ? 'warning' : 'error', title: classified.title, message: classified.message }); + setStep('idle'); + } + }, [canSubmit, selectedPair, writeContractAsync, recipientTrimmed, parsedAmount, amount, addToast]); + + const rawModalSym = selectedPair?.symbol ?? selectedExtra?.symbol ?? ''; + const tokenSymbolForModal = mode === 'confidential' + ? (selectedPair?.isWrapper === false || rawModalSym.startsWith('c') ? rawModalSym : `c${rawModalSym}`) + : rawModalSym; + + const isConfMode = mode === 'confidential'; + + const tokenGroups = useMemo(() => { + const official = transferablePairs.filter((p) => p.source !== 'custom' && (isConfMode || p.isWrapper !== false)); + const custom = transferablePairs.filter((p) => p.source === 'custom' && (isConfMode || p.isWrapper !== false)); + + const groups: TokenSelectGroup[] = []; + if (official.length > 0) { + groups.push({ + label: 'Official Registry', + options: official.map((p) => { + const sym = isConfMode ? (p.isWrapper === false || p.symbol.startsWith('c') ? p.symbol : `c${p.symbol}`) : p.symbol; + return { + value: p.symbol, + symbol: sym, + name: p.name, + iconSymbol: p.symbol, + address: p.erc7984Address, + }; + }), + }); + } + if (custom.length > 0) { + groups.push({ + label: 'Custom / Dev-only', + options: custom.map((p) => { + const sym = isConfMode ? (p.isWrapper === false || p.symbol.startsWith('c') ? p.symbol : `c${p.symbol}`) : p.symbol; + return { + value: p.symbol, + symbol: sym, + name: p.name, + iconSymbol: p.symbol, + badge: { text: 'Custom', variant: 'accent' }, + address: p.erc7984Address, + }; + }), + }); + } + if (extraTokens.length > 0 && isConfMode) { + groups.push({ + label: 'Detected (unverified)', + options: extraTokens.map((t) => ({ + value: t.address, + symbol: t.symbol, + name: t.name, + iconSymbol: t.symbol, + badge: { text: 'Unverified', variant: 'warning' }, + address: t.address, + })), + }); + } + return groups; + }, [transferablePairs, extraTokens, isConfMode]); + + const noTokensAvailable = + !isRegistryLoading && + transferablePairs.length === 0 && + extraTokens.length === 0 && + isConnected; + + return ( +
+ {/* Header */} +
+
+ +

+ Transfer +

+ + FHE + +
+

+ Send tokens to any recipient. Choose Confidential (ERC-7984, amount encrypted) + or Standard (plain ERC-20 transfer). +

+
+ + {/* Mode tabs */} +
+ + +
+ + {/* Mode description */} + {isConfMode ? ( +
+ + The amount is encrypted client-side via FHE before submission. + On-chain observers see who sent to whom, but never the value. + Gas cost is higher than a standard ERC-20 transfer. +
+ ) : ( +
+ + Standard ERC-20 transfer — amount and recipient are fully public on-chain. + Uses the underlying token, not the confidential wrapper. +
+ )} + + {!isConnected ? ( + +
+ +
+
Connect a wallet to continue
+
+ {isConfMode + ? 'Confidential transfers require a signer to encrypt the amount.' + : 'Connect your wallet to sign the ERC-20 transfer.'} +
+
+
+ {connectors.map((c) => ( + + ))} +
+
+
+ ) : noTokensAvailable ? ( + +
+ +
+
No confidential tokens detected
+
+ Your wallet doesn't hold any ERC-7984 tokens yet. + Shield some ERC-20 tokens first, or get test tokens from the Faucet. +
+
+ +
+
+ ) : ( + + {/* Step indicator (confidential only) */} + {isConfMode && } + + {/* Token select */} + + + {/* Recipient */} +
+
Recipient address
+ {/* Recent recipient chips */} + {recents.length > 0 && ( +
+ + Recent: + + {recents.map((r) => ( + + ))} +
+ )} + setRecipient(e.target.value.trim())} + disabled={isPending} + spellCheck={false} + style={{ + width: '100%', + padding: 'var(--sp-2) var(--sp-3)', + borderRadius: 'var(--radius-md)', + border: `1px solid ${recipientError ? 'var(--error)' : 'var(--border)'}`, + background: 'var(--bg-elevated)', + color: 'var(--text-primary)', + fontFamily: 'var(--font-mono, monospace)', + fontSize: 'var(--text-sm)', + }} + /> + {recipientError && ( +
+ {recipientError} +
+ )} + {recipientWarning && ( +
+ + {recipientWarning} +
+ )} +
+ + {/* Amount */} +
+
+ Amount + {hasToken && ( + + {isConfMode ? ( + decryptRequested && confBalance !== null ? ( + <> + Available:  + + {confBalance} {symbolDisplay} + + + + ) : ( + + ) + ) : erc20Balance !== null ? ( + <> + Available:  + + {erc20Balance} {selectedPair?.symbol ?? ''} + + + + ) : null} + + )} +
+ setAmount(e.target.value.replace(/[^0-9.]/g, ''))} + disabled={!hasToken || isPending} + style={{ + width: '100%', + padding: 'var(--sp-3) var(--sp-4)', + borderRadius: 'var(--radius-md)', + border: `1px solid ${isInsufficient ? 'var(--error)' : 'var(--border)'}`, + background: 'var(--bg-elevated)', + color: 'var(--text-primary)', + fontSize: 'var(--text-xl)', + fontFamily: 'var(--font-mono, monospace)', + }} + /> + {isInsufficient && ( +
+ Amount exceeds your available balance. +
+ )} +
+ + {/* CTA */} + + + {/* Disabled reason */} + {!canSubmit && !isPending && disabledReason && ( +
+ {disabledReason} +
+ )} + + {pendingTxHash && ( + + )} +
+ )} + + {finalTxHash && (selectedPair || selectedExtra) && ( + { + setIsSuccessOpen(false); + setFinalTxHash(undefined); + setPendingTxHash(undefined); + setStep('idle'); + setAmount(''); + setRecipient(''); + }} + action="transfer" + amount={amount || '0'} + tokenSymbol={tokenSymbolForModal} + txHash={finalTxHash} + /> + )} +
+ ); +} diff --git a/src/app/app/wrapper/page.tsx b/src/app/app/wrapper/page.tsx new file mode 100644 index 0000000..9b7fc3f --- /dev/null +++ b/src/app/app/wrapper/page.tsx @@ -0,0 +1,1067 @@ +'use client'; + +import React, { useState, useMemo, useEffect, useRef, Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; +import Card from '@/components/ui/Card'; +import Button from '@/components/ui/Button'; +import Badge from '@/components/ui/Badge'; +import Modal from '@/components/ui/Modal'; +import TokenIcon from '@/components/ui/TokenIcon'; +import TokenSelect, { type TokenSelectGroup } from '@/components/ui/TokenSelect'; +import WalletActivityFeed from '@/components/WalletActivityFeed'; +import { formatAddress, formatAmount, parseAmount } from '@/lib/utils'; +import { classifyError } from '@/lib/errors'; +import PendingUnshieldBanner from '@/components/PendingUnshieldBanner'; +import { useActiveNetwork } from '@/app/ClientLayout'; +import { useRegistryPairs, findPairBySymbol } from '@/lib/registry'; +import { useToast } from '@/components/ui/Toast'; +import { useSessionReset } from '@/lib/reset-session'; +import { + useAccount, + useReadContract, + useConnect, + useSwitchChain, + usePublicClient, + useWriteContract, +} from 'wagmi'; +import { + useConfidentialBalance, + useShield, + useUnshield, + useZamaSDK, + savePendingUnshield, + clearPendingUnshield, +} from '@zama-fhe/react-sdk'; +import { ERC20_ABI, WRAPPER_ABI } from '@/lib/wrapper-abi'; +import { isAddress } from 'viem'; +import BlurIn from '@/components/ui/BlurIn'; +import TypingAnimation from '@/components/ui/TypingAnimation'; +import { CHAIN_CONFIG } from '@/config/chains'; +import TransactionSuccessModal from '@/components/ui/TransactionSuccessModal'; +import { + ArrowUpDown, + Lock, + Unlock, + Check, + Info, + ExternalLink, + AlertCircle, +} from 'lucide-react'; + +/** + * Renders the confidential balance inline in the "Balance:" label. + * — Not yet requested → shows a real "Decrypt" button (never auto-fires) + * — Awaiting permit → shows a spinner text + * — Error → shows an error hint + retry button + * — Decrypted → shows the formatted balance with lock icon + */ +function ConfidentialBalanceInline({ + isConnected, + decryptedBalance, + isDecrypting, + error, + wrapperDecimals, + onDecrypt, +}: { + isConnected: boolean; + decryptedBalance: bigint | undefined | null; + isDecrypting: boolean; + error: Error | null | undefined; + wrapperDecimals: number; + onDecrypt: () => void; +}) { + if (!isConnected) return 0.00; + + if (decryptedBalance !== undefined && decryptedBalance !== null) { + return ( + + {formatAmount(decryptedBalance, wrapperDecimals)} + + + + + ); + } + + if (isDecrypting) { + return Awaiting signature...; + } + + if (error) { + return ( + + ); + } + + // Default: explicit decrypt button — never auto-fires. Neutral gray palette + // so it doesn't compete with the primary Shield/Unshield CTA (which owns the + // accent color). + return ( + + ); +} + +function WrapPageContent() { + const searchParams = useSearchParams(); + const initialToken = searchParams.get('token') || ''; + const initialAction = (searchParams.get('action') as 'wrap' | 'unwrap') || 'wrap'; + + const [action, setAction] = useState<'wrap' | 'unwrap'>(initialAction); + const [selectedToken, setSelectedToken] = useState(initialToken); + const [amount, setAmount] = useState(''); + const [txStep, setTxStep] = useState(0); // 0: idle, 1: approve pending, 2: approve mining, 3: action pending, 4: action mining, 5: completed + const [activeTxHash, setActiveTxHash] = useState<`0x${string}` | undefined>(undefined); + const [finalTxHash, setFinalTxHash] = useState(undefined); + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); + // Bumped after each completed wrap/unshield so the activity feed auto-refreshes. + const [feedRefreshKey, setFeedRefreshKey] = useState(0); + + const { activeChainId } = useActiveNetwork(); + const { addToast } = useToast(); + + // Wallet Connection Hooks + const { address, isConnected, chainId } = useAccount(); + const { connect, connectors } = useConnect(); + const { switchChain } = useSwitchChain(); + const publicClient = usePublicClient({ chainId: activeChainId }); + const { writeContractAsync } = useWriteContract(); + const [isConnectModalOpen, setIsConnectModalOpen] = useState(false); + // Gate confidential-balance decryption behind an explicit user action. + // The EIP-712 permit must NEVER fire automatically — it must only trigger + // when the user clicks "Decrypt to view". This flag is reset when the + // selected token changes so the user is always in control. + const [decryptRequested, setDecryptRequested] = useState(false); + + // App-wide session reset — re-arm the Decrypt button so the next click + // prompts for a fresh EIP-712 signature (IndexedDB is empty after reset). + const { resetToken } = useSessionReset(); + useEffect(() => { + if (resetToken > 0) setDecryptRequested(false); + }, [resetToken]); + + // Dynamic registry: list of wrapper pairs for the active chain, including + // any pair added on-chain after this client was built. + const { pairs: allPairs } = useRegistryPairs(activeChainId); + // Only let users wrap/unwrap pairs that the registry still considers + // valid; revoked pairs are kept in `allPairs` so the registry table can + // surface them, but they have no business in the swap selector. + const wrappers = useMemo( + () => allPairs.filter((p) => p.isValid !== false && p.isWrapper !== false), + [allPairs], + ); + + const wrapperGroups = useMemo(() => { + const official = wrappers.filter((w) => w.source !== 'custom'); + const custom = wrappers.filter((w) => w.source === 'custom'); + const groups: TokenSelectGroup[] = []; + if (official.length > 0) { + groups.push({ + label: 'Official Registry', + options: official.map((w) => ({ + value: w.erc7984Address.toLowerCase(), + symbol: action === 'wrap' ? w.symbol : `c${w.symbol}`, + name: w.name, + iconSymbol: w.symbol, + })), + }); + } + if (custom.length > 0) { + groups.push({ + label: 'Custom / Dev-only', + options: custom.map((w) => ({ + value: w.erc7984Address.toLowerCase(), + symbol: action === 'wrap' ? w.symbol : `c${w.symbol}`, + name: w.name, + iconSymbol: w.symbol, + badge: { text: 'Custom', variant: 'accent' }, + })), + }); + } + return groups; + }, [wrappers, action]); + + const isTokenAddress = useMemo(() => isAddress(selectedToken), [selectedToken]); + + // ── Address-first pair resolution ───────────────────────────────────────── + // Registry rows link with `?token=`; custom rows link with + // `?token=`. Both live in `allPairs` (registry + user custom + // via mergeCustomPairs) so we look up by both — no on-chain refetch needed + // when the pair is already known to the app. + const addressMatchedPair = useMemo(() => { + if (!isTokenAddress) return undefined; + const key = (selectedToken as string).toLowerCase(); + return allPairs.find( + (p) => + p.erc7984Address.toLowerCase() === key || + p.erc20Address.toLowerCase() === key, + ); + }, [isTokenAddress, selectedToken, allPairs]); + + // Only fall back to on-chain reads for a token address the app doesn't know + // about. This eliminates the "raw address shown as symbol" flash. + const needsOnChainLookup = isTokenAddress && !addressMatchedPair; + + const { data: customSymbol } = useReadContract({ + abi: ERC20_ABI, + address: needsOnChainLookup ? (selectedToken as `0x${string}`) : undefined, + functionName: 'symbol', + query: { enabled: needsOnChainLookup }, + }); + + const { data: customName } = useReadContract({ + abi: ERC20_ABI, + address: needsOnChainLookup ? (selectedToken as `0x${string}`) : undefined, + functionName: 'name', + query: { enabled: needsOnChainLookup }, + }); + + const { data: customDecimals } = useReadContract({ + abi: ERC20_ABI, + address: needsOnChainLookup ? (selectedToken as `0x${string}`) : undefined, + functionName: 'decimals', + query: { enabled: needsOnChainLookup }, + }); + + // Canonical OpenZeppelin ERC7984ERC20Wrapper getter is `underlying()`. + // The legacy `underlyingToken()` alias reverts on real Sepolia wrappers + // (verified on-chain against the live registry pair). + const { data: customUnderlying } = useReadContract({ + abi: WRAPPER_ABI, + address: needsOnChainLookup ? (selectedToken as `0x${string}`) : undefined, + functionName: 'underlying', + query: { enabled: needsOnChainLookup }, + }); + + const selectedWrapper = useMemo(() => { + // 1) Symbol match (registry pairs deep-linked by symbol from the registry table). + const bySymbol = findPairBySymbol(wrappers, selectedToken); + if (bySymbol) return bySymbol; + + // 2) Address match against merged registry + custom (also filters revoked + // isValid === false; the wrap selector never surfaces those). + if (addressMatchedPair && addressMatchedPair.isValid !== false) return addressMatchedPair; + + // 3) Unknown address — synthesize a pair from on-chain reads. Only build the + // wrapper object once symbol AND name have resolved; before that, return + // undefined so the UI shows a skeleton instead of raw text. + if (needsOnChainLookup && customSymbol && customName) { + return { + erc20Address: (customUnderlying as `0x${string}`) || ('0x0000000000000000000000000000000000000000' as `0x${string}`), + erc7984Address: selectedToken as `0x${string}`, + symbol: String(customSymbol).replace(/Mock$/i, ''), + name: String(customName), + decimals: typeof customDecimals === 'number' ? customDecimals : (typeof customDecimals === 'bigint' ? Number(customDecimals) : 18), + wrapperDecimals: 6, + isValid: true, + source: 'custom' as const, + }; + } + return undefined; + }, [wrappers, selectedToken, addressMatchedPair, needsOnChainLookup, customSymbol, customName, customDecimals, customUnderlying]); + + // Real contract balance reads (Public underlying) + const { data: rawPublicBalance, refetch: refetchPublicBalance, error: publicBalanceError } = useReadContract({ + abi: ERC20_ABI, + address: selectedWrapper?.erc20Address, + functionName: 'balanceOf', + args: address ? [address] : undefined, + query: { + enabled: !!address && !!selectedWrapper?.erc20Address, + }, + }); + + // Log public balance read error + useEffect(() => { + if (publicBalanceError) { + console.error('Error reading public balance:', publicBalanceError); + } + }, [publicBalanceError]); + + // Real contract balance reads (Confidential FHE) + // IMPORTANT: `enabled` depends on `decryptRequested` — the permit signature + // prompt must ONLY appear after the user explicitly clicks "Decrypt to view". + // Never auto-fire a wallet signature on token selection or page load. + const { + data: decryptedWrapperBalance, + refetch: refetchWrapperBalance, + isLoading: isDecryptingWrapper, + error: decryptWrapperError, + } = useConfidentialBalance( + { tokenAddress: selectedWrapper?.erc7984Address ?? '0x0000000000000000000000000000000000000000' }, + { + // retry: false — a rejected permit signature must NOT re-prompt the wallet. + enabled: decryptRequested && !!address && !!selectedWrapper?.erc7984Address, + retry: false, + refetchOnWindowFocus: false, + } + ); + + // Fire-once: on decrypt error (incl. signature rejection), disable the query + // so it can't re-fire on remount/focus. The Decrypt button re-arms itself. + const decryptErrorRef = useRef(null); + useEffect(() => { + if (!decryptWrapperError) { + decryptErrorRef.current = null; + return; + } + const msg = decryptWrapperError.message ?? ''; + if (decryptErrorRef.current === msg) return; + decryptErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(decryptWrapperError); + addToast({ + variant: 'warning', + title: classified.title, + message: classified.message, + }); + }, [decryptWrapperError, addToast]); + + // Read allowance + const { data: rawAllowance, refetch: refetchAllowance } = useReadContract({ + abi: ERC20_ABI, + address: selectedWrapper?.erc20Address, + functionName: 'allowance', + args: address && selectedWrapper ? [address, selectedWrapper.erc7984Address] : undefined, + query: { + enabled: !!address && !!selectedWrapper?.erc20Address && !!selectedWrapper?.erc7984Address, + }, + }); + + // Keep PUBLIC balances in sync when address or wrapper changes. + // Do NOT refetch the confidential balance here — that requires a permit + // signature and must only happen when the user explicitly clicks + // "Decrypt to view". + useEffect(() => { + if (address && selectedWrapper) { + refetchPublicBalance(); + refetchAllowance(); + } + }, [address, selectedWrapper, refetchPublicBalance, refetchAllowance]); + + + // Zama official Shield/Unshield hooks + const sdk = useZamaSDK(); + + const { mutateAsync: shield } = useShield({ + tokenAddress: selectedWrapper?.erc7984Address ?? '0x0000000000000000000000000000000000000000', + }); + + const { mutateAsync: unshield } = useUnshield({ + tokenAddress: selectedWrapper?.erc7984Address ?? '0x0000000000000000000000000000000000000000', + }); + + const underlyingDecimals = selectedWrapper?.decimals ?? 18; + const wrapperDecimals = selectedWrapper?.wrapperDecimals ?? 6; + const inputDecimals = action === 'wrap' ? underlyingDecimals : wrapperDecimals; + + const parsedInputAmount = (() => { + if (!amount) return 0n; + try { + return parseAmount(amount, inputDecimals); + } catch { + return 0n; + } + })(); + + const hasPublicBalance = rawPublicBalance !== undefined ? (rawPublicBalance as bigint) : 0n; + const hasWrapperBalance = decryptedWrapperBalance !== undefined && decryptedWrapperBalance !== null ? decryptedWrapperBalance : 0n; + const hasAllowance = rawAllowance !== undefined ? (rawAllowance as bigint) : 0n; + + const needsApproval = action === 'wrap' && hasAllowance < parsedInputAmount; + + const handleAction = async () => { + if (!selectedWrapper || !address) return; + try { + if (action === 'wrap') { + // ── Approve first, and WAIT for it to be mined, before the wrap ────── + // The installed SDK 3.0.1 `shield()` broadcasts the ERC-20 approval and + // immediately submits the wrap in the same call WITHOUT awaiting the + // approval receipt (verified in node_modules/@zama-fhe/sdk — the private + // approval helper calls signer.writeContract but never + // waitForTransactionReceipt). The wallet then simulates the wrap against + // an allowance that isn't mined yet and rejects it ("Third-party + // contract execution error"). So we run the approval ourselves, await + // the receipt, then call shield with approvalStrategy:'skip'. + if (needsApproval) { + if (!publicClient) throw new Error('No RPC client available for the active network.'); + setTxStep(1); // Approve pending (awaiting signature) + + // Some ERC-20s (notably real USDT, and this app's USDTMock which + // deliberately replicates it — verified on-chain: USDTMock.approve + // has `require(!(value != 0 && allowance(...) != 0))`) revert an + // approve() call that changes a non-zero allowance directly to a + // different non-zero value. The wallet's pre-flight simulation + // catches this and blocks confirmation ("Third-party contract + // execution error") before the user can even sign. Zero the + // allowance first when one is already outstanding, then approve + // the real amount — this is required by USDT-style tokens and a + // harmless no-op extra tx for standard ERC-20s. + if (hasAllowance > 0n) { + const zeroHash = await writeContractAsync({ + abi: ERC20_ABI, + address: selectedWrapper.erc20Address, + functionName: 'approve', + args: [selectedWrapper.erc7984Address, 0n], + }); + addToast({ + variant: 'info', + title: 'Resetting Allowance', + message: 'This token requires clearing the existing approval before setting a new one.', + }); + await publicClient.waitForTransactionReceipt({ hash: zeroHash }); + await refetchAllowance(); + } + + const approveHash = await writeContractAsync({ + abi: ERC20_ABI, + address: selectedWrapper.erc20Address, + functionName: 'approve', + args: [selectedWrapper.erc7984Address, parsedInputAmount], + }); + setTxStep(2); // Approve mining + setActiveTxHash(approveHash); + addToast({ + variant: 'info', + title: 'Approval Submitted', + message: 'Approve transaction sent. Waiting for on-chain confirmation before shielding…', + }); + await publicClient.waitForTransactionReceipt({ hash: approveHash }); + await refetchAllowance(); + } + + // Allowance is now confirmed on-chain — the wrap simulation will pass. + setTxStep(3); // Shield pending (awaiting signature) + const res = await shield({ + amount: parsedInputAmount, + approvalStrategy: 'skip', + onShieldSubmitted: (txHash) => { + setTxStep(4); // Shield mining + setActiveTxHash(txHash); + addToast({ + variant: 'info', + title: 'Shielding Submitted', + message: 'Shield transaction sent. Waiting for confirmation...', + }); + }, + }); + + setFinalTxHash(res.txHash); + + addToast({ + variant: 'success', + title: 'Shielding Confirmed', + message: `Successfully wrapped ${amount} ${selectedWrapper?.symbol ?? ''} into confidential c${selectedWrapper?.symbol ?? ''}.`, + }); + setTxStep(5); // Completed + setIsSuccessModalOpen(true); + // Reset decrypt gate — the confidential balance has changed after + // shielding, so any cached value is stale. The user must click + // "Decrypt" again. Also prevents TanStack Query's refetchOnWindowFocus + // from auto-firing a new permit while decryptRequested is still true. + setDecryptRequested(false); + refetchPublicBalance(); + refetchAllowance(); + setFeedRefreshKey((k) => k + 1); // auto-refresh the activity feed + } else { + setTxStep(3); // Unshield pending + const wrapperAddress = selectedWrapper.erc7984Address; + const res = await unshield({ + amount: parsedInputAmount, + onUnwrapSubmitted: (txHash) => { + setTxStep(4); // Unwrap on-chain, waiting for proof + setActiveTxHash(txHash); + // Persist the unwrap tx hash immediately. Per the official Zama + // unshield guide (docs.zama.org/protocol/sdk/guides/unshield-tokens), + // the SDK does NOT auto-persist this. If the user closes the tab + // between here and finalize, `loadPendingUnshield` on next mount + // will pick it up and `PendingUnshieldBanner` can offer Resume. + if (sdk?.storage) { + void savePendingUnshield(sdk.storage, wrapperAddress, txHash).catch((err) => { + console.error('savePendingUnshield failed:', err); + }); + } + addToast({ + variant: 'info', + title: 'Unwrap Submitted', + message: 'On-chain unwrap request sent. Waiting for Gateway proof...', + }); + }, + onFinalizing: () => { + // Gateway is generating the decryption proof + addToast({ + variant: 'info', + title: 'Finalizing', + message: 'Zama Gateway is generating the decryption proof. This may take 15–40 seconds.', + }); + }, + onFinalizeSubmitted: (txHash) => { + setActiveTxHash(txHash); + addToast({ + variant: 'info', + title: 'Finalize Submitted', + message: 'Finalization transaction sent. Almost done...', + }); + }, + }); + + setFinalTxHash(res.txHash); + + // Finalization confirmed on-chain — the pending record is no longer + // needed. Best-effort clear; failure here is non-fatal because the + // banner also self-clears on successful Resume. + if (sdk?.storage) { + void clearPendingUnshield(sdk.storage, wrapperAddress).catch(() => { + /* non-fatal */ + }); + } + + addToast({ + variant: 'success', + title: 'Unshielding Confirmed', + message: `Successfully unshielded ${amount} c${selectedWrapper?.symbol ?? ''} into public ${selectedWrapper?.symbol ?? ''}.`, + }); + setTxStep(5); // Completed + setIsSuccessModalOpen(true); + // Reset decrypt gate — same reason as wrap path above. + setDecryptRequested(false); + refetchPublicBalance(); + setFeedRefreshKey((k) => k + 1); // auto-refresh the activity feed + } + } catch (err: unknown) { + console.error(err); + setTxStep(0); + setActiveTxHash(undefined); + const classified = classifyError(err); + addToast({ + variant: 'error', + title: classified.title, + message: classified.message, + }); + } + }; + + const handleToggleAction = () => { + setAction(prev => (prev === 'wrap' ? 'unwrap' : 'wrap')); + setAmount(''); + setTxStep(0); + setActiveTxHash(undefined); + setFinalTxHash(undefined); + }; + + const isChainMismatch = isConnected && chainId !== activeChainId; + + return ( +
+
+

+ {' '} + +

+

+ {action === 'wrap' + ? 'Convert public ERC-20 tokens to encrypted ERC-7984 confidential tokens.' + : 'Convert encrypted ERC-7984 tokens back to public ERC-20 tokens.'} +

+
+ + {/* Pending unshield banners — one per wrapper; each self-hides if nothing is pending */} + {isConnected && ( +
+ {wrappers.map((w) => ( + + ))} +
+ )} + + {/* Swap Card */} +
+ 0 && txStep < 5 ? 'swap-card-pending' : ''}`} + > + {/* From Panel */} +
+
+ + {action === 'wrap' ? 'From (Public)' : 'From (Confidential)'} + + + Balance:{' '} + {action === 'wrap' ? ( + isConnected ? formatAmount(hasPublicBalance, underlyingDecimals) : '0.00' + ) : ( + setDecryptRequested(true)} + /> + )} + +
+
+ 0} + onChange={e => { + const v = e.target.value; + if (/^[0-9]*\.?[0-9]*$/.test(v)) setAmount(v); + }} + style={{ flex: 1, background: 'transparent', border: 'none', padding: 0 }} + /> + + {/* Token Display / Selector */} +
+ { + setSelectedToken(val); + setTxStep(0); + setAmount(''); + setDecryptRequested(false); + }} + groups={wrapperGroups} + placeholder="Select Token" + disabled={txStep > 0} + size="sm" + style={{ minWidth: '160px' }} + /> +
+
+ + {/* Percentage Selector Pills */} + {isConnected && selectedToken && ( +
+ {[10, 25, 50, 75, 100].map((percent) => { + const balanceBig = action === 'wrap' ? hasPublicBalance : hasWrapperBalance; + return ( + + ); + })} +
+ )} + + {selectedWrapper && ( +
+ {action === 'wrap' + ? formatAddress(selectedWrapper.erc20Address) + : formatAddress(selectedWrapper.erc7984Address)} +
+ )} +
+ + {/* Swap Direction Arrow */} +
+ +
+ + {/* To Panel */} +
+
+ + {action === 'wrap' ? 'To (Confidential)' : 'To (Public)'} + + + Balance:{' '} + {action === 'unwrap' ? ( + isConnected ? formatAmount(hasPublicBalance, underlyingDecimals) : '0.00' + ) : ( + setDecryptRequested(true)} + /> + )} + +
+
+
+ {amount || '0.0'} +
+ {selectedToken && ( +
+ {/* Display the resolved symbol from selectedWrapper — never + the raw ?token= address (that's how "c0xfF89…" leaked into + the UI when navigated from a custom row). */} + + + {action === 'wrap' && ( + + + + )} + {selectedWrapper + ? (action === 'wrap' ? `c${selectedWrapper.symbol}` : selectedWrapper.symbol) + : 'Loading…'} + +
+ )} +
+ {selectedWrapper && ( +
+ {action === 'wrap' + ? formatAddress(selectedWrapper.erc7984Address) + : formatAddress(selectedWrapper.erc20Address)} +
+ )} +
+ + {/* Progress Steps */} + {txStep > 0 && ( +
+
+ {action === 'wrap' && needsApproval && ( + <> +
= 2 ? 'completed' : txStep === 1 ? 'active' : ''}`}> +
{txStep >= 2 ? : '1'}
+ Approve +
+
+ + )} +
= 4 ? 'completed' : txStep === 3 ? 'active' : ''}`}> +
+ {txStep >= 4 ? : (action === 'wrap' && needsApproval) ? '2' : '1'} +
+ {action === 'wrap' ? 'Shield' : 'Unwrap'} +
+
+ {action === 'unwrap' && ( + <> +
= 4 ? 'active' : ''}`}> +
{txStep === 5 ? : '2'}
+ Finalize +
+
+ + )} +
+
+ {txStep === 5 ? : action === 'unwrap' ? '3' : (needsApproval ? '3' : '2')} +
+ Done +
+
+
+ )} + + {/* Primary Action Button — ALWAYS in this slot */} +
+ {!isConnected ? ( + + ) : isChainMismatch ? ( + + ) : txStep === 5 ? ( + + ) : needsApproval ? ( + + ) : ( + + )} +
+ + {/* Transaction Info — appears BELOW the primary button, never replaces it */} + {(activeTxHash || finalTxHash) && ( +
+
+
+ + {txStep === 5 + ? `${action === 'wrap' ? 'Shield' : 'Unshield'} Successful` + : txStep === 2 + ? 'Approval Pending...' + : txStep === 4 + ? `${action === 'wrap' ? 'Shielding' : 'Unshielding'} Pending...` + : 'Transaction Submitted'} + + + {formatAddress(finalTxHash || activeTxHash || '')} + +
+ + View Explorer + +
+ + {txStep > 0 && txStep < 5 && ( +
+ ⚠️ + + FHE transactions require Zama Gateway & Coprocessor proof generation, which takes 15–40 seconds to confirm on-chain. Please keep this window open. + +
+ )} +
+ )} + + + {/* Info */} + +
+
+ +
+ + {action === 'wrap' + ? 'Shielding wraps your public ERC-20 tokens into encrypted ERC-7984 confidential tokens. Your balance and amounts are encrypted on-chain.' + : 'Unshielding burns your encrypted wrappers and releases the equivalent underlying ERC-20 tokens back to your public address.'} + +
+
+ + {/* Compact wallet activity feed */} + {isConnected && address && wrappers.length > 0 && ( + + )} +
+ + {/* Connect Wallet Modal */} + {isConnectModalOpen && ( + setIsConnectModalOpen(false)} + title="Connect Wallet" + > +
+
Select a wallet provider:
+ {connectors.map(c => ( + + ))} +
+
+ )} + + {isSuccessModalOpen && finalTxHash && ( + { + setIsSuccessModalOpen(false); + setTxStep(0); + setAmount(''); + setFinalTxHash(undefined); + setActiveTxHash(undefined); + }} + action={action} + amount={amount} + tokenSymbol={selectedWrapper?.symbol ?? selectedToken} + txHash={finalTxHash} + /> + )} +
+ ); +} + +export default function WrapPage() { + return ( + +
+

+ Shield Tokens +

+
+
+ } + > + + + ); +} diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..55e0297 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,85 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; + +export default function ErrorBoundary({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+

+ Something went wrong +

+

+ An unexpected error occurred. You can try reloading the page or return to the home screen. +

+
+ Error details +
+          {error.message}
+        
+ {error.digest && ( +

Digest: {error.digest}

+ )} +
+
+ + + Back to Registry + +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 124bea6..b92afdc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,5 @@ /* ========================================================================== - ZAMAVAULT — PREMIUM UNIVERSAL DESIGN SYSTEM + SHADOWLINE — PREMIUM UNIVERSAL DESIGN SYSTEM Multi-Theme Design Directions: Cyber, Nebula, Nordic, Emerald ========================================================================== */ @@ -66,6 +66,8 @@ table { border-collapse: collapse; } --accent-muted: rgba(56, 189, 248, 0.18); --accent-subtle: rgba(56, 189, 248, 0.05); --accent-glow: rgba(56, 189, 248, 0.2); + --zama-gold: #FFD208; + --zama-gold-glow: rgba(255, 210, 8, 0.15); --text-primary: #f8f9fa; --text-secondary: #a0a5b5; @@ -241,38 +243,349 @@ html[data-design-theme='aurora'] { /* ---------- 5. LIGHT MODE OVERRIDES ---------- */ html[data-theme='light'] { - --bg-base: #f4f4f5; + --bg-base: #fafafa; --bg-surface: #ffffff; - --bg-elevated: #e4e4e7; + --bg-elevated: #f4f4f5; --bg-card: #ffffff; --bg-card-hover: #ffffff; --bg-input: #ffffff; - --accent: #09090b; - --accent-hover: #18181b; - --accent-muted: rgba(9, 9, 11, 0.08); - --accent-subtle: rgba(9, 9, 11, 0.03); - --accent-glow: rgba(9, 9, 11, 0.04); - - --text-primary: #09090b; - --text-secondary: #3f3f46; - --text-muted: #71717a; - --text-accent: #09090b; - --text-inverse: #ffffff; + --accent: #FFD208; + --accent-hover: #e0b806; + --accent-muted: rgba(255, 210, 8, 0.15); + --accent-subtle: rgba(255, 210, 8, 0.04); + --accent-glow: rgba(255, 210, 8, 0.2); + + --text-primary: #000000; + --text-secondary: #27272a; + --text-muted: #52525b; + --text-accent: #000000; + --text-inverse: #000000; - --border: #d4d4d8; + --border: #e4e4e7; --border-hover: #a1a1aa; - --border-accent: rgba(9, 9, 11, 0.4); - --border-focus: #09090b; + --border-accent: #FFD208; + --border-focus: #000000; --shadow-sm: 0 1px 3px rgba(0,0,0,0.05); --shadow-md: 0 4px 10px rgba(0,0,0,0.06); --shadow-lg: 0 10px 25px rgba(0,0,0,0.08); - --shadow-glow: 0 0 12px rgba(9, 9, 11, 0.02); + --shadow-glow: 0 0 16px rgba(255, 210, 8, 0.18); - --radial-glow-1: rgba(9, 9, 11, 0.01); - --radial-glow-2: rgba(113, 128, 150, 0.01); - --grid-line: rgba(0, 0, 0, 0.025); + --radial-glow-1: rgba(255, 210, 8, 0.03); + --radial-glow-2: rgba(0, 0, 0, 0.01); + --grid-line: rgba(0, 0, 0, 0.015); +} + +/* ========================================================================== + HERO SECTION + ========================================================================== */ + +.hero-section { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; + padding: var(--sp-20) var(--sp-4) var(--sp-12); +} + +.hero-gradient-overlay { + position: absolute; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 30% 40%, rgba(56, 189, 248, 0.06) 0%, transparent 60%), + radial-gradient(ellipse 50% 40% at 70% 30%, var(--zama-gold-glow) 0%, transparent 50%), + linear-gradient(to bottom, transparent 70%, var(--bg-base) 100%); + pointer-events: none; + z-index: 1; +} + +.hero-content { + position: relative; + z-index: 2; + text-align: center; + max-width: 800px; + will-change: transform, opacity; +} + +/* ── Headline ── */ +.hero-headline-wrap { + position: relative; +} + +.hero-headline { + font-size: clamp(2.5rem, 6vw, 5rem); + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--text-primary); + margin: 0; +} + +.hero-headline-line { + display: inline-block; +} + +.hero-headline-shimmer { + background: linear-gradient( + 105deg, + var(--text-primary) 35%, + var(--accent) 50%, + var(--text-primary) 65% + ); + background-size: 250% 100%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmerText 6s ease-in-out infinite; +} + +@keyframes shimmerText { + 0%, 100% { background-position: 100% 50%; } + 50% { background-position: 0% 50%; } +} + +.hero-sub { + font-size: var(--text-lg); + color: var(--text-secondary); + max-width: 560px; + margin: 0 auto; + line-height: var(--lh-relaxed); +} + +/* ── Floating badges ── */ +.hero-badges { + position: absolute; + inset: -40px; + pointer-events: none; + z-index: 0; +} + +.hero-badge { + position: absolute; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + color: var(--accent); + opacity: 0.15; + padding: 3px 8px; + border: 1px solid var(--accent); + border-radius: var(--radius-sm); + animation: floatBadge 8s ease-in-out infinite alternate; + backdrop-filter: blur(4px); +} + +@keyframes floatBadge { + 0% { transform: translateY(0) rotate(0deg); opacity: 0.1; } + 50% { opacity: 0.25; } + 100% { transform: translateY(-20px) rotate(3deg); opacity: 0.1; } +} + +/* ── CTA Buttons ── */ +.hero-cta-row { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-4); + margin-top: var(--sp-8); + flex-wrap: wrap; +} + +.hero-btn-primary { + position: relative; + display: inline-flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-4) var(--sp-8); + font-size: var(--text-base); + font-weight: 700; + color: var(--text-inverse); + background: var(--accent); + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + overflow: hidden; + text-decoration: none; + transition: transform var(--t-fast), box-shadow var(--t-fast); + clip-path: polygon(8px 0, calc(100% - 8px) 0, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px), 0 8px); +} + +.hero-btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 25px var(--accent-glow), 0 8px 40px var(--zama-gold-glow); +} + +.hero-btn-primary:active { + transform: scale(0.97); +} + +.hero-btn-shimmer { + position: absolute; + inset: 0; + background: linear-gradient( + 105deg, + transparent 30%, + rgba(255, 255, 255, 0.25) 50%, + transparent 70% + ); + background-size: 300% 100%; + background-position: 200% 0; + transition: background-position 0.6s ease; + pointer-events: none; +} + +.hero-btn-primary:hover .hero-btn-shimmer { + background-position: -100% 0; +} + +.hero-btn-secondary { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-6); + font-size: var(--text-sm); + font-weight: 600; + color: var(--text-secondary); + background: transparent; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + cursor: pointer; + position: relative; + overflow: hidden; + transition: color var(--t-fast), border-color var(--t-fast); + clip-path: polygon(6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px), calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px), 0 6px); +} + +.hero-btn-secondary::before { + content: ''; + position: absolute; + inset: 0; + background: var(--accent-muted); + transform: scaleX(0); + transform-origin: center; + transition: transform 0.3s var(--ease); +} + +.hero-btn-secondary:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.hero-btn-secondary:hover::before { + transform: scaleX(1); +} + +/* ── Stats row ── */ +.hero-stats { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-6); + margin-top: var(--sp-10); + padding-top: var(--sp-8); + border-top: 1px solid var(--border); + opacity: 0; + animation: fadeIn 0.6s var(--ease) 2.2s forwards; +} + +.hero-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.hero-stat-value { + font-size: var(--text-xl); + font-weight: 800; + color: var(--accent); + font-family: var(--font-mono); +} + +.hero-stat-label { + font-size: var(--text-xs); + color: var(--text-muted); + text-transform: lowercase; + letter-spacing: 0.05em; +} + +.hero-stat-divider { + width: 1px; + height: 32px; + background: var(--border); +} + +/* ── Scroll hint ── */ +.hero-scroll-hint { + position: absolute; + bottom: var(--sp-8); + left: 50%; + transform: translateX(-50%); + z-index: 2; +} + +.hero-scroll-line { + width: 1px; + height: 40px; + background: linear-gradient(to bottom, var(--accent), transparent); + animation: scrollPulse 2s ease-in-out infinite; +} + +@keyframes scrollPulse { + 0%, 100% { opacity: 0.3; transform: scaleY(1); } + 50% { opacity: 0.8; transform: scaleY(1.3); } +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .hero-section { + min-height: 85vh; + padding: var(--sp-16) var(--sp-3) var(--sp-8); + } + + .hero-headline { + font-size: clamp(2rem, 8vw, 3rem); + } + + .hero-sub { + font-size: var(--text-base); + } + + .hero-stats { + gap: var(--sp-4); + } + + .hero-badges { + display: none; + } + + .hero-cta-row { + flex-direction: column; + } + + .hero-btn-primary, + .hero-btn-secondary { + width: 100%; + justify-content: center; + } +} + +@media (max-width: 480px) { + .hero-headline { + font-size: clamp(1.75rem, 10vw, 2.5rem); + } + + .hero-stats { + flex-wrap: wrap; + gap: var(--sp-3); + } + + .hero-stat-divider { + display: none; + } } /* ---------- BASE STYLES ---------- */ @@ -493,8 +806,7 @@ h4 { font-size: var(--text-xl); } font-weight: 600; padding: 3px 10px; border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.02em; } .badge-default { background: var(--bg-elevated); color: var(--text-secondary); border: 1px solid var(--border); } @@ -564,6 +876,53 @@ h4 { font-size: var(--text-xl); } gap: var(--sp-2); } +/* ---------- Registry pair cards ---------- */ +/* Each token pair is a single rounded/bordered card holding exactly two rows + (public token, confidential wrapper) laid out on the same grid as the column + header bar above the list. Reuses existing tokens only — same border, radius, + surface, and spacing scale as the rest of the app; no new palette. */ +.registry-pair-columns { + grid-template-columns: minmax(210px, 1.7fr) minmax(170px, 1.3fr) 90px minmax(170px, 1.2fr) minmax(220px, 1fr); +} +.registry-grid-wrap { overflow-x: auto; } +.registry-grid-header { + display: grid; + min-width: 780px; + padding: var(--sp-4) var(--sp-5); + font-weight: 600; + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: rgba(0,0,0,0.15); + margin-bottom: var(--sp-3); + align-items: center; + gap: var(--sp-3); +} +.registry-pair-list { + display: flex; + flex-direction: column; + gap: var(--sp-3); + min-width: 780px; +} +.registry-pair-card { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-card); + backdrop-filter: blur(12px); + overflow: hidden; +} +.registry-pair-row { + display: grid; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-4) var(--sp-5); +} +.registry-pair-row + .registry-pair-row { border-top: 1px dashed var(--border); } +.registry-pair-row:hover { background: rgba(255,255,255,0.015); } + /* ---------- Network Badge ---------- */ .network-switcher { display: flex; @@ -592,6 +951,34 @@ h4 { font-size: var(--text-xl); } .network-option:not(.active):hover { color: var(--text-primary); } /* ---------- Swap Card ---------- */ + +/* In-flight pulse: subtle gold border halo while a tx is pending. */ +.swap-card-pending { + animation: pulse-gold 2.2s ease-in-out infinite; + position: relative; +} +.swap-card-pending::after { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + pointer-events: none; + border: 1px solid rgba(255, 210, 8, 0.4); + animation: pulse-gold-ring 2.2s ease-in-out infinite; +} +@keyframes pulse-gold { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(255, 210, 8, 0.08), 0 0 0 0 rgba(255, 210, 8, 0); + } + 50% { + box-shadow: 0 0 0 4px rgba(255, 210, 8, 0.12), 0 0 28px 2px rgba(255, 210, 8, 0.18); + } +} +@keyframes pulse-gold-ring { + 0%, 100% { opacity: 0.35; } + 50% { opacity: 0.85; } +} + .swap-panel { background: var(--bg-input); border: 1px solid var(--border); @@ -750,8 +1137,8 @@ h4 { font-size: var(--text-xl); } z-index: 100; height: var(--header-h); background: rgba(var(--bg-base), 0.85); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(80px) saturate(200%); + -webkit-backdrop-filter: blur(80px) saturate(200%); border-bottom: 1px solid var(--border); display: flex; align-items: center; @@ -771,7 +1158,8 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } width: 100%; max-width: var(--container-max); margin: 0 auto; - padding: 0 var(--sp-6); + padding: 0 var(--sp-4); + gap: var(--sp-2); } .header-logo { @@ -781,52 +1169,56 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } font-size: var(--text-xl); font-weight: 800; letter-spacing: -0.03em; + flex-shrink: 0; } -.header-nav { display: flex; align-items: center; gap: var(--sp-1); } +/* Nav sits between the logo and the actions cluster. `min-width: 0` lets it + shrink instead of forcing header-inner to overflow the viewport. All the app + routes are shown directly, so link padding is kept tight to fit the row at + common desktop widths (1280px+); below the tablet breakpoint it collapses + into the hamburger drawer. */ +.header-nav { + display: flex; + align-items: center; + gap: 2px; + min-width: 0; + flex-shrink: 1; + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} +.header-nav::-webkit-scrollbar { + display: none; +} .header-link { - padding: var(--sp-2) var(--sp-4); - font-size: var(--text-sm); + padding: 6px 8px; + font-size: 13.5px; font-weight: 500; color: var(--text-secondary); border-radius: var(--radius-md); transition: all var(--t-fast); + white-space: nowrap; } .header-link:hover { color: var(--text-primary); background: var(--bg-elevated); } -.header-link.active { color: var(--accent); background: var(--accent-subtle); } -.header-actions { display: flex; align-items: center; gap: var(--sp-3); } - -/* ---------- Footer ---------- */ -.footer { - border-top: 1px solid var(--border); - padding: var(--sp-12) 0; - margin-top: var(--sp-20); - background: var(--bg-surface); -} - -/* ---------- Keyframe Animations ---------- */ -@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } -@keyframes slideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } } -@keyframes slideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } -@keyframes spin { to { transform: rotate(360deg); } } -@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } -@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } -@keyframes shrink { from { width: 100%; } to { width: 0%; } } - -.animate-fade-in { animation: fadeIn 0.4s var(--ease); } -.animate-slide-up { animation: slideUp 0.4s var(--ease); } +.header-link.active { color: var(--text-primary) !important; background: var(--bg-elevated) !important; } +.header-actions { display: flex; align-items: center; gap: var(--sp-3); flex-shrink: 0; } -/* ---------- Theme Switcher Dropdown ---------- */ -.theme-selector-dropdown { - position: relative; +/* ---------- "More" nav dropdown ---------- */ +.nav-more-wrapper { position: relative; } +.nav-more-trigger { + display: inline-flex; + align-items: center; + background: none; + border: none; + cursor: pointer; + font-family: inherit; } - -.theme-dropdown-menu { +.nav-more-menu { position: absolute; - right: 0; + left: 0; top: calc(100% + var(--sp-2)); - width: 180px; + width: 200px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-md); @@ -834,8 +1226,7 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } padding: 4px; z-index: 200; } - -.theme-dropdown-item { +.nav-more-item { width: 100%; padding: 8px 12px; display: flex; @@ -847,13 +1238,1641 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } border-radius: var(--radius-sm); transition: all var(--t-fast); } +.nav-more-item:hover { background: var(--bg-elevated); color: var(--text-primary); } +.nav-more-item.active { color: var(--accent); background: var(--accent-subtle); } -.theme-dropdown-item:hover { - background: var(--bg-elevated); - color: var(--text-primary); +/* ---------- Mobile hamburger + drawer ---------- */ +.nav-hamburger { display: none; } + +.mobile-nav-overlay { + position: fixed; + inset: 0; + z-index: 300; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); } -.theme-dropdown-item.active { - color: var(--accent); - background: var(--accent-subtle); +.mobile-nav-panel { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: min(320px, 85vw); + background: var(--bg-surface); + border-left: 1px solid var(--border); + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + overflow-y: auto; } + +.mobile-nav-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-4); + border-bottom: 1px solid var(--border); +} + +.mobile-nav-list { + display: flex; + flex-direction: column; + padding: var(--sp-2); +} + +.mobile-nav-link { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + font-size: var(--text-base); + font-weight: 500; + color: var(--text-secondary); + border-radius: var(--radius-md); +} + +.mobile-nav-link:hover { background: var(--bg-elevated); color: var(--text-primary); } +.mobile-nav-link.active { color: var(--text-primary); background: var(--bg-elevated); } + +.btn-primary-black { + background: var(--text-primary) !important; + color: var(--bg-surface) !important; + box-shadow: var(--shadow-sm); + border: none; +} +.btn-primary-black:hover { + background: var(--accent-hover) !important; + opacity: 0.95; + box-shadow: var(--shadow-md); +} + +/* ---------- Footer ---------- */ +.footer { + border-top: 1px solid var(--border); + padding: var(--sp-12) 0; + margin-top: var(--sp-20); + background: var(--bg-surface); +} + +/* ---------- Keyframe Animations ---------- */ +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes slideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } } +@keyframes slideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } +@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } +@keyframes shrink { from { width: 100%; } to { width: 0%; } } + +.animate-fade-in { animation: fadeIn 0.4s var(--ease); } +.animate-slide-up { animation: slideUp 0.4s var(--ease); } + +/* ---------- Theme Switcher Dropdown ---------- */ +.theme-selector-dropdown { + position: relative; +} + +.theme-dropdown-menu { + position: absolute; + right: 0; + top: calc(100% + var(--sp-2)); + width: 180px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: 4px; + z-index: 200; +} + +.theme-dropdown-item { + width: 100%; + padding: 8px 12px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: var(--text-sm); + font-weight: 500; + color: var(--text-secondary); + border-radius: var(--radius-sm); + transition: all var(--t-fast); +} + +.theme-dropdown-item:hover { + background: var(--bg-elevated); + color: var(--text-primary); +} + +.theme-dropdown-item.active { + color: var(--accent); + background: var(--accent-subtle); +} + +/* ========================================================================== + LEARN PAGE — Interactive Tutorial + ========================================================================== */ + +.learn-page { + max-width: var(--container-max); + margin: 0 auto; + padding: var(--sp-8) var(--sp-4); +} + +.learn-header { + text-align: center; + margin-bottom: var(--sp-10); +} + +/* ── Progress bar ── */ +.learn-progress-bar { + display: flex; + justify-content: center; + gap: 0; + margin-bottom: var(--sp-8); + overflow-x: auto; + padding: var(--sp-2) 0; +} + +.learn-progress-step { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-4); + border-radius: var(--radius-md); + transition: all var(--t-fast); + cursor: pointer; + background: transparent; + color: var(--text-muted); + position: relative; + min-width: 100px; +} + +.learn-progress-step:hover { + color: var(--text-secondary); + background: var(--bg-elevated); +} + +.learn-progress-step.active { + color: var(--accent); + background: var(--accent-subtle); +} + +.learn-progress-step.complete { + color: var(--success); +} + +.learn-progress-icon { + width: 40px; + height: 40px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + border: 2px solid currentColor; + transition: all var(--t-fast); +} + +.learn-progress-step.active .learn-progress-icon { + border-color: var(--accent); + background: var(--accent-muted); +} + +.learn-progress-step.complete .learn-progress-icon { + border-color: var(--success); + background: var(--success-muted); +} + +.learn-progress-label { + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; +} + +.learn-progress-number { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; +} + +.learn-progress-title { + font-size: var(--text-xs); + font-weight: 600; +} + +.learn-progress-connector { + display: none; /* Connectors hidden for cleaner mobile layout */ +} + +/* ── Content card ── */ +.learn-content-card { + margin-bottom: var(--sp-8); +} + +.learn-content-header { + margin-bottom: var(--sp-6); + padding-bottom: var(--sp-6); + border-bottom: 1px solid var(--border); +} + +.learn-content-icon { + width: 48px; + height: 48px; + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +/* ── Step body ── */ +.learn-step-body { + padding: 0 var(--sp-2); +} + +.learn-lead { + font-size: var(--text-base); + line-height: var(--lh-relaxed); + color: var(--text-secondary); + margin-bottom: var(--sp-6); + max-width: 720px; +} + +.learn-lead strong { + color: var(--text-primary); +} + +/* ── Diagram ── */ +.learn-diagram { + margin: var(--sp-6) 0; + overflow-x: auto; +} + +.learn-diagram-row { + display: flex; + align-items: center; + gap: var(--sp-4); + justify-content: center; + min-width: 500px; +} + +.learn-diagram-box { + padding: var(--sp-4); + border-radius: var(--radius-md); + border: 1px solid; + background: var(--bg-surface); + text-align: center; + min-width: 140px; + transition: transform var(--t-fast); +} + +.learn-diagram-box:hover { + transform: translateY(-2px); +} + +/* ── Highlights grid ── */ +.learn-highlights { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--sp-4); + margin: var(--sp-6) 0; +} + +/* ── Key terms ── */ +.learn-key-terms { + margin-top: var(--sp-6); + padding: var(--sp-5); + border-radius: var(--radius-md); + background: var(--bg-surface); + border: 1px solid var(--border); +} + +.learn-terms-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--sp-4); +} + +.learn-term { + display: flex; + flex-direction: column; + gap: var(--sp-1); +} + +.learn-term-code { + font-family: var(--font-mono); + font-size: var(--text-sm); + font-weight: 600; + color: var(--accent); + background: var(--accent-subtle); + padding: 2px 8px; + border-radius: var(--radius-sm); + width: fit-content; +} + +/* ── Instructions (numbered list) ── */ +.learn-instructions { + display: flex; + flex-direction: column; + gap: var(--sp-4); + margin: var(--sp-4) 0; +} + +.learn-instruction { + display: flex; + gap: var(--sp-4); + align-items: flex-start; +} + +.learn-instruction-number { + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: var(--accent-muted); + color: var(--accent); + font-weight: 700; + font-size: var(--text-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 1.5px solid var(--accent); +} + +/* ── Callout box ── */ +.learn-callout-box { + margin-top: var(--sp-6); + padding: var(--sp-5); + border-radius: var(--radius-md); + border: 1px solid; +} + +/* ── Navigation ── */ +.learn-nav { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: var(--sp-8); + padding-top: var(--sp-6); + border-top: 1px solid var(--border); + flex-wrap: wrap; + gap: var(--sp-3); +} + +/* ── Resources footer ── */ +.learn-resources { + margin-top: var(--sp-4); +} + +.learn-resources-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--sp-3); +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .learn-progress-bar { + justify-content: flex-start; + gap: 0; + } + + .learn-progress-step { + min-width: 70px; + padding: var(--sp-2); + } + + .learn-progress-title { + display: none; + } + + .learn-diagram-row { + min-width: 0; + flex-direction: column; + } + + .learn-diagram-row svg[class*="arrow"] { + transform: rotate(90deg); + } + + .learn-highlights { + grid-template-columns: 1fr; + } + + .learn-terms-grid { + grid-template-columns: 1fr; + } + + .learn-nav { + flex-direction: column; + align-items: stretch; + } +} + +/* ========================================================================== + DEVELOPERS PAGE — Code Snippet Generator + ========================================================================== */ + +.dev-page { + max-width: var(--container-max); + margin: 0 auto; + padding: var(--sp-8) var(--sp-4); +} + +.dev-header { + text-align: center; + margin-bottom: var(--sp-8); +} + +.dev-layout { + display: grid; + grid-template-columns: 300px 1fr; + gap: var(--sp-6); + align-items: start; +} + +/* ── Controls panel ── */ +.dev-controls { + display: flex; + flex-direction: column; + gap: var(--sp-4); + position: sticky; + top: calc(var(--header-h) + var(--sp-4)); +} + +.dev-op-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-2); +} + +.dev-op-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-2); + border-radius: var(--radius-md); + background: var(--bg-surface); + border: 1px solid var(--border); + cursor: pointer; + transition: all var(--t-fast); + color: var(--text-secondary); +} + +.dev-op-btn:hover { + border-color: var(--op-color, var(--accent)); + color: var(--text-primary); + background: var(--bg-elevated); +} + +.dev-op-btn.active { + border-color: var(--op-color, var(--accent)); + color: var(--op-color, var(--accent)); + background: color-mix(in srgb, var(--op-color, var(--accent)) 8%, transparent); +} + +.dev-op-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.dev-op-label { + font-size: var(--text-xs); + font-weight: 600; + text-align: center; +} + +.dev-op-rest { + flex-direction: row; + gap: var(--sp-3); + justify-content: center; +} + +.dev-select { + width: 100%; + padding: var(--sp-3) var(--sp-3); + border-radius: var(--radius-md); + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text-primary); + font-size: var(--text-sm); + cursor: pointer; + transition: border-color var(--t-fast); +} + +.dev-select:focus { + border-color: var(--accent); + outline: none; +} + +.dev-select option { + background: var(--bg-surface); + color: var(--text-primary); +} + +.dev-fw-list { + display: flex; + flex-direction: column; + gap: var(--sp-2); +} + +.dev-fw-btn { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-2); + padding: var(--sp-3); + border-radius: var(--radius-md); + background: var(--bg-surface); + border: 1px solid var(--border); + cursor: pointer; + transition: all var(--t-fast); + color: var(--text-secondary); + font-size: var(--text-sm); + width: 100%; +} + +.dev-fw-btn:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.dev-fw-btn.active { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-subtle); +} + +/* ── Code output ── */ +.dev-output { + min-width: 0; +} + +.dev-code-card { + overflow: hidden; +} + +.dev-code-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + background: var(--bg-elevated); +} + +.dev-code-body { + overflow-x: auto; +} + +.dev-code-pre { + padding: var(--sp-5); + margin: 0; + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.7; + color: var(--text-primary); + tab-size: 2; + white-space: pre; + overflow-x: auto; +} + +.dev-code-pre code { + font-family: inherit; +} + +/* ── Responsive ── */ +@media (max-width: 900px) { + .dev-layout { + grid-template-columns: 1fr; + } + + .dev-controls { + position: static; + flex-direction: row; + flex-wrap: wrap; + } + + .dev-controls > * { + flex: 1 1 260px; + } + + .dev-op-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 480px) { + .dev-op-grid { + grid-template-columns: 1fr; + } +} + +/* ========================================================================== + ANALYTICS PAGE + ========================================================================== */ + +.analytics-stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--sp-4); + margin-bottom: var(--sp-8); +} + +.analytics-insights-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-4); + margin-bottom: var(--sp-6); +} + +.analytics-main-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-6); + margin-bottom: var(--sp-8); +} + +/* ── TVL bar ── */ +.analytics-tvl-row { + display: flex; + align-items: center; + gap: var(--sp-3); +} + +.analytics-tvl-info { + display: flex; + align-items: center; + gap: var(--sp-3); + width: 80px; + flex-shrink: 0; +} + +.analytics-tvl-bar-wrap { + flex: 1; + height: 8px; + background: var(--bg-elevated); + border-radius: var(--radius-full); + overflow: hidden; +} + +.analytics-tvl-bar-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent) 0%, color-mix(in srgb, var(--accent) 60%, transparent) 100%); + border-radius: var(--radius-full); + transition: width 0.6s var(--ease); + min-width: 4px; +} + +.analytics-tvl-stats { + width: 80px; + flex-shrink: 0; +} + +/* ── Activity row ── */ +.analytics-activity-row { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3); + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--bg-surface); + transition: border-color var(--t-fast); +} + +.analytics-activity-row:hover { + border-color: var(--border-hover); +} + +/* ── Responsive ── */ +@media (max-width: 1024px) { + .analytics-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .analytics-stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: var(--sp-3); + } + .analytics-insights-grid { + grid-template-columns: 1fr; + } + .analytics-main-grid { + grid-template-columns: 1fr; + } + .analytics-tvl-info { + width: 60px; + } + .analytics-tvl-stats { + width: 60px; + } +} + +@media (max-width: 480px) { + .analytics-stats-grid { + grid-template-columns: 1fr 1fr; + } +} + +/* ========================================================================== + DOCS PAGE — Developer Documentation + ========================================================================== */ + +.docs-page { + display: grid; + grid-template-columns: 260px 1fr; + min-height: calc(100vh - var(--header-h)); + max-width: 1400px; + margin: 0 auto; +} + +/* ── Sidebar ── */ +.docs-sidebar { + position: sticky; + top: var(--header-h); + height: calc(100vh - var(--header-h)); + overflow-y: auto; + padding: var(--sp-6) var(--sp-4); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: var(--sp-5); + background: var(--bg-surface); +} + +.docs-sidebar-title { + display: flex; + align-items: center; + gap: var(--sp-2); + font-weight: 700; + font-size: var(--text-sm); + color: var(--text-primary); + padding: var(--sp-2) var(--sp-2) var(--sp-4); + border-bottom: 1px solid var(--border); +} + +.docs-sidebar-nav { + display: flex; + flex-direction: column; + gap: var(--sp-5); + flex: 1; +} + +.docs-nav-group { + display: flex; + flex-direction: column; + gap: var(--sp-1); +} + +.docs-nav-group-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + padding: 0 var(--sp-2); + margin-bottom: var(--sp-1); +} + +.docs-nav-item { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-3); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + background: none; + border: none; + text-align: left; + width: 100%; + transition: all var(--t-fast); +} + +.docs-nav-item:hover { + color: var(--text-primary); + background: var(--bg-elevated); +} + +.docs-nav-item.active { + color: var(--accent); + background: var(--accent-subtle); + font-weight: 600; +} + +.docs-sidebar-footer { + display: flex; + flex-direction: column; + gap: var(--sp-2); + padding-top: var(--sp-4); + border-top: 1px solid var(--border); +} + +.docs-ext-link { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: var(--text-xs); + color: var(--text-muted); + transition: color var(--t-fast); +} + +.docs-ext-link:hover { + color: var(--accent); +} + +/* ── Main content ── */ +.docs-content { + padding: var(--sp-8) var(--sp-10); + max-width: 860px; + width: 100%; +} + +/* ── Sections ── */ +.docs-section { + margin-bottom: var(--sp-16); + scroll-margin-top: calc(var(--header-h) + var(--sp-4)); +} + +.docs-section-title { + font-size: var(--text-2xl); + font-weight: 800; + color: var(--text-primary); + margin-bottom: var(--sp-5); + padding-bottom: var(--sp-4); + border-bottom: 1px solid var(--border); + letter-spacing: -0.03em; +} + +.docs-subsection { + margin-top: var(--sp-8); + scroll-margin-top: calc(var(--header-h) + var(--sp-4)); +} + +.docs-subsection-title { + font-size: var(--text-lg); + font-weight: 700; + color: var(--text-primary); + margin-bottom: var(--sp-4); +} + +.docs-h4 { + font-size: var(--text-sm); + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: var(--sp-6) 0 var(--sp-3); +} + +.docs-lead { + font-size: var(--text-base); + line-height: var(--lh-relaxed); + color: var(--text-secondary); + margin-bottom: var(--sp-6); +} + +.docs-lead strong { + color: var(--text-primary); +} + +.docs-p { + font-size: var(--text-sm); + line-height: var(--lh-relaxed); + color: var(--text-secondary); + margin-bottom: var(--sp-4); +} + +.docs-p strong { color: var(--text-primary); } + +.docs-list { + list-style: disc; + padding-left: var(--sp-5); + display: flex; + flex-direction: column; + gap: var(--sp-2); + margin-top: var(--sp-3); +} + +.docs-list li { + font-size: var(--text-sm); + color: var(--text-secondary); + line-height: var(--lh-relaxed); +} + +/* ── Code blocks ── */ +.docs-code-block { + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border); + margin: var(--sp-4) 0; +} + +.docs-code-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-2) var(--sp-4); + background: var(--bg-elevated); + border-bottom: 1px solid var(--border); +} + +.docs-code-lang { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + letter-spacing: 0.03em; +} + +.docs-code-pre { + padding: var(--sp-5); + margin: 0; + overflow-x: auto; + background: var(--bg-surface); + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.75; + color: var(--text-primary); + tab-size: 2; + white-space: pre; +} + +/* ── Tables ── */ +.docs-table { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + margin: var(--sp-3) 0; +} + +.docs-table th { + background: var(--bg-elevated); + color: var(--text-muted); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: var(--sp-3) var(--sp-4); + text-align: left; +} + +.docs-prop-row td { + padding: var(--sp-3) var(--sp-4); + border-top: 1px solid var(--border); + vertical-align: top; + line-height: var(--lh-relaxed); +} + +.docs-prop-name { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--accent); + background: var(--accent-subtle); + padding: 1px 6px; + border-radius: var(--radius-sm); +} + +.docs-prop-required { + font-size: 10px; + font-weight: 700; + color: var(--error); + background: var(--error-muted); + padding: 1px 5px; + border-radius: var(--radius-sm); + margin-left: 5px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.docs-prop-type { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + background: var(--bg-elevated); + padding: 1px 6px; + border-radius: var(--radius-sm); +} + +.docs-prop-desc { + color: var(--text-secondary); + font-size: var(--text-sm); +} + +/* ── Endpoint badge ── */ +.docs-endpoint { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-2) var(--sp-4); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + margin-bottom: var(--sp-5); + font-family: var(--font-mono); +} + +.docs-endpoint-method { + font-size: var(--text-xs); + font-weight: 700; + color: var(--success); + background: var(--success-muted); + padding: 2px 8px; + border-radius: var(--radius-sm); + letter-spacing: 0.05em; +} + +.docs-endpoint-path { + font-size: var(--text-sm); + color: var(--text-primary); +} + +/* ── Info / callout boxes ── */ +.docs-info-box { + padding: var(--sp-4) var(--sp-5); + border-radius: var(--radius-md); + border: 1px solid rgba(59, 130, 246, 0.3); + background: rgba(59, 130, 246, 0.05); + font-size: var(--text-sm); + color: var(--text-secondary); + line-height: var(--lh-relaxed); + margin: var(--sp-5) 0; +} + +.docs-info-box strong { color: var(--text-primary); } + +.docs-callout { + padding: var(--sp-4) var(--sp-5); + border-radius: var(--radius-md); + border: 1px solid; + font-size: var(--text-sm); + line-height: var(--lh-relaxed); + margin: var(--sp-5) 0; +} + +.docs-callout strong { font-weight: 700; } + +.docs-callout-warning { + border-color: rgba(245, 158, 11, 0.35); + background: rgba(245, 158, 11, 0.05); + color: var(--text-secondary); +} + +.docs-callout-error { + border-color: rgba(239, 68, 68, 0.35); + background: rgba(239, 68, 68, 0.05); + color: var(--text-secondary); +} + +/* ── Feature grid ── */ +.docs-feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--sp-4); + margin: var(--sp-5) 0; +} + +.docs-feature-card { + display: flex; + gap: var(--sp-4); + padding: var(--sp-5); + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--bg-surface); + transition: border-color var(--t-fast); +} + +.docs-feature-card:hover { + border-color: var(--border-hover); +} + +.docs-feature-icon { + font-size: 24px; + flex-shrink: 0; + line-height: 1; +} + +.docs-feature-body strong { + font-weight: 600; + font-size: var(--text-sm); + color: var(--text-primary); +} + +/* ── Hook cards ── */ +.docs-hook-card { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); + margin: var(--sp-6) 0; + background: var(--bg-surface); +} + +.docs-hook-header { + margin-bottom: var(--sp-4); +} + +.docs-hook-name { + font-family: var(--font-mono); + font-size: var(--text-base); + font-weight: 700; + color: var(--accent); +} + +.docs-hook-desc { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-top: var(--sp-2); + line-height: var(--lh-relaxed); +} + +/* ── Steps ── */ +.docs-steps { + display: flex; + flex-direction: column; + gap: var(--sp-4); + margin: var(--sp-5) 0; +} + +.docs-step { + display: flex; + gap: var(--sp-4); + align-items: flex-start; +} + +.docs-step-num { + width: 28px; + height: 28px; + border-radius: var(--radius-full); + background: var(--accent-muted); + color: var(--accent); + font-weight: 700; + font-size: var(--text-xs); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 1.5px solid var(--accent); +} + +/* ── Address table ── */ +.docs-address-table-wrap { + margin: var(--sp-4) 0; +} + +.docs-address-registry { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-bottom: none; + border-radius: var(--radius-md) var(--radius-md) 0 0; + gap: var(--sp-4); + flex-wrap: wrap; +} + +.docs-addr-mono { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-primary); +} + +.docs-addr-short { + color: var(--text-secondary); + font-size: 11px; +} + +/* ── Footer ── */ +.docs-footer { + margin-top: var(--sp-16); + padding-top: var(--sp-8); + border-top: 1px solid var(--border); +} + +/* ── Multi-page docs: page header ── */ +.docs-page-header { + margin-bottom: var(--sp-8); + padding-bottom: var(--sp-6); + border-bottom: 1px solid var(--border); +} +.docs-eyebrow { + display: inline-block; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: var(--sp-3); +} +.docs-page-title { + font-size: var(--text-3xl); + font-weight: 800; + letter-spacing: -0.03em; + color: var(--text-primary); + margin: 0 0 var(--sp-3); + line-height: 1.1; +} +.docs-page-desc { + font-size: var(--text-md); + color: var(--text-secondary); + line-height: 1.6; + margin: 0; + max-width: 62ch; +} + +/* Section heading inside a page body */ +.docs-h2 { + font-size: var(--text-xl); + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; + margin: var(--sp-10) 0 var(--sp-4); + padding-bottom: var(--sp-3); + border-bottom: 1px solid var(--border); +} + +/* Prose links */ +.docs-p a, +.docs-lead a, +.docs-list a, +.docs-callout a, +.docs-info-box a, +.docs-faq-item a, +.docs-page-desc a { + color: var(--text-primary); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--border-hover); + font-weight: 600; +} +.docs-p a:hover, +.docs-lead a:hover, +.docs-list a:hover, +.docs-callout a:hover, +.docs-info-box a:hover, +.docs-faq-item a:hover, +.docs-page-desc a:hover { + text-decoration-color: var(--text-primary); +} + +/* Sidebar items are now s */ +a.docs-nav-item { text-decoration: none; } + +/* Success callout (info=neutral, warning=amber, error=red already exist) */ +.docs-callout-success { + border-left: 3px solid var(--success); + background: color-mix(in srgb, var(--success) 8%, transparent); +} + +/* FAQ items reuse .docs-h2 for questions but tighten the spacing */ +.docs-faq-item + .docs-faq-item { margin-top: var(--sp-2); } +.docs-faq-item .docs-h2 { font-size: var(--text-lg); } + +/* SVG diagrams */ +.docs-diagram { + margin: var(--sp-6) 0; + padding: var(--sp-5); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-surface); +} +.docs-diagram svg { display: block; } +.docs-diagram figcaption { + margin-top: var(--sp-3); + text-align: center; + font-size: var(--text-xs); + color: var(--text-muted); +} + +/* Prev / Next pager */ +.docs-pager { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-4); + margin-top: var(--sp-16); + padding-top: var(--sp-8); + border-top: 1px solid var(--border); +} +.docs-pager-link { + display: flex; + flex-direction: column; + gap: 4px; + padding: var(--sp-4); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + text-decoration: none; + transition: all var(--t-fast); + background: var(--bg-surface); +} +.docs-pager-link:hover { + border-color: var(--accent); + background: var(--bg-elevated); +} +.docs-pager-next { text-align: right; align-items: flex-end; } +.docs-pager-dir { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: var(--text-xs); + font-weight: 600; + color: var(--text-muted); +} +.docs-pager-label { + font-size: var(--text-sm); + font-weight: 700; + color: var(--text-primary); +} + +@media (max-width: 640px) { + .docs-pager { grid-template-columns: 1fr; } + .docs-pager-next { text-align: left; align-items: flex-start; } +} + +/* ── Mobile toggle ── */ +.docs-mobile-toggle { + display: none; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-4); + font-size: var(--text-sm); + font-weight: 600; + color: var(--text-primary); + border-bottom: 1px solid var(--border); + background: var(--bg-surface); + cursor: pointer; + width: 100%; + grid-column: 1 / -1; +} + +.docs-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.4); + z-index: 49; +} + +/* ── Responsive ── */ +@media (max-width: 1024px) { + .docs-page { + grid-template-columns: 220px 1fr; + } + + .docs-content { + padding: var(--sp-8) var(--sp-6); + } +} + +@media (max-width: 768px) { + .docs-page { + grid-template-columns: 1fr; + position: relative; + } + + .docs-mobile-toggle { + display: flex; + position: sticky; + top: var(--header-h); + z-index: 50; + } + + .docs-sidebar { + position: fixed; + top: var(--header-h); + left: 0; + height: calc(100vh - var(--header-h)); + width: 280px; + z-index: 50; + transform: translateX(-100%); + transition: transform var(--t-fast); + border-right: 1px solid var(--border); + box-shadow: var(--shadow-lg); + } + + .docs-sidebar.open { + transform: translateX(0); + } + + .docs-overlay { + display: block; + } + + .docs-content { + padding: var(--sp-6) var(--sp-4); + } + + .docs-feature-grid { + grid-template-columns: 1fr; + } + + .docs-table { + font-size: var(--text-xs); + } + + .docs-table th, + .docs-prop-row td { + padding: var(--sp-2) var(--sp-3); + } +} + +/* ========================================================================== + TABLET — trim non-essential header chrome before the hamburger kicks in + ========================================================================== */ + +@media (max-width: 1100px) { + .header-inner { padding: 0 var(--sp-4); } + /* Design theme swapper is a nice-to-have — first thing to go under + pressure so Registry/Wrap/.../Faucet + network switcher + wallet + always have room. */ + .theme-selector-dropdown { display: none; } +} + +/* ========================================================================== + MOBILE RESPONSIVE — Global fixes for 360px–768px + ========================================================================== */ + +@media (max-width: 768px) { + /* Header: hide nav labels, collapse to icon-only on very small screens */ + .header-inner { + padding: 0 var(--sp-3); + gap: var(--sp-2); + } + + .header-nav { + display: none; /* replaced by the hamburger drawer below this width */ + } + + .nav-hamburger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + } + + .header-actions { + gap: var(--sp-2); + } + + /* Container padding */ + .container { + padding: 0 var(--sp-3); + } + + /* Page headers */ + .page-header { + padding: var(--sp-6) 0 var(--sp-4); + } + + h1 { font-size: var(--text-3xl); } + h2 { font-size: var(--text-2xl); } + + /* Registry table: horizontal scroll */ + .registry-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + /* Portfolio grid: single column */ + .grid-2 { + grid-template-columns: 1fr !important; + } + + /* Swap card */ + .swap-panel { + padding: var(--sp-4); + } + + /* Modal */ + .modal-content { + max-width: calc(100vw - 32px); + margin: 0 var(--sp-4); + } + + /* Steps (wrap flow) */ + .steps { + gap: var(--sp-2); + flex-wrap: wrap; + } +} + +/* ── Mobile: hide registry address columns ─────────────────────────── */ +@media (max-width: 768px) { + .registry-addr-col { display: none; } + + /* Prevent iOS auto-zoom on inputs inside swap/transfer panels */ + .swap-panel select, + .swap-panel input[type="text"], + .swap-panel input[type="number"] { + font-size: 16px; + } +} + +/* ── Grid-3: responsive breakpoints (previously missing) ───────────── */ +@media (max-width: 900px) { + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 480px) { + .grid-3 { grid-template-columns: 1fr; } +} + +@media (max-width: 480px) { + .header-logo span { display: none; } /* hide text, keep logo icon */ + .header-logo svg, .header-logo img { margin: 0; } + + /* Collapse "Connect Wallet" to icon-only so header fits in 375px */ + .btn-connect-label { display: none; } + + /* Network switcher: compact */ + .network-switcher { + gap: 2px; + } + + .network-option { + padding: 4px 8px !important; + font-size: 11px; + } + + /* Theme palette button: hide label */ + .theme-selector-dropdown .btn span:not(:first-child) { + display: none; + } + + /* Typography */ + h1 { font-size: var(--text-2xl); } + + /* Stat cards: 1 column */ + .analytics-stats-grid { + grid-template-columns: 1fr 1fr; + } + + /* Faucet input */ + .faucet-amount-row { + flex-direction: column; + } + + /* Docs content */ + .docs-content { + padding: var(--sp-4) var(--sp-3); + } + + /* Dev page code block */ + .dev-code-pre { + font-size: 11px; + padding: var(--sp-3); + } +} + +/* ---------- LANDING PAGE DARK MODE OVERRIDES ---------- */ +.landing-page-container[data-theme='dark'] { + background: var(--bg-base) !important; + color: var(--text-primary) !important; +} +.landing-page-container[data-theme='dark'] section[style*="background: #fff"], +.landing-page-container[data-theme='dark'] section[style*="background: rgb(255, 255, 255)"], +.landing-page-container[data-theme='dark'] section[style*="background: #ffffff"] { + background: var(--bg-surface) !important; + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] section[style*="background: #fafafa"], +.landing-page-container[data-theme='dark'] section[style*="background: rgb(250, 250, 250)"] { + background: var(--bg-base) !important; + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] [style*="color: #000"], +.landing-page-container[data-theme='dark'] [style*="color: rgb(0, 0, 0)"], +.landing-page-container[data-theme='dark'] [style*="color: #000000"] { + color: var(--text-primary) !important; +} +.landing-page-container[data-theme='dark'] [style*="color: #71717a"], +.landing-page-container[data-theme='dark'] [style*="color: rgb(113, 113, 122)"], +.landing-page-container[data-theme='dark'] [style*="color: #52525b"] { + color: var(--text-muted) !important; +} +.landing-page-container[data-theme='dark'] [style*="border: 1px solid #e4e4e7"], +.landing-page-container[data-theme='dark'] [style*="border-top: 1px solid #e4e4e7"], +.landing-page-container[data-theme='dark'] [style*="border-bottom: 1px solid #e4e4e7"], +.landing-page-container[data-theme='dark'] [style*="border: 1px solid rgb(228, 228, 231)"] { + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] [style*="background: #f4f4f5"], +.landing-page-container[data-theme='dark'] .pill { + background: var(--bg-elevated) !important; + color: var(--text-secondary) !important; + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] nav a[style*="color: #52525b"] { + color: var(--text-secondary) !important; +} + diff --git a/src/app/icon.jpg b/src/app/icon.jpg new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/src/app/icon.jpg differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7694c1a..52a68fa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,48 +1,53 @@ -import type { Metadata } from "next"; -import "./globals.css"; -import ClientLayout from "./ClientLayout"; +import type { Metadata } from 'next'; +import { Fraunces, Plus_Jakarta_Sans } from 'next/font/google'; + +/* Fraunces — optical-size variable serif. Distinctive editorial feel, + completely unlike Zama's clean tech sans. Used for hero + section h2. */ +const fraunces = Fraunces({ + subsets: ['latin'], + variable: '--font-fraunces', + display: 'swap', +}); + +/* Plus Jakarta Sans — humanist rounded sans. Modern, not overused in crypto. + Used for body text, nav, UI labels. */ +const jakarta = Plus_Jakarta_Sans({ + subsets: ['latin'], + variable: '--font-jakarta', + display: 'swap', +}); export const metadata: Metadata = { - title: "ZamaVault — Confidential Wrapper Registry", + title: 'ShadowLine — Confidential Tokens, Made Simple', description: - "Discover, wrap, and manage confidential ERC-7984 tokens on the Zama Protocol. The canonical interface for converting ERC-20 tokens to their encrypted counterparts using Fully Homomorphic Encryption.", + "Discover, shield, and manage confidential ERC-7984 tokens on Zama's fully-homomorphic encryption protocol. The canonical interface for the Confidential Wrappers Registry.", keywords: [ - "Zama", - "FHE", - "ERC-7984", - "confidential tokens", - "wrapper registry", - "privacy", - "Ethereum", - "DeFi", + 'Zama', 'FHE', 'fully homomorphic encryption', 'ERC-7984', + 'confidential tokens', 'wrapper registry', 'privacy', 'Ethereum', 'DeFi', ], openGraph: { - title: "ZamaVault — Confidential Wrapper Registry", + title: 'ShadowLine — Confidential Tokens, Made Simple', description: - "The definitive interface for managing confidential ERC-7984 tokens powered by Fully Homomorphic Encryption.", - type: "website", + "The definitive interface for managing confidential ERC-7984 tokens powered by Zama's Fully Homomorphic Encryption.", + type: 'website', }, }; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - +