diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..9f93da6 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "ZamaVault Dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000, + "autoPort": true + }, + { + "name": "ZamaVault Production Preview", + "runtimeExecutable": "npx", + "runtimeArgs": ["next", "start", "-p", "3030"], + "port": 3030 + } + ] +} diff --git a/.env.example b/.env.example index 9c3ad78..2012454 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,8 @@ 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://zamavault.vercel.app +# Falls back to relative paths when not set. +NEXT_PUBLIC_APP_URL= diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md new file mode 100644 index 0000000..76abe01 --- /dev/null +++ b/AGENT_GUIDE.md @@ -0,0 +1,468 @@ +# ZamaVault — Master Reference Document for AI Agents + +> این فایل یک راهنمای کامل برای هر agent هوش مصنوعی است که روی پروژه ZamaVault کار می‌کند. +> شامل همه جزئیات معماری، قوانین حیاتی، اشتباهات رایج، و تجربیات کسب‌شده است. + +--- + +## ۱. بستر پروژه + +**ZamaVault** یک رابط کاربری برای اکوسیستم توکن محرمانه Zama است: +ثبت‌نام پویای Registry، wrap/unwrap ERC-20، رمزگشایی پورتفولیوی رمزنگاری‌شده، و فاست Sepolia. + +- **مخزن:** https://github.com/hosein-ul/zamavault +- **شاخه:** `feat/dynamic-registry-finding-1` (PR #1 → main) +- **Stack:** Next.js 16 (App Router, Turbopack), React 19, Wagmi 3, Viem 2, @zama-fhe/react-sdk 3.0.1, TypeScript 5 +- **هدف:** Zama Developer Program Mainnet Season 3 Bounty Track (deadline: 2026-07-07 AOE) +- **دایرکتوری:** `C:\NEW work\` + +--- + +## ۲. قوانین حیاتی — هرگز نقض نشوند + +### 🚨 قانون ۱: هیچ اشاره‌ای به Claude/Anthropic +هرگز نام Claude، Claude Code، Anthropic، یا هر چیز مشابه را در کد، commit‌ها، PR، README، یا هر فایل قابل مشاهده توسط داوران قرار ندهید. هیچ `Co-Authored-By` header نزنید. این یک ارسال مسابقه است. + +### 🚨 قانون ۲: هرگز permit EIP-712 را auto-fire نکنید +هر decrypt/permit باید پشت یک کلیک صریح کاربر باشد. الگوی `decryptRequested` در `wrap/page.tsx` دقیقاً برای این است: +- `decryptRequested` به طور پیش‌فرض `false` است +- فقط وقتی کاربر روی "Decrypt" کلیک می‌کند `true` می‌شود +- **سورس باگ رایج:** صدا زدن `refetch()` مستقیم روی `useConfidentialBalance` — TanStack Query متد `refetch()` را از `enabled: false` bypass می‌کند! +- **راه‌حل:** بعد از هر shield/unshield موفق، `setDecryptRequested(false)` صدا بزنید +- Reset را **به صورت synchronous** در `onChange` کنید، نه در `useEffect` (race condition یک فریمی) + +### 🚨 قانون ۳: Decimal Scaling بار حیاتی دارد +``` +wrapper decimals: همیشه 6 (محدودیت euint64 FHE) +underlying decimals: 6 یا 18 + +Shield (wrap): parseAmount(input, underlyingDecimals) ← دسیمال underlying +Unshield: parseAmount(input, 6) ← همیشه 6 +Display: formatAmount(balance, 6) ← همیشه 6 + +⚠️ اشتباه رایج: formatAmount(balance, 18) → نمایش صفر برای توکن‌های 18 دسیمال +``` + +### 🚨 قانون ۴: هرگز secret commit نکنید +فایل‌های `.env` در `.gitignore` هستند. از `.env.example` برای مستندسازی استفاده کنید. + +### 🚨 قانون ۵: Blocklist را بدون تأیید تیم حذف نکنید +`BLOCKLISTED_WRAPPERS` در `registry.ts` دارای مستندات است. `cbbqTGBP` (Mainnet) آدرس vanity مشکوک دارد. + +--- + +## ۳. معماری و ساختار فایل + +``` +C:\NEW work\ +├── src/ +│ ├── app/ +│ │ ├── page.tsx ← جدول Registry (صفحه اصلی) +│ │ ├── wrap/page.tsx ← Shield/Unshield (مهم‌ترین صفحه) +│ │ ├── portfolio/page.tsx ← پورتفولیو محرمانه با batch decrypt +│ │ ├── faucet/page.tsx ← فاست توکن mock سپولیا +│ │ ├── analytics/page.tsx ← داشبورد TVL و activity +│ │ ├── learn/page.tsx ← آموزش تعاملی ۵ مرحله‌ای FHE +│ │ ├── developers/page.tsx ← مولد کد snippet +│ │ ├── docs/page.tsx ← مستندات developer +│ │ ├── error.tsx ← مرز خطای Next.js +│ │ ├── api/registry/route.ts ← REST API عمومی +│ │ ├── ClientLayout.tsx ← provider‌های context (theme/network) +│ │ ├── layout.tsx ← layout ریشه با تزریق theme SSR +│ │ └── globals.css ← سیستم طراحی (4 تم Nordic) +│ ├── components/ +│ │ ├── ui/ ← 13 کامپوننت UI قابل استفاده مجدد +│ │ │ ├── Badge.tsx ← ⚠️ text-transform:uppercase حذف شد (باگ fix) +│ │ │ ├── Button.tsx ← forwardRef, variants: primary/secondary/ghost/danger +│ │ │ ├── Card.tsx ← variants: default/glass/outlined/accent +│ │ │ ├── CopyButton.tsx ← کپی clipboard با بازخورد چک‌مارک +│ │ │ ├── Skeleton.tsx ← props: width, height, variant (نه style!) +│ │ │ ├── Toast.tsx ← ToastProvider + useToast() hook +│ │ │ ├── Modal.tsx +│ │ │ ├── Tooltip.tsx ← Portal-based, HelpCircle پیش‌فرض trigger +│ │ │ ├── TokenIcon.tsx ← نقشه symbol→لوگو، حذف prefix "c" و suffix "Mock" +│ │ │ ├── BlurIn.tsx ← انیمیشن blur-in +│ │ │ ├── TypingAnimation.tsx +│ │ │ ├── Spinner.tsx +│ │ │ └── TransactionSuccessModal.tsx +│ │ ├── layout/ +│ │ │ ├── Header.tsx ← ۸ مسیر nav، wallet connect، network switcher +│ │ │ └── Footer.tsx +│ │ └── PendingUnshieldBanner.tsx ← recovery برای unshield قطع‌شده +│ ├── config/ +│ │ ├── contracts.ts ← WrapperPair، KNOWN_WRAPPERS (fallback)، REGISTRY_ADDRESSES +│ │ ├── chains.ts ← SupportedChainId، explorer Blockscout +│ │ └── tokens.ts ← متادیتای نمایش توکن (لوگو، رنگ) +│ ├── lib/ +│ │ ├── registry.ts ← useRegistryPairs، isMintablePair، blocklist +│ │ ├── errors.ts ← classifyError با 16 کد خطا +│ │ ├── utils.ts ← formatAmount، parseAmount، formatAddress، cn +│ │ ├── wrapper-abi.ts ← WRAPPER_ABI، ERC20_ABI +│ │ └── __tests__/utils.test.ts ← 30 تست Vitest +│ └── providers/ +│ └── Providers.tsx ← Wagmi + Zama + TanStack Query (⚠️ بسیار حساس) +├── .github/workflows/ci.yml ← TypeScript → Vitest → Build +├── .env.example ← همه متغیرهای محیطی اختیاری هستند +├── vitest.config.ts ← alias @/ → src/ +├── CLAUDE.md ← مستندات پروژه +└── memory.md ← درس‌های آموخته‌شده +``` + +--- + +## ۴. چرخه SDK Zama (از دیدگاه flow) + +### Shield (Wrap) +``` +useShield({ tokenAddress: erc7984Address }) +→ mutateAsync({ amount, onApprovalSubmitted, onShieldSubmitted }) +→ اگر allowance < amount: approve ERC-20 → onApprovalSubmitted(txHash) → setTxStep(2) +→ shield tx → onShieldSubmitted(txHash) → setTxStep(4) +→ res.txHash → setTxStep(5), setDecryptRequested(false) +``` + +### Unshield (Unwrap) — دو مرحله‌ای +``` +useUnshield(erc7984Address) ← positional در v3.0.1 +→ mutateAsync({ amount, onUnwrapSubmitted, onFinalizing, onFinalizeSubmitted }) +→ unwrap tx on-chain → onUnwrapSubmitted(txHash) → setTxStep(4) +→ Gateway اثبات رمزگشایی تولید می‌کند → onFinalizing() → toast "15-40s" +→ finalize tx → onFinalizeSubmitted(txHash) → setTxStep(5) +``` + +### Decrypt Balance — PERMIT GATING +``` +const [decryptRequested, setDecryptRequested] = useState(false) + +useConfidentialBalance( + { tokenAddress }, + { enabled: decryptRequested && !!address } +) + +// ✅ درست: فقط روی کلیک صریح + + +// ❌ غلط: refetch() مستقیم enabled را bypass می‌کند! +refetchWrapperBalance() ← هرگز در success handler نزنید + +// ✅ بعد از هر tx موفق: +setDecryptRequested(false) ← جلوگیری از refetchOnWindowFocus +``` + +### Batch Decrypt (Portfolio) +``` +useConfidentialBalances( + { tokenAddresses: requestedAddresses }, + { enabled: isConnected && requestedAddresses.length > 0 } +) +// یک EIP-712 permit، همه توکن‌ها را decrypt می‌کند +``` + +--- + +## ۵. الگوهای کلیدی React + +### الگوی تشخیص Registry +```typescript +// useRegistryPairs محاسبه می‌کند +const isChainAligned = isConnected && chain?.id === chainId + +if (isChainAligned && sdkResult.data?.items?.length > 0) → live data +else if (isChainAligned && sdkResult.isLoading) → loading + fallback +else → KNOWN_WRAPPERS fallback (isFromCache: true) +``` + +### الگوی جایگزینی Tooltip +```tsx +// ✅ درست: ? icon جداگانه +Confidential + {/* HelpCircle پیش‌فرض نشان می‌دهد */} + +// ❌ غلط: badge به‌عنوان trigger +Confidential +``` + +### اشتباه Skeleton Props +```tsx +// ✅ درست + + +// ❌ غلط — Skeleton prop style ندارد! + +``` + +### Badge — text-transform حذف شد +```css +/* ❌ قبلاً وجود داشت — باگ cZAMA → CZAMA */ +.badge { text-transform: uppercase; } + +/* ✅ الان — متن دقیقاً همان‌طور که هست نمایش داده می‌شود */ +/* text-transform حذف شد — cZAMA همیشه cZAMA می‌ماند */ +``` + +--- + +## ۶. درس‌های آموخته‌شده (از اشتباهات گذشته) + +### ۶.۱ باگ Auto-Permit (بارها رخ داد) +**علت اصلی:** TanStack Query's `refetch()` از `enabled: false` bypass می‌کند. +بعد از shield موفق، `refetchWrapperBalance()` صدا می‌زدیم → permit بدون کلیک کاربر fire می‌شد → کیف پول ۳ بار prompt می‌داد. +**راه‌حل:** هرگز `refetchWrapperBalance()` در handler‌های موفقیت صدا نزنید. فقط `refetchPublicBalance()` و `refetchAllowance()` مجاز است. + +### ۶.۲ باگ Display صفر برای توکن‌های ۱۸ دسیمال +**علت:** `formatAmount(balance, 18)` روی موجودی FHE با ۶ دسیمال → مقدار نزدیک به صفر. +**راه‌حل:** همیشه `formatAmount(balance, 6)` برای موجودی‌های FHE confidential. + +### ۶.۳ باگ Race Condition برای Token Select +**علت:** `setDecryptRequested(false)` فقط در `useEffect` → یک فریم delay → درخواست قدیمی `true` با آدرس توکن جدید trigger می‌شد. +**راه‌حل:** reset را **synchronously** در `onChange` handler انجام دهید. + +### ۶.۴ Tooltip چه‌طور باید باشد +**علت:** Tooltip کل button را wrap می‌کرد → کلیک روی Tooltip کار نمی‌کرد. +**راه‌حل:** همیشه `` بدون children (→ ? icon) را **بعد از** button/badge قرار دهید. + +### ۶.۵ باگ TVL Ranking +**علت:** Sort روی raw bigint — ZAMA با ۱۸ دسیمال raw value بزرگتری از USDC با ۶ دسیمال داشت. +**راه‌حل:** `tvlHuman` (float نرمال‌شده) را محاسبه کنید و برای sort و عرض bar استفاده کنید. + +### ۶.۶ CZAMA بجای cZAMA +**علت:** `.badge { text-transform: uppercase }` در CSS. +**راه‌حل:** `text-transform` از `.badge` حذف شد. نام‌گذاری: همیشه lowercase `c` — `cZAMA`، `cUSDC`، `cWETH`. + +### ۶.۷ API Domain هاردکد +**علت:** `zamavault.xyz` مستقیم در کد نوشته شد. +**راه‌حل:** از `process.env.NEXT_PUBLIC_APP_URL` استفاده کنید. در Vercel: Settings → Environment Variables. + +### ۶.۸ useCallback در PendingUnshieldBanner +**علت:** `useCallback` با `sdk?.storage` dependency مشکل react-hooks lint داشت. +**راه‌حل:** از plain async functions به جای `useCallback` استفاده کنید. + +### ۶.۹ Unshield step indicator +**علت:** UI همیشه Approve step نشان می‌داد حتی وقتی allowance کافی بود. +**راه‌حل:** `setTxStep(needsApproval ? 1 : 3)` — Approve step فقط وقتی `needsApproval === true` نشان داده می‌شود. + +### ۶.۱۰ اسکرول CSS بدون `margin: auto` +**علت:** `maxWidth: 640` بدون `margin: '... auto 0'` → متن به چپ می‌رفت. +**راه‌حل:** همیشه `margin: 'var(--sp-3) auto 0'` برای centered containers با max-width. + +--- + +## ۷. قراردادها و استانداردها + +### نام‌گذاری توکن‌ها +``` +ERC-20 عمومی: ZAMA، USDC، USDT، WETH، BRON، tGBP، XAUt +ERC-7984 محرمانه: cZAMA، cUSDC، cUSDT، cWETH، cBRON، ctGBP، cXAUt + ↑ همیشه lowercase c +توکن‌های mock: نمایش: "ZAMA" + badge "Mock" (نه "ZAMAMock") +``` + +### Tooltip +- متن کوتاه: ۱-۲ خط حداکثر +- هیچ‌وقت badge/button را به‌عنوان trigger tooltip wrap نکنید +- همیشه `` standalone (→ ? icon) + +### Explorer +- **Blockscout** (نه Etherscan) — چون FHE/Zama protocol decode را پشتیبانی می‌کند +- Sepolia: `https://eth-sepolia.blockscout.com` +- Mainnet: `https://eth.blockscout.com` + +### Commit Messages +- هیچ اشاره‌ای به AI یا Claude +- format: `fix(scope): description`، `feat: description` + +### CSS Variables +```css +--text-xs، --text-sm، --text-base، --text-lg، --text-xl، --text-2xl، --text-3xl +--sp-1 (4px)، --sp-2 (8px)، --sp-3 (12px)، --sp-4 (16px)، --sp-6 (24px)، --sp-8 (32px) +--accent، --success، --warning، --error، --info +--bg-base، --bg-surface، --bg-elevated، --bg-card +--border، --border-hover، --border-accent +--radius-sm، --radius-md، --radius-lg، --radius-xl +``` + +--- + +## ۸. SDK Zama — مرجع سریع + +### نسخه و API +- **نسخه:** `@zama-fhe/react-sdk@3.0.1` — هنوز v2 TypeScript API +- Migration guide برای ۳.۱.x است — تا وقتی از `@^3.0` استفاده می‌کنیم، نیازی به migration نیست +- `WagmiSigner`، `RelayerWeb`، `indexedDBStorage` هنوز در v3.0.1 export می‌شوند + +### Hook‌های مهم +| Hook | Package | نحوه فراخوانی | +|------|---------|-------------| +| `useListPairs({ page, pageSize, metadata })` | react-sdk | config object | +| `useShield({ tokenAddress })` | react-sdk | config object | +| `useUnshield(tokenAddress)` | react-sdk | positional در v3! | +| `useConfidentialBalance({ tokenAddress }, { enabled })` | react-sdk | 2 arg | +| `useConfidentialBalances({ tokenAddresses }, { enabled })` | react-sdk | 2 arg | +| `useResumeUnshield({ tokenAddress })` | react-sdk | config object | +| `useRevokeSession()` | react-sdk | no args | +| `useZamaSDK()` | react-sdk | SDK instance | +| `loadPendingUnshield(storage, addr)` | react-sdk | function | +| `clearPendingUnshield(storage, addr)` | react-sdk | function | +| `matchZamaError(err, handlers)` | @zama-fhe/sdk | function | + +### کدهای خطا +`SIGNING_REJECTED`، `SIGNING_FAILED`، `ENCRYPTION_FAILED`، `DECRYPTION_FAILED`، `TRANSACTION_REVERTED`، `INVALID_KEYPAIR`، `KEYPAIR_EXPIRED`، `NO_CIPHERTEXT`، `RELAYER_REQUEST_FAILED`، `CONFIGURATION`، `INSUFFICIENT_CONFIDENTIAL_BALANCE`، `INSUFFICIENT_ERC20_BALANCE`، `BALANCE_CHECK_UNAVAILABLE`، `ERC20_READ_FAILED`، `ACL_PAUSED`، `APPROVAL_FAILED` + +--- + +## ۹. آدرس‌های قرارداد + +### WrappersRegistry +- Sepolia: `0x2f0750Bbb0A246059d80e94c454586a7F27a128e` +- Mainnet: `0xeb5015fF021DB115aCe010f23F55C2591059bBA0` + +### Sepolia — ۸ pair (۷ mock + ۱ restricted) +| Symbol | ERC-20 | ERC-7984 | Decimals | +|--------|--------|----------|---------| +| USDC | 0x9b5C...FfFF | 0x7c5B...3639 | 6/6 | +| USDT | 0xa7dA...b0 | 0x4E7B...491 | 6/6 | +| WETH | 0xff54...F3F | 0x4620...158 | 18/6 | +| ZAMA | 0x7535...F57 | 0xf2D6...bFB | 18/6 | +| BRON | 0xFf02...E | 0xaa56...891 | 18/6 | +| tGBP | 0x93c9...442 | 0xfCE5...7CC | 18/6 | +| XAUt | 0x2437...940 | 0xe4Fc...0C7 | 6/6 | +| ctGBP (restricted) | 0x167D...A208 | — | 18/6 | + +### Mainnet — ۷ pair + ۱ blocklisted +| Symbol | ERC-20 | ERC-7984 | Decimals | +|--------|--------|----------|---------| +| USDC | 0xa0b8...48 | 0xe978...2B2 | 6/6 | +| USDT | 0xdAC1...c7 | 0xAe02...c50 | 6/6 | +| WETH | 0xc02a...c2 | 0xda93...893 | 18/6 | +| ZAMA | 0xA12C...A3 | 0x80CB...071 | 18/6 | +| BRON | 0xBA2C...83 | 0x85dE...bc | 18/6 | +| tGBP | 0x27f6...87 | 0xa873...DD9 | 18/6 | +| XAUt | 0x6874...38 | 0x73cc...Ef1 | 6/6 | +| cbbqTGBP | **BLOCKLISTED** | آدرس vanity مشکوک | — | + +--- + +## ۱۰. صفحات و routing + +| مسیر | فایل | توضیح | +|------|------|-------| +| `/` | `app/page.tsx` | جدول Registry با موجودی‌های live | +| `/wrap` | `app/wrap/page.tsx` | Shield/Unshield با query `?token=SYMBOL&action=wrap` | +| `/portfolio` | `app/portfolio/page.tsx` | batch decrypt، activity feed | +| `/faucet` | `app/faucet/page.tsx` | فقط Sepolia، فقط mock token‌ها | +| `/analytics` | `app/analytics/page.tsx` | TVL، ratio، activity (24h) | +| `/learn` | `app/learn/page.tsx` | ۵ مرحله تعاملی | +| `/developers` | `app/developers/page.tsx` | snippet generator | +| `/docs` | `app/docs/page.tsx` | مستندات کامل | +| `/api/registry` | `app/api/registry/route.ts` | `?chain=sepolia\|mainnet` | + +**Nav items در Header (به ترتیب):** +Registry → Wrap → Portfolio → Faucet (TESTNET) → Learn → Dev Tools → Analytics → Docs + +--- + +## ۱۱. متغیرهای محیطی + +همه اختیاری هستند — app fallback عمومی دارد: + +```env +NEXT_PUBLIC_SEPOLIA_RPC= # Alchemy یا RPC دیگر (پیش‌فرض: publicnode) +NEXT_PUBLIC_MAINNET_RPC= # Alchemy یا RPC دیگر (پیش‌فرض: publicnode) +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # اگر نبود، فقط injected wallets +NEXT_PUBLIC_APP_URL= # URL deployment برای docs/API (مثال: https://zamavault.vercel.app) +``` + +**تنظیم در Vercel:** Settings → Environment Variables → Add → Deploy دوباره + +--- + +## ۱۲. CI/CD و دستورات + +```bash +# توسعه +npm run dev # سرور dev روی localhost:3000 + +# بررسی کیفیت +npm test # Vitest — 30 تست +npx tsc --noEmit # TypeScript type-check (باید clean باشد) +npm run lint # ESLint (advisory — برخی خطاهای pre-existing وجود دارند) + +# تولید +npx next build # باید موفق باشد +``` + +**GitHub Actions:** Push به `main` یا `feat/**` → TypeScript → Vitest → Next build +ESLint advisory است (نه blocking) — برخی خطاهای pre-existing در AUDIT_REPORT.md ردیابی شده‌اند. + +--- + +## ۱۳. الگوهای Error Handling + +### در هر catch block +```typescript +} catch (err: unknown) { // ← نه err: any + const classified = classifyError(err); + addToast({ + variant: classified.retryable ? 'warning' : 'error', + title: classified.title, + message: classified.message, + }); +} +``` + +### در REST API +```typescript +} catch (err) { + // fallback به KNOWN_WRAPPERS + return NextResponse.json({ pairs: fallback, source: 'cached-snapshot', warning: '...' }) +} +``` + +--- + +## ۱۴. تم‌ها و طراحی + +**۴ تم dark Nordic:** +- Charcoal (پیش‌فرض): accent `#38bdf8` (sky blue) +- Midnight: accent `#f4f4f5` (white) +- Frost: accent `#60a5fa` (ice blue) +- Aurora: accent `#2dd4bf` (teal) + +**Light mode:** accent `#09090b` (black/inverse) + +**فونت‌ها:** +- Sans: Plus Jakarta Sans +- Mono: JetBrains Mono + +--- + +## ۱۵. کارهای باقی‌مانده + +### کاربر باید انجام دهد +- D1: Deploy Vercel + تنظیم `NEXT_PUBLIC_APP_URL` +- D2: Mainnet Relayer API key (اختیاری، برای بهتر شدن UX) + +### کارهای آینده +- Phase 2.2: npm package `@zamavault/sdk` +- Phase 5.1: README rewrite کامل +- Phase 4.4: تست mobile responsive در 360px + +--- + +## ۱۶. URL های مهم مستندات Zama + +- SDK overview: https://docs.zama.org/protocol/sdk/overview +- useShield: https://docs.zama.org/protocol/sdk/api-references/react/useshield +- useUnshield: https://docs.zama.org/protocol/sdk/api-references/react/useunshield +- useConfidentialBalance: https://docs.zama.org/protocol/sdk/api-references/react/useconfidentialbalance +- useResumeUnshield: https://docs.zama.org/protocol/sdk/api-references/react/useresumeunshield +- matchZamaError/errors: https://docs.zama.org/protocol/sdk/api-references/sdk/errors +- WrappersRegistry: https://docs.zama.org/protocol/sdk/api-references/sdk/wrappersregistry +- Sepolia addresses: https://docs.zama.org/protocol/protocol-apps/addresses/testnet/sepolia +- Mainnet addresses: https://docs.zama.org/protocol/protocol-apps/addresses/mainnet/ethereum +- Migration v2→v3: https://docs.zama.org/protocol/sdk/migration/migrate-v2-to-v3 +- Relayer API keys: https://docs.zama.org/protocol/sdk/guides/authentication + +--- + +*آخرین بروزرسانی: 2026-06-24 | branch: feat/dynamic-registry-finding-1* diff --git a/README.md b/README.md index 1e07fb5..122ec7d 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,258 @@ -# 🛡️ ZamaVault — Confidential Asset Vault +# ZamaVault -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. +> The canonical web interface for the Zama Confidential Wrappers Registry — discover every ERC-20 ↔ ERC-7984 pair, shield in seconds, decrypt on demand. -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. +[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/) +[![React](https://img.shields.io/badge/React-19-61dafb?logo=react)](https://react.dev/) +[![Wagmi](https://img.shields.io/badge/Wagmi-3-1c1b1f?logo=ethereum)](https://wagmi.sh/) +[![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) --- -## ✨ Features +## What it is -* **🔒 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**: Reads Zama's on-chain `WrappersRegistry` directly (via `useListPairs` from `@zama-fhe/react-sdk`) and lists every registered ERC-20 ↔ ERC-7984 pair on the active network — including pairs added after this client was built. Falls back to a hardcoded snapshot when no wallet is connected so anonymous visitors can still browse. Revoked pairs (`isValid === false`) are flagged with a "Revoked" badge and hidden from the wrap/unwrap selector. -* **🚰 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. +ZamaVault is a Next.js dApp that turns the on-chain `WrappersRegistry` into a product. Every registered ERC-20 ↔ ERC-7984 confidential wrapper pair on **Sepolia** and **Ethereum mainnet** is discovered live; users can shield (wrap) public tokens into encrypted balances, transfer them privately, decrypt with a single EIP-712 signature, and unshield back to public ERC-20 — all from one consistent interface. + +Built around the official `@zama-fhe/react-sdk` and `WrappersRegistry` contract, it serves both end-users (Portfolio, Wrap, Faucet, Learn) and developers (REST API, snippet generator, on-chain Analytics). --- -## 🏗️ Technical Architecture & Stack +## Pages & routes -ZamaVault is designed as a secure, decentralized client-side application that interfaces with the Ethereum blockchain: +| Route | Purpose | +| ---------------------- | -------------------------------------------------------------------------------------- | +| `/` | Marketing landing page with scroll-narrative onboarding | +| `/app` | Live wrapper registry table with per-row public + confidential balance reads | +| `/app/wrap` | Shield (wrap) and unshield (unwrap) swap interface for any registered pair | +| `/app/portfolio` | Confidential portfolio with batch EIP-712 decrypt, re-shield, and unshield | +| `/app/analytics` | TVS (Total Value Shielded) by token + 24h shield/unshield activity, since 00:00 UTC | +| `/app/faucet` | Sepolia mock token mint (USDC, USDT, WETH, BRON, ZAMA, tGBP, XAUt) | +| `/app/learn` | 5-step interactive tutorial covering FHE → faucet → shield → decrypt → unshield | +| `/app/developers` | Copy-paste code snippets (React hook / viem / ethers) | +| `/app/docs` | In-app reference for the SDK calls ZamaVault uses | +| `/api/registry` | Public REST endpoint returning every wrapper pair on a given chain | -* **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. +The landing (`/`) and the application (`/app/*`) are deliberately isolated — they use independent stylesheets, fonts, and providers (see [Architecture](#architecture)). --- -## 🔑 FHE & Confidential Flows +## Architecture + +``` +src/ +├── app/ +│ ├── layout.tsx ← root: , fonts, theme bootstrap (no CSS) +│ ├── landing.css ← Tailwind v4, scoped to landing only +│ ├── page.tsx ← landing route (cream/gold design) +│ ├── error.tsx ← global error boundary +│ ├── ClientLayout.tsx ← providers + theme/network context (app only) +│ ├── globals.css ← app design system (vanilla CSS, scoped to /app) +│ ├── app/ +│ │ ├── layout.tsx ← imports globals.css + ClientLayout +│ │ ├── page.tsx ← /app — registry table +│ │ ├── wrap/page.tsx +│ │ ├── portfolio/page.tsx +│ │ ├── analytics/page.tsx +│ │ ├── faucet/page.tsx +│ │ ├── learn/page.tsx +│ │ ├── developers/page.tsx +│ │ └── docs/page.tsx +│ └── api/registry/route.ts ← public REST endpoint +├── components/ +│ ├── landing/ ← landing sections (hero, features, etc.) +│ ├── magic/ ← Magic UI primitives (TextAnimate, MagicCard…) +│ ├── ui/ ← in-app design system (Card, Button, Badge…) +│ ├── layout/ ← in-app Header, Footer +│ └── PendingUnshieldBanner.tsx +├── config/ +│ ├── chains.ts ← Sepolia + Mainnet definitions +│ ├── contracts.ts ← WrappersRegistry addresses, KNOWN_WRAPPERS fallback +│ └── tokens.ts ← logo + colour metadata keyed by symbol +├── lib/ +│ ├── registry.ts ← useRegistryPairs hook + blocklist + helpers +│ ├── errors.ts ← classifyError() using matchZamaError +│ ├── utils.ts ← formatAmount, parseAmount (decimal scaling) +│ └── wrapper-abi.ts ← WRAPPER_ABI, ERC20_ABI +└── providers/ + └── Providers.tsx ← Wagmi + ZamaProvider + TanStack Query +``` -### 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**. +### Two design systems, hermetically separated -### 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. +The landing (`/`) and the application (`/app/*`) need to look completely different — the landing is editorial gold-on-cream, the app is a dense data UI. They live in the same Next.js project but never share CSS: -### 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. +- `src/app/layout.tsx` (the root) imports **no** CSS. It only sets ``, the font variables (Fraunces + Plus Jakarta Sans), and the theme bootstrap script. +- `src/app/page.tsx` (landing) imports **`landing.css`** — Tailwind v4 with `@import 'tailwindcss'` and a `@theme` block of gold/cream/ink tokens. +- `src/app/app/layout.tsx` imports **`globals.css`** — the vanilla-CSS app design system with `--bg-base`, `--accent`, `--sp-*` tokens — and wraps everything in `ClientLayout` (providers + Header + Footer). + +Because Next.js route-subtree layouts only apply CSS within their own subtree, Tailwind's preflight never leaks into `/app`, and the app's design tokens never leak into `/`. --- -## 🚀 Getting Started +## FHE & flows + +### Permit-based decryption + +Confidential balances are stored on-chain as `euint64` ciphertext handles. To display the plaintext to the user, ZamaVault uses Zama's `useConfidentialBalance` hook, gated behind an **explicit user click** — the EIP-712 permit signature must never auto-fire on render. The flow: + +1. User clicks **Decrypt** next to any encrypted balance. +2. Wallet shows a typed-data signature request (EIP-712, not a transaction — zero gas). +3. The signature scopes an ephemeral session key to that wallet + contract. +4. SDK sends ciphertext + permit to the Zama Gateway/Coprocessor. +5. Coprocessor verifies the signature, decrypts, returns plaintext to the browser. +6. The plaintext is rendered client-side; private keys never leave the wallet and plaintext is never stored on-chain. + +### Shield (wrap) — one or two transactions + +`WrappedToken.shield(amount)` routes through one of two paths depending on the underlying ERC-20: + +| Path | Triggered when | Wallet prompts | Tokens (mainnet) | +| ------------------- | --------------------------------------------- | -------------- | -------------------- | +| `transferAndCall` | Underlying implements **ERC-1363** | 1 | cTGBP, cZAMA | +| `approve` + `wrap` | Underlying does **not** implement ERC-1363 | 2 | cUSDC, cUSDT, cWETH, cBRON | + +The SDK detects ERC-1363 support automatically via `supportsInterface` — callers don't choose a path. On the two-tx path, ZamaVault checks the existing ERC-20 allowance and passes `approvalStrategy: 'skip'` when it's already sufficient, so users aren't prompted for a fresh `approve(0)` + `approve(amount)` every time. + +### Unshield (unwrap) — two phases + +Unshielding is **not** a single transaction: + +1. **Unwrap request** — user submits an on-chain transaction burning the encrypted wrapper amount. +2. **Gateway finalize** — the Zama Gateway generates a decryption proof (~15–40 s), then submits a finalize transaction that releases the underlying ERC-20. + +If the user closes the browser between (1) and (2), ZamaVault's `PendingUnshieldBanner` detects the unfinalized request on next visit and offers a one-click Resume. + +### Decimal scaling + +All wrapper tokens use **6 decimals** regardless of the underlying token's precision. This is because FHE operates on `euint64` (a 64-bit unsigned integer), which would overflow at 18-decimal values. The wrapper contract scales amounts during shield and unshield. `formatAmount`/`parseAmount` in `src/lib/utils.ts` must be kept in sync with this constraint — see the inline comments. + +--- + +## Tech stack + +| Layer | Library / version | +| ----------------- | ---------------------------------------------------------------------------------------------------------------- | +| Framework | [Next.js 16](https://nextjs.org/) (App Router, Turbopack, static prerender) | +| UI runtime | [React 19](https://react.dev/) | +| Web3 | [Wagmi 3](https://wagmi.sh/) + [Viem 2](https://viem.sh/) | +| FHE | [`@zama-fhe/react-sdk` 3](https://docs.zama.org/protocol/sdk/overview) | +| Data fetching | [TanStack Query 5](https://tanstack.com/query) | +| Animation | [motion (Framer Motion) 12](https://motion.dev/) for landing primitives | +| 3D / canvas | [three.js](https://threejs.org/) + [@react-three/fiber](https://r3f.docs.pmnd.rs/) | +| Landing styling | [Tailwind v4](https://tailwindcss.com/) (scoped to `/`) | +| App styling | Vanilla CSS custom properties (scoped to `/app/*`) | +| Icons | [lucide-react](https://lucide.dev/) + [react-icons](https://react-icons.github.io/react-icons/) | +| Magic UI | [magicui.design](https://magicui.design/) — TextAnimate, MagicCard, BorderBeam, NumberTicker, Marquee, BlurFade | +| Confetti | [canvas-confetti](https://www.npmjs.com/package/canvas-confetti) | +| Language | TypeScript 5 (strict mode) | + +--- + +## Quick start ### Prerequisites -Make sure you have [Node.js (v18+)](https://nodejs.org/) and a package manager (`npm`, `pnpm`, or `yarn`) installed. - -### 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 - ``` - -### 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 +- Node.js v18 or newer +- npm / pnpm / yarn + +### Install + +```bash +git clone https://github.com/hosein-ul/zamavault.git +cd zamavault +npm install ``` -### Running Locally -To launch the local development server: +### Run + ```bash -npm run dev +npm run dev # Dev server on http://localhost:3000 +npm run build # Production build +npm run start # Serve production build +npm run lint # ESLint +npx tsc --noEmit # TypeScript check (no output files) ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. -### Building for Production -To generate a fully optimized static and server-rendered production build: +### Environment (all optional) + +ZamaVault works with no configuration — it falls back to public RPC endpoints. To use your own, create `.env.local`: + +```env +NEXT_PUBLIC_SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +NEXT_PUBLIC_MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY +``` + +--- + +## REST API + +`GET /api/registry?chain=sepolia|mainnet` + +Returns every registered wrapper pair on the given chain, with ERC-20 metadata enriched on the server. CORS-enabled, cached at the edge for 60 s. + +### Example + ```bash -npm run build -npm run start +curl https://your-deployment.vercel.app/api/registry?chain=sepolia +``` + +### Response + +```json +{ + "pairs": [ + { + "tokenAddress": "0x...", + "confidentialTokenAddress": "0x...", + "symbol": "USDC", + "confidentialSymbol": "cUSDC", + "name": "USD Coin", + "decimals": 6, + "wrapperDecimals": 6 + } + ], + "total": 8, + "chain": "sepolia", + "registryAddress": "0x...", + "timestamp": 1735000000000, + "source": "on-chain" +} ``` +If the on-chain read fails (RPC issue), the endpoint falls back to a hardcoded snapshot and sets `source: "cached-snapshot"` with a `warning` field. + --- -## 🎨 Theme & Design System +## Registry & blocklist + +ZamaVault reads the on-chain `WrappersRegistry` via `useListPairs` whenever a wallet is connected to a supported chain. When disconnected (or the chain doesn't match), it falls back to `KNOWN_WRAPPERS` in `src/config/contracts.ts` — a hand-curated snapshot — so anonymous visitors can still browse. + +Revoked pairs (`isValid === false`) are tagged with a red "Revoked" badge in the registry table and excluded from the wrap/unwrap selector. -ZamaVault supports multiple visual design themes which can be customized via the Header theme-selector. The default theme is **Nordic Clean**: +A small `BLOCKLISTED_WRAPPERS` set in `src/lib/registry.ts` hides one suspicious mainnet entry (`cbbqTGBP` at `0xBA4c…6762`) — vanity-prefix wrapper for an unknown underlying token; documented in code with the rationale. + +--- + +## Project conventions + +- **No auto-fire signatures.** Every `useConfidentialBalance` is gated behind a `decryptRequested` boolean set only by an explicit user click. The state pattern in `src/app/app/wrap/page.tsx` is the reference — do not weaken it. +- **No secrets in the repo.** `.env*` files are gitignored. `.env.example` documents the optional vars. +- **Strict mode TS.** All new code must type-check with `npx tsc --noEmit`. +- **Two design systems, never mix.** If a CSS rule touches the landing, it belongs in `landing.css`; if it touches the app, in `globals.css`. + +--- -* **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. +## Roadmap -All layouts, cards, buttons, badges, tables, and inputs are built using semantic variables declared in `src/app/globals.css`. +- [ ] Polygon, Base, Arbitrum support (once Zama deploys WrappersRegistry there) +- [ ] Confidential transfer UI (send encrypted balance to another address) +- [ ] Multi-account permit caching in browser storage +- [ ] WalletConnect v2 + Safe support +- [ ] Subgraph-backed historical analytics --- -## 📄 License +## License -This project is licensed under the MIT License. +MIT — see [LICENSE](LICENSE). 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/package-lock.json b/package-lock.json index 9a6b769..3fcd445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,24 +8,43 @@ "name": "zamavault", "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", "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", + "gsap": "^3.15.0", + "lenis": "^1.3.24", "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", + "tailwindcss": "^4.3.1", "typescript": "^5", "vitest": "^4.1.9" } @@ -36,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", @@ -228,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", @@ -276,6 +317,12 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -453,6 +500,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", @@ -1083,6 +1168,24 @@ "@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", @@ -1355,160 +1458,1750 @@ "url": "https://github.com/sponsors/Boshen" } }, - "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, + "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", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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, + "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", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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, + "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", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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, + "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", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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, + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "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/@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": [ + "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", @@ -1704,7 +3397,319 @@ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.8.0" + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "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": ">= 20" + } + }, + "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": { + "@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": ">=14.0.0" + } + }, + "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": ">= 20" + } + }, + "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", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "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": { + "@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/@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": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, "node_modules/@tanstack/query-core": { @@ -1733,6 +3738,12 @@ "react": "^18 || ^19" } }, + "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", @@ -1762,6 +3773,13 @@ "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", @@ -1769,6 +3787,12 @@ "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", @@ -1800,11 +3824,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" @@ -1814,12 +3843,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", @@ -2469,6 +4533,24 @@ "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", @@ -2748,6 +4830,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", @@ -2988,6 +5082,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", @@ -3000,6 +5114,15 @@ "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/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -3058,6 +5181,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", @@ -3118,6 +5265,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", @@ -3175,12 +5335,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", @@ -3225,11 +5406,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", @@ -3244,9 +5442,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", @@ -3369,6 +5578,24 @@ "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", @@ -3379,6 +5606,12 @@ "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", @@ -3392,6 +5625,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", @@ -3421,6 +5660,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", @@ -4236,6 +6489,12 @@ "license": "MIT", "peer": true }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4316,6 +6575,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.41.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.41.0.tgz", + "integrity": "sha512-OHAMNiCEON1RDBlRGuulsN5AD8ptMjvk5QWfFmYmBLPZ3zFGIJe60kQucQQf4cez1OzQmjYBWDY+dYfISkUdqg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.41.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", @@ -4420,6 +6706,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", @@ -4508,6 +6803,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", @@ -4521,6 +6822,19 @@ "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/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4632,6 +6946,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", @@ -4642,6 +6982,12 @@ "node": ">= 4" } }, + "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", @@ -4977,6 +7323,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", @@ -5133,7 +7485,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": { @@ -5169,6 +7520,28 @@ "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/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/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5308,6 +7681,37 @@ "node": ">=0.10" } }, + "node_modules/lenis": { + "version": "1.3.24", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.24.tgz", + "integrity": "sha512-UdWz0Bx/Z9dDgRdWe+N1dPmPWXGCF4VOTX3mugyX7lRpdjXG6ACfLI/cVmDOEMeQXf+j5Km/uKrvvf1X0k+u6g==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5322,6 +7726,15 @@ "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", @@ -5650,6 +8063,16 @@ "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", @@ -5680,6 +8103,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", @@ -5729,14 +8167,55 @@ ], "license": "MIT", "peerDependencies": { - "typescript": ">=5.0.4" + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/motion": { + "version": "12.41.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.41.0.tgz", + "integrity": "sha512-avEDKE22rFPJqDr3Ttk7gMQpeaOmNik60NoJ5T0tj+RBCNvz21D3ArY3l4uitoeQ7eIpDqueWaO3pPYFv8JOVA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.41.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": { - "typescript": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { "optional": true } } }, + "node_modules/motion-dom": { + "version": "12.41.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.41.0.tgz", + "integrity": "sha512-Lk3J39fOGg6xNr1KRZsN6usDyBf8aP7MEbUPez1VCughHt79OrP7VGqNrPyFL0riaT7WS8t9DRw1M3BHtM/xKw==", + "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", @@ -5838,6 +8317,16 @@ } } }, + "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", @@ -6162,7 +8651,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" @@ -6239,6 +8727,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", @@ -6249,6 +8743,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", @@ -6292,6 +8796,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", @@ -6329,6 +8910,90 @@ "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", @@ -6388,6 +9053,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", @@ -6443,6 +9117,12 @@ "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", @@ -6704,7 +9384,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" @@ -6717,7 +9396,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" @@ -6806,6 +9484,12 @@ "dev": true, "license": "ISC" }, + "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", @@ -6829,6 +9513,32 @@ "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", @@ -7046,6 +9756,46 @@ "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", @@ -7053,6 +9803,44 @@ "license": "BSD-3-Clause-Clear", "peer": true }, + "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", @@ -7148,6 +9936,36 @@ "node": ">=8.0" } }, + "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", @@ -7193,6 +10011,43 @@ "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7427,6 +10282,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", @@ -7443,6 +10341,15 @@ "license": "MIT", "peer": true }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/viem": { "version": "2.52.2", "resolved": "https://registry.npmjs.org/viem/-/viem-2.52.2.tgz", @@ -7839,11 +10746,21 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -8034,6 +10951,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 f9a896c..052d5c7 100644 --- a/package.json +++ b/package.json @@ -10,24 +10,43 @@ "test": "vitest run" }, "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", "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", + "gsap": "^3.15.0", + "lenis": "^1.3.24", "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", + "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/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index fc4b401..649d6fb 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -12,7 +12,7 @@ import { REGISTRY_ADDRESSES, KNOWN_WRAPPERS } from '@/config/contracts'; * Falls back to the hardcoded snapshot when the on-chain read fails. * * Usage: - * fetch("https://zamavault.xyz/api/registry?chain=sepolia") + * fetch("https://YOUR_DEPLOYMENT_URL/api/registry?chain=sepolia") * .then(r => r.json()) * .then(data => console.log(data.pairs)) */ 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/developers/page.tsx b/src/app/app/developers/page.tsx similarity index 99% rename from src/app/developers/page.tsx rename to src/app/app/developers/page.tsx index d9beac9..c1328e4 100644 --- a/src/app/developers/page.tsx +++ b/src/app/app/developers/page.tsx @@ -1,6 +1,8 @@ 'use client'; import React, { useState, useMemo } from 'react'; + +const APP_URL = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? 'https://YOUR_DEPLOYMENT_URL'; import Card from '@/components/ui/Card'; import Badge from '@/components/ui/Badge'; import Button from '@/components/ui/Button'; @@ -477,7 +479,7 @@ function restApiSnippet(chain: string): string { // Returns all registered wrapper pairs with metadata. const response = await fetch( - 'https://zamavault.xyz/api/registry?chain=${chain}' + '${APP_URL}/api/registry?chain=${chain}' ); const data = await response.json(); @@ -563,7 +565,7 @@ export default function DevelopersPage() { style={{ fontSize: 'var(--text-lg)', maxWidth: 640, - marginTop: 'var(--sp-3)', + margin: 'var(--sp-3) auto 0', lineHeight: 'var(--lh-relaxed)', }} > diff --git a/src/app/docs/page.tsx b/src/app/app/docs/page.tsx similarity index 98% rename from src/app/docs/page.tsx rename to src/app/app/docs/page.tsx index 1d059eb..b5fd9f5 100644 --- a/src/app/docs/page.tsx +++ b/src/app/app/docs/page.tsx @@ -1,6 +1,9 @@ 'use client'; import React, { useState, useEffect, useRef, useCallback } from 'react'; + +// Base URL for API examples in docs — set NEXT_PUBLIC_APP_URL in your deployment. +const APP_URL = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? 'https://YOUR_DEPLOYMENT_URL'; import Link from 'next/link'; import Badge from '@/components/ui/Badge'; import CopyButton from '@/components/ui/CopyButton'; @@ -218,8 +221,8 @@ function AddressTable({ pairs: { symbol: string; erc20: string; wrapper: string; decimals: number }[]; }) { const explorerBase = network === 'Sepolia' - ? 'https://sepolia.etherscan.io/address' - : 'https://etherscan.io/address'; + ? 'https://eth-sepolia.blockscout.com/address' + : 'https://eth.blockscout.com/address'; return (
@@ -591,15 +594,15 @@ function ShieldButton() { lang="bash" filename="curl" code={`# Fetch all Sepolia pairs -curl "https://zamavault.xyz/api/registry?chain=sepolia" +curl "${APP_URL}/api/registry?chain=sepolia" # Fetch Mainnet pairs -curl "https://zamavault.xyz/api/registry?chain=mainnet"`} +curl "${APP_URL}/api/registry?chain=mainnet"`} /> REST API (Sepolia) - + Interactive Tutorial
diff --git a/src/app/faucet/page.tsx b/src/app/app/faucet/page.tsx similarity index 99% rename from src/app/faucet/page.tsx rename to src/app/app/faucet/page.tsx index 7034f33..46823b6 100644 --- a/src/app/faucet/page.tsx +++ b/src/app/app/faucet/page.tsx @@ -302,7 +302,7 @@ export default function FaucetPage() { Transaction pending...
{children}; +} diff --git a/src/app/learn/page.tsx b/src/app/app/learn/page.tsx similarity index 90% rename from src/app/learn/page.tsx rename to src/app/app/learn/page.tsx index 513dbbb..75e0dd0 100644 --- a/src/app/learn/page.tsx +++ b/src/app/app/learn/page.tsx @@ -1,12 +1,13 @@ 'use client'; -import React, { useState } from 'react'; +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, @@ -21,6 +22,7 @@ import { Cpu, ArrowRight, Sparkles, + PartyPopper, } from 'lucide-react'; /* ─── Step definitions ──────────────────────────────────────────────────────── */ @@ -209,10 +211,10 @@ function StepFaucet() {
- -
@@ -224,7 +226,8 @@ function StepShield() { return (

- Shielding (also called "wrapping") converts your public + 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.

@@ -282,10 +285,10 @@ function StepShield() {
- -
@@ -297,8 +300,8 @@ 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 + 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.

@@ -314,8 +317,8 @@ function StepDecrypt() { title="Sign the EIP-712 permit in your wallet" description={ <> - Your wallet will show a typed data signature request. This creates a - temporary session key that the Zama Gateway uses to + 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. } @@ -359,7 +362,8 @@ function StepUnshield() { return (

- Unshielding (also called "unwrapping") converts your + 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.

@@ -417,16 +421,16 @@ function StepUnshield() {
- - - -
@@ -493,7 +497,7 @@ function InstructionStep({ return (
{number}
-
+
{title}
@@ -510,31 +514,58 @@ function InstructionStep({ 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) => { - // Mark current step as completed when navigating forward - if (id > activeStep) { - setCompletedSteps((prev) => new Set([...prev, activeStep])); - } setActiveStep(id); }; const goNext = () => { - if (activeStep < STEPS.length) goTo(activeStep + 1); + if (activeStep < STEPS.length) { + setCompletedSteps((prev) => new Set([...prev, activeStep])); + setActiveStep(activeStep + 1); + } }; const goPrev = () => { if (activeStep > 1) setActiveStep(activeStep - 1); }; - const markComplete = () => { - setCompletedSteps((prev) => new Set([...prev, activeStep])); - if (activeStep < STEPS.length) goTo(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))); }; - const allComplete = completedSteps.size >= STEPS.length; + useEffect(() => { + if (allComplete) fireConfetti(); + }, [allComplete, fireConfetti]); return (
@@ -551,7 +582,7 @@ export default function LearnPage() { style={{ fontSize: 'var(--text-lg)', maxWidth: 600, - marginTop: 'var(--sp-3)', + margin: 'var(--sp-3) auto 0', lineHeight: 'var(--lh-relaxed)', }} > @@ -602,7 +633,7 @@ export default function LearnPage() { }} >
- +

Tutorial Complete! @@ -613,17 +644,17 @@ export default function LearnPage() {

- + - + - + @@ -672,15 +703,6 @@ export default function LearnPage() {
- {!completedSteps.has(activeStep) && ( - - )} {activeStep < STEPS.length ? ( ) : null}
diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx new file mode 100644 index 0000000..643b7f7 --- /dev/null +++ b/src/app/app/page.tsx @@ -0,0 +1,455 @@ +'use client'; + +import React, { useState, useMemo } 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, isMintablePair, type RegistryPairsResult } from '@/lib/registry'; +import { type WrapperPair } from '@/config/contracts'; +import { ERC20_ABI } from '@/lib/wrapper-abi'; +import BlurIn from '@/components/ui/BlurIn'; +import { useAccount, useReadContract } from 'wagmi'; +import { useConfidentialBalance } from '@zama-fhe/react-sdk'; +import { + Search, + Lock, + Unlock, + Shield, + ExternalLink, + Info, + AlertTriangle, + AlertCircle, + Database, + Settings2, +} 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.', + confidentialBadge: 'Balances are encrypted on-chain via FHE. Only you can decrypt them by signing an EIP-712 permit.', + publicBalance: 'Your unencrypted ERC-20 balance, visible to anyone on-chain. Shield it to make it private.', + confidentialBalance: 'Encrypted balance. Click Decrypt to sign a read-only EIP-712 permit — no tokens are spent, your private key stays in your wallet.', + mockBadge: 'Testnet mock token deployed by Zama. Has a public mint() — get free tokens from the Faucet page.', + shield: (sym: string) => `Convert public ${sym} into encrypted c${sym}. Requires ERC-20 approval then the shield transaction.`, + unshield: (sym: string) => `Burn encrypted c${sym} and retrieve public ${sym}. Two-step: on-chain unwrap + Gateway proof finalization.`, + permit: 'Read-only off-chain signature (EIP-712). Authorises Zama Gateway to decrypt your balance for this session. Does not spend tokens or approve contracts.', +}; + +// ─── Per-row component ──────────────────────────────────────────────────────── + +function RegistryTokenRow({ + wrapper, + explorerBase, + isTestnet, +}: { + wrapper: WrapperPair; + explorerBase: string; + isTestnet: boolean; +}) { + const { address, isConnected } = useAccount(); + const [decryptRequested, setDecryptRequested] = useState(false); + + // 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; + + // Confidential balance — only fires after explicit user click + const { + data: confidentialBalance, + isLoading: isDecrypting, + error: decryptError, + refetch: refetchConfidential, + } = useConfidentialBalance( + { tokenAddress: wrapper.erc7984Address }, + { enabled: decryptRequested && isConnected && !!address }, + ); + + const isRevoked = wrapper.isValid === false; + const cleanName = wrapper.name.replace(/\s*\(Mock\)\s*/gi, '').trim(); + const isMock = isMintablePair(wrapper) && isTestnet; + const confidentialSymbol = `c${wrapper.symbol}`; + + const handleDecrypt = () => { + setDecryptRequested(true); + refetchConfidential(); + }; + + return ( + + + {/* ── Token ─────────────────────────────────────────────────────────── */} + +
+ +
+ {/* Name + badges on one line */} +
+ {cleanName} + {isMock && ( +
+ Mock + +
+ )} + {isRevoked && ( + + Revoked + + )} +
+
{wrapper.symbol}
+
+
+ + + {/* ── ERC-20 Address ────────────────────────────────────────────────── */} + +
+ + + {/* ── ERC-7984 Wrapper ──────────────────────────────────────────────── */} + + + + + {/* ── Public Balance ────────────────────────────────────────────────── */} + + {!isConnected ? ( + + ) : publicBalance !== undefined ? ( + + {formatAmount(publicBalance, wrapper.decimals)}{' '} + {wrapper.symbol} + + ) : ( + + )} + + + {/* ── Confidential Balance ──────────────────────────────────────────── */} + + {!isConnected ? ( + + ) : confidentialBalance !== undefined && confidentialBalance !== null ? ( + + {formatAmount(confidentialBalance, wrapper.wrapperDecimals)}{' '} + {confidentialSymbol} + + + ) : isDecrypting ? ( + Awaiting signature… + ) : decryptError ? ( + + ) : ( + + + + + )} + + + {/* ── Actions ───────────────────────────────────────────────────────── */} + + {isRevoked ? ( + + Unavailable + + ) : ( +
+ + + + + + + + + + + +
+ )} + + + ); +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + +export default function HomePage() { + const [searchQuery, setSearchQuery] = useState(''); + const [showRevoked, setShowRevoked] = useState(false); + const { isTestnet, activeChainId } = useActiveNetwork(); + + const { pairs, isLoading, isFromCache, total }: RegistryPairsResult = + useRegistryPairs(activeChainId); + + const visibleWrappers = useMemo( + () => (showRevoked ? pairs : pairs.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'; + + 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 */} +
+ +
Registered Pairs
+
+ {isLoading && pairs.length === 0 ? : total} +
+ {revokedCount > 0 &&
{revokedCount} revoked
} +
+ +
Active Network
+ + {isTestnet ? 'Sepolia Testnet' : 'Ethereum Mainnet'} + +
+ +
FHE Security
+
+ ERC-7984 Standard + +
+
+
+ + {/* Search */} +
+
+ + + + setSearchQuery(e.target.value)} + style={{ paddingLeft: 40 }} + aria-label="Search registered wrapper pairs" + /> +
+ {revokedCount > 0 && ( + + )} +
+ + {/* Table */} +
+ + + + + + + + + + + + + {isLoading && filteredWrappers.length === 0 ? ( + Array.from({ length: 5 }).map((_, i) => ( + + + + )) + ) : filteredWrappers.length === 0 ? ( + + + + ) : ( + filteredWrappers.map(wrapper => ( + + )) + )} + +
TokenERC-20 Address + + ERC-7984 Wrapper + + + + + Public Balance + + + + + Confidential Balance + + + Actions
+
+
+ +
+
+ {searchQuery ? 'No tokens match your search query' : 'No registered wrappers found on this network'} +
+
+
+
+ + {/* 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/portfolio/page.tsx b/src/app/app/portfolio/page.tsx similarity index 63% rename from src/app/portfolio/page.tsx rename to src/app/app/portfolio/page.tsx index 157816a..5162028 100644 --- a/src/app/portfolio/page.tsx +++ b/src/app/app/portfolio/page.tsx @@ -1,21 +1,25 @@ 'use client'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import Link from 'next/link'; 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 Skeleton from '@/components/ui/Skeleton'; 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 { useAccount, useConnect, usePublicClient } from 'wagmi'; import { useConfidentialBalances, useRevokeSession } from '@zama-fhe/react-sdk'; import { useToast } from '@/components/ui/Toast'; import BlurIn from '@/components/ui/BlurIn'; +import { parseAbiItem, formatUnits } from 'viem'; +import { CHAIN_CONFIG } from '@/config/chains'; import { Lock, Unlock, @@ -23,8 +27,17 @@ import { Shield, Wallet, RefreshCw, + Clock, + ArrowUpRight, + ArrowDownLeft, + BarChart2, + ExternalLink, } from 'lucide-react'; +const TRANSFER_ABI = parseAbiItem( + 'event Transfer(address indexed from, address indexed to, uint256 value)', +); + interface TokenPositionProps { wrapper: WrapperPair; isConnected: boolean; @@ -135,7 +148,7 @@ function TokenPositionCard({ variant="secondary" fullWidth size="sm" - onClick={() => (window.location.href = `/wrap?token=${wrapper.symbol}&action=unwrap`)} + onClick={() => (window.location.href = `/app/wrap?token=${wrapper.symbol}&action=unwrap`)} > Unshield @@ -149,6 +162,184 @@ function TokenPositionCard({ ); } +/* ─── Wallet Activity Feed ─────────────────────────────────────────────────── */ + +interface WalletEvent { + type: 'shield' | 'unshield'; + symbol: string; + amount: bigint; + decimals: number; + counterpart: string; + txHash: string; + blockNumber: bigint; +} + +function WalletActivityFeed({ + address, + wrappers, + chainId, +}: { + address: `0x${string}`; + wrappers: WrapperPair[]; + chainId: number; +}) { + const client = usePublicClient({ chainId }); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(false); + const explorerBase = CHAIN_CONFIG[chainId as keyof typeof CHAIN_CONFIG]?.explorerUrl ?? 'https://eth.blockscout.com'; + + const fetchActivity = useCallback(async () => { + if (!client || wrappers.length === 0 || !address) return; + setLoading(true); + try { + const latestBlock = await client.getBlockNumber(); + // Use fromBlock: 0n (genesis) so the feed shows the wallet's complete + // history. Most RPC providers handle address-filtered getLogs from block 0 + // efficiently because the address index keeps the result set small. + // If the provider rejects with a "block range too large" error we fall + // back to the last 500,000 blocks (~69 days on 12s chains) in the catch. + const fromBlock = 0n; + + const allEvents: WalletEvent[] = []; + await Promise.all( + wrappers.filter((p) => p.isValid !== false).map(async (pair) => { + try { + // Shield: ERC-20 Transfer from user to wrapper + const shields = await client.getLogs({ + address: pair.erc20Address, + event: TRANSFER_ABI, + args: { from: address, to: pair.erc7984Address }, + fromBlock, + toBlock: latestBlock, + }); + // Unshield: ERC-20 Transfer from wrapper to user + const unshields = await client.getLogs({ + address: pair.erc20Address, + event: TRANSFER_ABI, + args: { from: pair.erc7984Address, to: address }, + fromBlock, + toBlock: latestBlock, + }); + for (const log of shields) { + allEvents.push({ + type: 'shield', + symbol: pair.symbol, + amount: (log.args?.value as bigint) ?? 0n, + decimals: pair.decimals, + counterpart: pair.erc7984Address, + txHash: log.transactionHash ?? '', + blockNumber: log.blockNumber ?? 0n, + }); + } + for (const log of unshields) { + allEvents.push({ + type: 'unshield', + symbol: pair.symbol, + amount: (log.args?.value as bigint) ?? 0n, + decimals: pair.decimals, + counterpart: pair.erc7984Address, + txHash: log.transactionHash ?? '', + blockNumber: log.blockNumber ?? 0n, + }); + } + } catch { /* skip failed token */ } + }), + ); + allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber)); + setEvents(allEvents.slice(0, 100)); + } catch (err: unknown) { + // Some public RPC nodes reject unlimited block ranges even with an + // address filter. Fall back to last 500 000 blocks (~69 days). + const msg = err instanceof Error ? err.message : String(err); + const isRangeError = /block range|range too large|too many results/i.test(msg); + if (isRangeError) { + try { + const latestBlock = await client!.getBlockNumber(); + const fallbackFrom = latestBlock > 500_000n ? latestBlock - 500_000n : 0n; + const fallbackEvents: WalletEvent[] = []; + await Promise.all( + wrappers.filter((p) => p.isValid !== false).map(async (pair) => { + try { + const [shields, unshields] = await Promise.all([ + client!.getLogs({ address: pair.erc20Address, event: TRANSFER_ABI, args: { from: address, to: pair.erc7984Address }, fromBlock: fallbackFrom, toBlock: latestBlock }), + client!.getLogs({ address: pair.erc20Address, event: TRANSFER_ABI, args: { from: pair.erc7984Address, to: address }, fromBlock: fallbackFrom, toBlock: latestBlock }), + ]); + for (const log of shields) fallbackEvents.push({ type: 'shield', symbol: pair.symbol, amount: (log.args?.value as bigint) ?? 0n, decimals: pair.decimals, counterpart: pair.erc7984Address, txHash: log.transactionHash ?? '', blockNumber: log.blockNumber ?? 0n }); + for (const log of unshields) fallbackEvents.push({ type: 'unshield', symbol: pair.symbol, amount: (log.args?.value as bigint) ?? 0n, decimals: pair.decimals, counterpart: pair.erc7984Address, txHash: log.transactionHash ?? '', blockNumber: log.blockNumber ?? 0n }); + } catch { /* skip */ } + }), + ); + fallbackEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber)); + setEvents(fallbackEvents); + } catch { /* ignore */ } + } + } + finally { setLoading(false); } + }, [client, wrappers, address]); + + useEffect(() => { fetchActivity(); }, [fetchActivity]); + + return ( + +
+

+ + My Recent Activity +

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

+ No shield or unshield events found for this wallet. +

+ ) : ( +
+ {events.map((ev, i) => { + const isShield = ev.type === 'shield'; + const color = isShield ? 'var(--success)' : 'var(--warning)'; + const Icon = isShield ? ArrowUpRight : ArrowDownLeft; + return ( +
+
+ +
+
+
+ {isShield ? 'Shield' : 'Unshield'} + + {formatUnits(ev.amount, ev.decimals)} {ev.symbol} + +
+
+ Wrapper: {formatAddress(ev.counterpart)} +
+
+ + Tx + +
+ ); + })} +
+ )} +
+ ); +} + export default function PortfolioPage() { const { activeChainId } = useActiveNetwork(); const { address, isConnected } = useAccount(); @@ -404,6 +595,15 @@ export default function PortfolioPage() {
)} + {/* Wallet Activity Feed */} + {isConnected && address && wrappers.length > 0 && ( + + )} + {/* Info */}
diff --git a/src/app/wrap/page.tsx b/src/app/app/wrap/page.tsx similarity index 88% rename from src/app/wrap/page.tsx rename to src/app/app/wrap/page.tsx index 54e3818..3cd12b5 100644 --- a/src/app/wrap/page.tsx +++ b/src/app/app/wrap/page.tsx @@ -253,9 +253,12 @@ function WrapPageContent() { if (!selectedWrapper || !address) return; try { if (action === 'wrap') { - setTxStep(1); // Approval confirmation pending + // If allowance is already sufficient, SDK skips approval — + // jump straight to step 3 (Shield pending) for consistent UI. + setTxStep(needsApproval ? 1 : 3); const res = await shield({ amount: parsedInputAmount, + approvalStrategy: needsApproval ? 'exact' : 'skip', onApprovalSubmitted: (txHash) => { setTxStep(2); // Approve mining setActiveTxHash(txHash); @@ -285,20 +288,40 @@ function WrapPageContent() { }); 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(); - refetchWrapperBalance(); refetchAllowance(); } else { setTxStep(3); // Unshield pending const res = await unshield({ amount: parsedInputAmount, onUnwrapSubmitted: (txHash) => { - setTxStep(4); // Unshield mining + setTxStep(4); // Unwrap on-chain, waiting for proof setActiveTxHash(txHash); addToast({ variant: 'info', - title: 'Unshielding Submitted', - message: 'Unshield transaction sent. Waiting for confirmation...', + 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...', }); }, }); @@ -312,8 +335,9 @@ function WrapPageContent() { }); setTxStep(5); // Completed setIsSuccessModalOpen(true); + // Reset decrypt gate — same reason as wrap path above. + setDecryptRequested(false); refetchPublicBalance(); - refetchWrapperBalance(); } } catch (err: unknown) { console.error(err); @@ -352,19 +376,26 @@ function WrapPageContent() {

- {/* Pending unshield banner for the currently selected token */} - {selectedWrapper && ( -
- + {/* 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 */}
@@ -430,7 +461,7 @@ function WrapPageContent() { {wrappers.map(w => ( ))} @@ -559,7 +590,7 @@ function WrapPageContent() { {txStep > 0 && (
- {action === 'wrap' && ( + {action === 'wrap' && needsApproval && ( <>
= 2 ? 'completed' : txStep === 1 ? 'active' : ''}`}>
{txStep >= 2 ? : '1'}
@@ -569,89 +600,32 @@ function WrapPageContent() { )}
= 4 ? 'completed' : txStep === 3 ? 'active' : ''}`}> -
{txStep >= 4 ? : action === 'wrap' ? '2' : '1'}
- {action === 'wrap' ? 'Shield' : 'Unshield'} +
+ {txStep >= 4 ? : (action === 'wrap' && needsApproval) ? '2' : '1'} +
+ {action === 'wrap' ? 'Shield' : 'Unwrap'}
+ {action === 'unwrap' && ( + <> +
= 4 ? 'active' : ''}`}> +
{txStep === 5 ? : '2'}
+ Finalize +
+
+ + )}
-
{txStep === 5 ? : action === 'wrap' ? '3' : '2'}
+
+ {txStep === 5 ? : action === 'unwrap' ? '3' : (needsApproval ? '3' : '2')} +
Done
)} - {/* Transaction Link */} - {(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 - -
- - {/* Coprocessor / Gateway Delay Explanation */} - {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. - -
- )} -
- )} - - {/* Submit Button */} + {/* Primary Action Button — ALWAYS in this slot */}
{!isConnected ? ( )}
+ + {/* 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 */} diff --git a/src/app/error.tsx b/src/app/error.tsx index c3be582..55e0297 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -66,7 +66,7 @@ export default function ErrorBoundary({ Try again */ + --font-display: var(--font-fraunces, 'Fraunces'), Georgia, serif; + --font-sans: var(--font-jakarta, 'Plus Jakarta Sans'), system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + + /* shadcn/MagicCard compat — defaults to light; overridden by .section-dark */ + --color-background: #f8f5f0; + --color-border: rgba(15, 14, 12, 0.10); + + /* Motion */ + --animate-aurora: aurora 20s ease-in-out infinite alternate; + --animate-marquee: marquee var(--duration, 30s) linear infinite; + --animate-marquee-vertical: marquee-vertical var(--duration, 30s) linear infinite; + --animate-shimmer-slide: shimmer-slide var(--speed, 2s) ease-in-out infinite alternate; + --animate-spin-around: spin-around calc(var(--speed, 2s) * 2) infinite linear; + --animate-shine: shine var(--duration, 8s) ease-in-out infinite; + --animate-grid: grid 15s linear infinite; + --animate-border-beam: border-beam calc(var(--duration, 8s) * 1s) infinite linear; + --animate-blink-cursor: blink-cursor 1.2s step-end infinite; +} + +/* --------------------------------------------------------------------------- + * Keyframes + * -------------------------------------------------------------------------- */ +@keyframes aurora { + 0% { background-position: 0% 50%; transform: rotate(-5deg) scale(0.9); } + 25% { background-position: 50% 100%; transform: rotate(5deg) scale(1.1); } + 50% { background-position: 100% 50%; transform: rotate(-3deg) scale(0.95); } + 75% { background-position: 50% 0%; transform: rotate(3deg) scale(1.05); } + 100% { background-position: 0% 50%; transform: rotate(-5deg) scale(0.9); } +} +@keyframes marquee { + from { transform: translateX(0); } + to { transform: translateX(calc(-100% - var(--gap, 1rem))); } +} +@keyframes marquee-vertical { + from { transform: translateY(0); } + to { transform: translateY(calc(-100% - var(--gap, 1rem))); } +} +@keyframes shimmer-slide { + to { transform: translate(calc(100cqw - 100%), 0); } +} +@keyframes spin-around { + 0% { transform: translateZ(0) rotate(0); } + 15%, 35% { transform: translateZ(0) rotate(90deg); } + 65%, 85% { transform: translateZ(0) rotate(270deg); } + 100% { transform: translateZ(0) rotate(360deg); } +} +@keyframes shine { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 100%; } + 100% { background-position: 0% 0%; } +} +@keyframes grid { + 0% { transform: translateY(-50%); } + 100% { transform: translateY(0); } +} +@keyframes border-beam { + 100% { offset-distance: 100%; } +} +@keyframes blink-cursor { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} + +/* --------------------------------------------------------------------------- + * Landing root — light cream default. Never bleeds outside .landing-root. + * -------------------------------------------------------------------------- */ +.landing-root { + font-family: var(--font-sans); + background: var(--color-cream-100); + color: var(--color-ink-900); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 100vh; +} + +.landing-root ::selection { + background: var(--color-gold-500); + color: var(--color-ink-950); +} + +.landing-root h1, +.landing-root h2, +.landing-root h3 { + font-family: var(--font-display); + letter-spacing: -0.02em; +} + +/* Dark variant — used on FHE explainer, final CTA, footer */ +.section-dark { + --color-background: #050504; + --color-border: rgba(255, 255, 255, 0.08); +} + +/* Grain overlay for hero only */ +.landing-grain::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + opacity: 0.03; + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml;utf8,"); +} + +/* Gold rule divider */ +.landing-rule { + height: 1px; + background: linear-gradient( + to right, + transparent 0%, + var(--color-gold-500) 50%, + transparent 100% + ); + opacity: 0.4; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7694c1a..b993e2b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,36 +1,41 @@ -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: 'ZamaVault — 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: 'ZamaVault — 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 ( - +