From ab7b47c7e513c283baf7f834ed4f287207623fce Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 12:00:05 +0300 Subject: [PATCH 01/18] fix: Blockscout explorer, dynamic APP_URL, env.example update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all Etherscan links with Blockscout (eth-sepolia.blockscout.com / eth.blockscout.com) — Blockscout supports Zama FHE protocol decoding - Replace hardcoded zamavault.xyz in docs/developers/API with NEXT_PUBLIC_APP_URL env var (falls back to placeholder until deployed) - Add NEXT_PUBLIC_APP_URL to .env.example with documentation - chains.ts: update explorerUrl for both Sepolia and Mainnet --- .env.example | 5 +++++ src/app/api/registry/route.ts | 2 +- src/app/developers/page.tsx | 4 +++- src/app/docs/page.tsx | 15 +++++++++------ src/app/faucet/page.tsx | 2 +- src/app/page.tsx | 2 +- src/config/chains.ts | 7 ++++--- 7 files changed, 24 insertions(+), 13 deletions(-) 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/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/developers/page.tsx b/src/app/developers/page.tsx index d9beac9..b16a433 100644 --- a/src/app/developers/page.tsx +++ b/src/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(); diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 1d059eb..6d19eea 100644 --- a/src/app/docs/page.tsx +++ b/src/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"`} /> diff --git a/src/config/chains.ts b/src/config/chains.ts index dc26c25..4989234 100644 --- a/src/config/chains.ts +++ b/src/config/chains.ts @@ -9,6 +9,7 @@ export const DEFAULT_CHAIN = sepolia; export const CHAIN_CONFIG: Record Date: Tue, 23 Jun 2026 12:14:57 +0300 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20remove=20refetchWrapperBalance()?= =?UTF-8?q?=20from=20success=20handlers=20=E2=80=94=20critical=20permit=20?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TanStack Query's refetch() bypasses the enabled flag unconditionally. After a successful shield or unshield, calling refetchWrapperBalance() was silently triggering useConfidentialBalance even though decryptRequested=false, causing the EIP-712 permit wallet prompt to auto-fire without user consent (and retry up to 3x before surfacing the rejection error). Fix: remove refetchWrapperBalance() from both success paths in handleAction. The confidential balance is only refreshed when the user explicitly clicks the "Decrypt" button (which sets decryptRequested=true before calling refetch). --- src/app/wrap/page.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/wrap/page.tsx b/src/app/wrap/page.tsx index 54e3818..ced5ec9 100644 --- a/src/app/wrap/page.tsx +++ b/src/app/wrap/page.tsx @@ -286,8 +286,11 @@ function WrapPageContent() { setTxStep(5); // Completed setIsSuccessModalOpen(true); refetchPublicBalance(); - refetchWrapperBalance(); refetchAllowance(); + // NOTE: do NOT call refetchWrapperBalance() here — that would + // bypass the enabled:decryptRequested gate and auto-fire an + // EIP-712 permit without user consent. The user must click + // "Decrypt" again to see the updated confidential balance. } else { setTxStep(3); // Unshield pending const res = await unshield({ @@ -313,7 +316,7 @@ function WrapPageContent() { setTxStep(5); // Completed setIsSuccessModalOpen(true); refetchPublicBalance(); - refetchWrapperBalance(); + // NOTE: do NOT call refetchWrapperBalance() — same reason as above. } } catch (err: unknown) { console.error(err); From cdd03e948eeddff2361b89e7e61aad3d971e8ee6 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 12:33:07 +0300 Subject: [PATCH 03/18] fix: reset decryptRequested after successful shield/unshield After a successful wrap/unwrap, setDecryptRequested(false) prevents TanStack Query's refetchOnWindowFocus from auto-firing the EIP-712 permit (which caused the wallet to prompt up to 3 times for a permit the user had not explicitly requested). The user must click Decrypt again to see the refreshed confidential balance after a transaction. --- src/app/wrap/page.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/wrap/page.tsx b/src/app/wrap/page.tsx index ced5ec9..16f6da8 100644 --- a/src/app/wrap/page.tsx +++ b/src/app/wrap/page.tsx @@ -285,12 +285,13 @@ 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(); refetchAllowance(); - // NOTE: do NOT call refetchWrapperBalance() here — that would - // bypass the enabled:decryptRequested gate and auto-fire an - // EIP-712 permit without user consent. The user must click - // "Decrypt" again to see the updated confidential balance. } else { setTxStep(3); // Unshield pending const res = await unshield({ @@ -315,8 +316,9 @@ function WrapPageContent() { }); setTxStep(5); // Completed setIsSuccessModalOpen(true); + // Reset decrypt gate — same reason as wrap path above. + setDecryptRequested(false); refetchPublicBalance(); - // NOTE: do NOT call refetchWrapperBalance() — same reason as above. } } catch (err: unknown) { console.error(err); From 3cef61c353753ff353537e82c6492a989aad28f6 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 12:42:09 +0300 Subject: [PATCH 04/18] feat: analytics dashboard, portfolio activity feed, mobile CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3.1 — /analytics page: - TVL per token via balanceOf on underlying ERC-20 held by wrapper - Shield/Unshield event counts from Transfer logs (last 5000 blocks) - Recent activity feed sorted by block number - 4 stat cards: active pairs, shields, unshields, unique shielders - Refresh button, Blockscout links, loading skeletons Phase 3.2 — Portfolio activity feed: - WalletActivityFeed component at bottom of portfolio - Shows recent shield/unshield events for connected wallet (last 10000 blocks) - Links to Analytics page Phase 4.4 — Mobile responsive CSS: - Header nav hidden on mobile, container padding reduced - Grid-2 collapses to 1 column at 768px - Swap panel, modal, typography scale for 360px–480px - Logo text hidden at 480px to save space - Network switcher compact on small screens --- src/app/analytics/page.tsx | 543 +++++++++++++++++++++++++++++++ src/app/globals.css | 204 ++++++++++++ src/app/portfolio/page.tsx | 173 +++++++++- src/components/layout/Header.tsx | 1 + 4 files changed, 919 insertions(+), 2 deletions(-) create mode 100644 src/app/analytics/page.tsx diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx new file mode 100644 index 0000000..f355949 --- /dev/null +++ b/src/app/analytics/page.tsx @@ -0,0 +1,543 @@ +'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 { formatAmount, formatAddress } from '@/lib/utils'; +import { CHAIN_CONFIG } from '@/config/chains'; +import { + BarChart2, + TrendingUp, + Shield, + Unlock, + Users, + RefreshCw, + ExternalLink, + Clock, + ArrowUpRight, + ArrowDownLeft, +} from 'lucide-react'; + +/* ─── Types ──────────────────────────────────────────────────────────────────── */ + +interface TokenTVL { + symbol: string; + tvlRaw: bigint; + tvlFormatted: string; + decimals: number; + erc20Address: string; + wrapperAddress: string; + shieldCount: number; + unshieldCount: number; +} + +interface ActivityEvent { + type: 'shield' | 'unshield'; + symbol: string; + amount: bigint; + decimals: number; + from: string; + to: string; + txHash: string; + blockNumber: bigint; +} + +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; + +/* ─── 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, + maxTvl, + explorerBase, +}: { + token: TokenTVL; + maxTvl: bigint; + explorerBase: string; +}) { + const pct = maxTvl > 0n + ? Number((token.tvlRaw * 10000n) / maxTvl) / 100 + : 0; + + return ( +
+
+ +
+
{token.symbol}
+
c{token.symbol}
+
+
+ +
+
+
+ +
+
+ {token.tvlFormatted} +
+
+ {token.shieldCount}↑ {token.unshieldCount}↓ +
+
+ +
+ + +
+ ); +} + +/* ─── 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 amount = formatUnits(event.amount, event.decimals); + + return ( +
+
+ +
+ +
+
+ + {isShield ? 'Shield' : 'Unshield'} + + + {amount} {event.symbol} + +
+
+ {isShield ? 'from' : 'to'}{' '} + {formatAddress(isShield ? event.from : event.to)} +
+
+ + + Tx + +
+ ); +} + +/* ─── Main page ──────────────────────────────────────────────────────────────── */ + +const BLOCK_LOOKBACK = 5000n; // ~17 hours on Sepolia (12s blocks) + +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 fromBlock = latestBlock > BLOCK_LOOKBACK + ? latestBlock - BLOCK_LOOKBACK + : 0n; + + // Fetch all data in parallel per token + const tokenResults = await Promise.all( + pairs + .filter((p) => p.isValid !== false) + .map(async (pair) => { + try { + // TVL: underlying ERC-20 balance held by the wrapper + const tvlRaw = await client.readContract({ + address: pair.erc20Address, + abi: ERC20_BALANCE_ABI, + functionName: 'balanceOf', + args: [pair.erc7984Address], + }) as bigint; + + // Shield events: Transfer(user → wrapper) + const shieldLogs = await client.getLogs({ + address: pair.erc20Address, + event: TRANSFER_ABI, + args: { to: pair.erc7984Address }, + fromBlock, + toBlock: latestBlock, + }); + + // Unshield events: Transfer(wrapper → user) + const unshieldLogs = await client.getLogs({ + address: pair.erc20Address, + event: TRANSFER_ABI, + args: { from: pair.erc7984Address }, + fromBlock, + toBlock: latestBlock, + }); + + const tokenTvl: TokenTVL = { + symbol: pair.symbol, + tvlRaw, + tvlFormatted: formatAmount(tvlRaw, pair.decimals), + decimals: pair.decimals, + erc20Address: pair.erc20Address, + wrapperAddress: pair.erc7984Address, + shieldCount: shieldLogs.length, + unshieldCount: unshieldLogs.length, + }; + + // Build activity events + const shieldEvents: ActivityEvent[] = shieldLogs.slice(-10).map((log) => ({ + type: 'shield' as const, + 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, + })); + + const unshieldEvents: ActivityEvent[] = unshieldLogs.slice(-10).map((log) => ({ + type: 'unshield' as const, + 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, + })); + + return { tokenTvl, events: [...shieldEvents, ...unshieldEvents] }; + } catch { + // If one token fails (e.g. no getLogs support), skip gracefully + 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, 30); + + setTvlData(allTvl.sort((a, b) => (b.tvlRaw > a.tvlRaw ? 1 : -1))); + 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 maxTvl = tvlData.reduce((m, t) => (t.tvlRaw > m ? t.tvlRaw : m), 0n); + const activePairs = tvlData.filter((t) => t.tvlRaw > 0n).length; + + return ( +
+ {/* ── Header ── */} +
+ + + {isTestnet ? 'Sepolia' : 'Mainnet'} · Live + +

+ +

+

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

+
+ + {/* ── Controls ── */} +
+
+ + {lastUpdated + ? `Updated ${lastUpdated.toLocaleTimeString()}` + : 'Loading…'} +  · Last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks (~17h) +
+ +
+ + {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 17h)" + value={isLoading && tvlData.length === 0 ? '…' : totalShields.toString()} + color="var(--success)" + loading={isLoading && tvlData.length === 0} + /> + } + label="Unshields (last 17h)" + value={isLoading && tvlData.length === 0 ? '…' : totalUnshields.toString()} + color="var(--warning)" + loading={isLoading && tvlData.length === 0} + /> + } + label="Unique Shielders" + value={isLoading && tvlData.length === 0 ? '…' : uniqueShielders.toString()} + sub="distinct addresses" + color="#a78bfa" + loading={isLoading && tvlData.length === 0} + /> +
+ + {/* ── Main grid: TVL + Activity ── */} +
+ {/* TVL by token */} + +

+ + TVL by Token +

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

+ No data yet — connect wallet to load registry pairs. +

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

+ TVL = underlying ERC-20 balance held by each wrapper contract. + Arrows show shield↑ / unshield↓ counts for the period. +

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

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

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

+ No shield or unshield events found in the last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks. +

+ ) : ( +
+ {activity.map((event, i) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index efaa45c..bc64654 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1363,6 +1363,105 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } } } +/* ========================================================================== + ANALYTICS PAGE + ========================================================================== */ + +.analytics-stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--sp-4); + margin-bottom: var(--sp-8); +} + +.analytics-main-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-6); + margin-bottom: var(--sp-8); +} + +/* ── TVL bar ── */ +.analytics-tvl-row { + display: flex; + align-items: center; + gap: var(--sp-3); +} + +.analytics-tvl-info { + display: flex; + align-items: center; + gap: var(--sp-3); + width: 80px; + flex-shrink: 0; +} + +.analytics-tvl-bar-wrap { + flex: 1; + height: 8px; + background: var(--bg-elevated); + border-radius: var(--radius-full); + overflow: hidden; +} + +.analytics-tvl-bar-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent) 0%, color-mix(in srgb, var(--accent) 60%, transparent) 100%); + border-radius: var(--radius-full); + transition: width 0.6s var(--ease); + min-width: 4px; +} + +.analytics-tvl-stats { + width: 80px; + flex-shrink: 0; +} + +/* ── Activity row ── */ +.analytics-activity-row { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3); + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--bg-surface); + transition: border-color var(--t-fast); +} + +.analytics-activity-row:hover { + border-color: var(--border-hover); +} + +/* ── Responsive ── */ +@media (max-width: 1024px) { + .analytics-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .analytics-stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: var(--sp-3); + } + .analytics-main-grid { + grid-template-columns: 1fr; + } + .analytics-tvl-info { + width: 60px; + } + .analytics-tvl-stats { + width: 60px; + } +} + +@media (max-width: 480px) { + .analytics-stats-grid { + grid-template-columns: 1fr 1fr; + } +} + /* ========================================================================== DOCS PAGE — Developer Documentation ========================================================================== */ @@ -1929,3 +2028,108 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } padding: var(--sp-2) var(--sp-3); } } + +/* ========================================================================== + MOBILE RESPONSIVE — Global fixes for 360px–768px + ========================================================================== */ + +@media (max-width: 768px) { + /* Header: hide nav labels, collapse to icon-only on very small screens */ + .header-inner { + padding: 0 var(--sp-3); + gap: var(--sp-2); + } + + .header-nav { + display: none; /* hidden on mobile — user scrolls or uses links */ + } + + .header-actions { + gap: var(--sp-2); + } + + /* Container padding */ + .container { + padding: 0 var(--sp-3); + } + + /* Page headers */ + .page-header { + padding: var(--sp-6) 0 var(--sp-4); + } + + h1 { font-size: var(--text-3xl); } + h2 { font-size: var(--text-2xl); } + + /* Registry table: horizontal scroll */ + .registry-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + /* Portfolio grid: single column */ + .grid-2 { + grid-template-columns: 1fr !important; + } + + /* Swap card */ + .swap-panel { + padding: var(--sp-4); + } + + /* Modal */ + .modal-content { + max-width: calc(100vw - 32px); + margin: 0 var(--sp-4); + } + + /* Steps (wrap flow) */ + .steps { + gap: var(--sp-2); + flex-wrap: wrap; + } +} + +@media (max-width: 480px) { + .header-logo span { display: none; } /* hide text, keep logo icon */ + .header-logo svg { margin: 0; } + + /* Network switcher: compact */ + .network-switcher { + gap: 2px; + } + + .network-option { + padding: 4px 8px !important; + font-size: 11px; + } + + /* Theme palette button: hide label */ + .theme-selector-dropdown .btn span:not(:first-child) { + display: none; + } + + /* Typography */ + h1 { font-size: var(--text-2xl); } + + /* Stat cards: 1 column */ + .analytics-stats-grid { + grid-template-columns: 1fr 1fr; + } + + /* Faucet input */ + .faucet-amount-row { + flex-direction: column; + } + + /* Docs content */ + .docs-content { + padding: var(--sp-4) var(--sp-3); + } + + /* Dev page code block */ + .dev-code-pre { + font-size: 11px; + padding: var(--sp-3); + } +} diff --git a/src/app/portfolio/page.tsx b/src/app/portfolio/page.tsx index 157816a..9f10bfa 100644 --- a/src/app/portfolio/page.tsx +++ b/src/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; @@ -149,6 +162,153 @@ 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(); + const fromBlock = latestBlock > 10000n ? latestBlock - 10000n : 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, 20)); + } 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 in the last ~34 hours 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 +564,15 @@ export default function PortfolioPage() {
)} + {/* Wallet Activity Feed */} + {isConnected && address && wrappers.length > 0 && ( + + )} + {/* Info */}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index afea17f..c9131b5 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -28,6 +28,7 @@ const NAV_ITEMS = [ { href: '/faucet', label: 'Faucet' }, { href: '/learn', label: 'Learn' }, { href: '/developers', label: 'Dev Tools' }, + { href: '/analytics', label: 'Analytics' }, { href: '/docs', label: 'Docs' }, ]; From a64bfa91e6dea4972f1be12dd1b9e1d135d21d30 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 13:17:53 +0300 Subject: [PATCH 05/18] fix(analytics): compact TVL numbers, timestamps, ratio card, volume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - formatTVLCompact(): 22976602.1114 → 23.00M (fixes display overflow) - estimateTimeAgo(): block-based time estimate without extra RPC calls (latestBlock - eventBlock) × 12s → '2m ago', '3h ago', etc. - Activity: show up to 25 events (max 8 per token), add timestamp to each row - Add amount localeString formatting (commas for thousands) - New: Shield vs Unshield ratio bar card - New: Most Active Token card (by tx count) - New: shieldVolume / unshieldVolume tracking (period volume in TVL bar) - insights-grid 2-col responsive layout --- src/app/analytics/page.tsx | 272 ++++++++++++++++++++++++++++--------- src/app/globals.css | 10 ++ 2 files changed, 221 insertions(+), 61 deletions(-) diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx index f355949..0bebf0e 100644 --- a/src/app/analytics/page.tsx +++ b/src/app/analytics/page.tsx @@ -11,8 +11,7 @@ import TokenIcon from '@/components/ui/TokenIcon'; import BlurIn from '@/components/ui/BlurIn'; import { useActiveNetwork } from '@/app/ClientLayout'; import { useRegistryPairs } from '@/lib/registry'; -import { formatAmount, formatAddress } from '@/lib/utils'; -import { CHAIN_CONFIG } from '@/config/chains'; +import { formatAddress } from '@/lib/utils'; import { BarChart2, TrendingUp, @@ -24,6 +23,8 @@ import { Clock, ArrowUpRight, ArrowDownLeft, + Scale, + Zap, } from 'lucide-react'; /* ─── Types ──────────────────────────────────────────────────────────────────── */ @@ -31,12 +32,14 @@ import { interface TokenTVL { symbol: string; tvlRaw: bigint; - tvlFormatted: string; + tvlCompact: string; // "23.0M", "5.1K", "412" decimals: number; erc20Address: string; wrapperAddress: string; shieldCount: number; unshieldCount: number; + shieldVolume: bigint; // total amount shielded (underlying decimals) + unshieldVolume: bigint; // total amount unshielded } interface ActivityEvent { @@ -48,6 +51,7 @@ interface ActivityEvent { to: string; txHash: string; blockNumber: bigint; + timeAgo: string; // pre-computed } const TRANSFER_ABI = parseAbiItem( @@ -64,6 +68,44 @@ const ERC20_BALANCE_ABI = [ }, ] 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, @@ -127,9 +169,11 @@ function TVLBar({ explorerBase: string; }) { const pct = maxTvl > 0n - ? Number((token.tvlRaw * 10000n) / maxTvl) / 100 + ? Math.max(Number((token.tvlRaw * 10000n) / maxTvl) / 100, 0.5) : 0; + const volCompact = formatTVLCompact(token.shieldVolume, token.decimals); + return (
@@ -141,18 +185,21 @@ function TVLBar({
-
+
- {token.tvlFormatted} + {token.tvlCompact}
- {token.shieldCount}↑ {token.unshieldCount}↓ + {token.shieldCount}↑{' '} + {token.unshieldCount}↓ + {token.shieldVolume > 0n && ( + + · vol {volCompact} + + )}
@@ -180,7 +227,9 @@ function ActivityRow({ const isShield = event.type === 'shield'; const color = isShield ? 'var(--success)' : 'var(--warning)'; const Icon = isShield ? ArrowUpRight : ArrowDownLeft; - const amount = formatUnits(event.amount, event.decimals); + const amountStr = Number(formatUnits(event.amount, event.decimals)).toLocaleString(undefined, { + maximumFractionDigits: 4, + }); return (
@@ -206,12 +255,17 @@ function ActivityRow({ {isShield ? 'Shield' : 'Unshield'} - {amount} {event.symbol} + {amountStr} {event.symbol}
-
- {isShield ? 'from' : 'to'}{' '} - {formatAddress(isShield ? event.from : event.to)} +
+ + {isShield ? 'from' : 'to'}{' '} + {formatAddress(isShield ? event.from : event.to)} + + + {event.timeAgo} +
@@ -221,6 +275,7 @@ function ActivityRow({ rel="noopener noreferrer" className="text-xs" style={{ color: 'var(--accent)', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 4 }} + title="View on Blockscout" > Tx @@ -228,9 +283,43 @@ function ActivityRow({ ); } +/* ─── 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_LOOKBACK = 5000n; // ~17 hours on Sepolia (12s blocks) +const BLOCK_TIME_MS = 12_000; // ~12 seconds per block export default function AnalyticsPage() { const { activeChainId, isTestnet } = useActiveNetwork(); @@ -253,17 +342,17 @@ export default function AnalyticsPage() { try { const latestBlock = await client.getBlockNumber(); + const fetchTimestamp = Date.now(); const fromBlock = latestBlock > BLOCK_LOOKBACK ? latestBlock - BLOCK_LOOKBACK : 0n; - // Fetch all data in parallel per token const tokenResults = await Promise.all( pairs .filter((p) => p.isValid !== false) .map(async (pair) => { try { - // TVL: underlying ERC-20 balance held by the wrapper + // TVL const tvlRaw = await client.readContract({ address: pair.erc20Address, abi: ERC20_BALANCE_ABI, @@ -271,7 +360,7 @@ export default function AnalyticsPage() { args: [pair.erc7984Address], }) as bigint; - // Shield events: Transfer(user → wrapper) + // Shield events const shieldLogs = await client.getLogs({ address: pair.erc20Address, event: TRANSFER_ABI, @@ -280,7 +369,7 @@ export default function AnalyticsPage() { toBlock: latestBlock, }); - // Unshield events: Transfer(wrapper → user) + // Unshield events const unshieldLogs = await client.getLogs({ address: pair.erc20Address, event: TRANSFER_ABI, @@ -289,43 +378,59 @@ export default function AnalyticsPage() { 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, + ); + const tokenTvl: TokenTVL = { symbol: pair.symbol, tvlRaw, - tvlFormatted: formatAmount(tvlRaw, pair.decimals), + tvlCompact: formatTVLCompact(tvlRaw, pair.decimals), decimals: pair.decimals, erc20Address: pair.erc20Address, wrapperAddress: pair.erc7984Address, shieldCount: shieldLogs.length, unshieldCount: unshieldLogs.length, + shieldVolume, + unshieldVolume, }; - // Build activity events - const shieldEvents: ActivityEvent[] = shieldLogs.slice(-10).map((log) => ({ - type: 'shield' as const, - 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, - })); - - const unshieldEvents: ActivityEvent[] = unshieldLogs.slice(-10).map((log) => ({ - type: 'unshield' as const, - 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, - })); - - return { tokenTvl, events: [...shieldEvents, ...unshieldEvents] }; + // 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 { - // If one token fails (e.g. no getLogs support), skip gracefully return null; } }), @@ -337,7 +442,7 @@ export default function AnalyticsPage() { .flatMap((r) => r.events) .filter((e) => e.txHash && e.amount > 0n) .sort((a, b) => Number(b.blockNumber - a.blockNumber)) - .slice(0, 30); + .slice(0, 25); setTvlData(allTvl.sort((a, b) => (b.tvlRaw > a.tvlRaw ? 1 : -1))); setActivity(allEvents); @@ -363,6 +468,11 @@ export default function AnalyticsPage() { const maxTvl = tvlData.reduce((m, t) => (t.tvlRaw > m ? t.tvlRaw : m), 0n); 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 ── */} @@ -376,7 +486,7 @@ export default function AnalyticsPage() {

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

@@ -397,6 +507,7 @@ export default function AnalyticsPage() { ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading…'}  · Last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks (~17h) +  · Showing up to 25 recent events
+ {/* ── 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 ·{' '} + TVL {mostActive.tvlCompact} +
+
+ + #{tvlData.indexOf(mostActive) + 1} TVL rank + +
+ ) : ( + No data + )} +
+
+ )} + {/* ── Main grid: TVL + Activity ── */}
{/* TVL by token */} @@ -472,13 +622,11 @@ export default function AnalyticsPage() { {isLoading && tvlData.length === 0 ? (
- {[1, 2, 3, 4].map((i) => ( - - ))} + {[1, 2, 3, 4].map((i) => )}
) : tvlData.length === 0 ? (

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

) : (
@@ -489,8 +637,9 @@ export default function AnalyticsPage() { )}

- TVL = underlying ERC-20 balance held by each wrapper contract. - Arrows show shield↑ / unshield↓ counts for the period. + TVL = underlying ERC-20 held by wrapper. Bar = relative share. + shield count ·{' '} + unshield count · vol = period volume

@@ -500,7 +649,7 @@ export default function AnalyticsPage() { style={{ fontWeight: 700, fontSize: 'var(--text-lg)', - marginBottom: 'var(--sp-5)', + marginBottom: 'var(--sp-2)', display: 'flex', alignItems: 'center', gap: 8, @@ -514,16 +663,17 @@ export default function AnalyticsPage() { )} +

+ Latest shield & unshield events across all tokens · last ~17h · up to 25 shown +

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

- No shield or unshield events found in the last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks. + No shield/unshield events in the last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks.

) : (
diff --git a/src/app/globals.css b/src/app/globals.css index bc64654..f51b3d5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1374,6 +1374,13 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } margin-bottom: var(--sp-8); } +.analytics-insights-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-4); + margin-bottom: var(--sp-6); +} + .analytics-main-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -1445,6 +1452,9 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } grid-template-columns: repeat(2, 1fr); gap: var(--sp-3); } + .analytics-insights-grid { + grid-template-columns: 1fr; + } .analytics-main-grid { grid-template-columns: 1fr; } From 7a6e6f6415e22e00ed2c1956504ef01f90c019c8 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 13:44:34 +0300 Subject: [PATCH 06/18] fix(wrap): smart step indicator, onFinalizing callback for unshield MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shield: skip Approve step in UI when allowance is already sufficient (SDK auto-skips it; now UI matches by starting at step 3) - Unshield: add onFinalizing callback — shows toast when Zama Gateway is generating the decryption proof (15-40s wait phase) - Unshield: add onFinalizeSubmitted callback — shows finalization tx - Step indicator: Approve step only shown when needsApproval is true - Step indicator: Unshield now shows 3 steps (Unwrap → Finalize → Done) instead of 2, matching the actual 2-phase protocol flow --- src/app/wrap/page.tsx | 47 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/app/wrap/page.tsx b/src/app/wrap/page.tsx index 16f6da8..8bb9021 100644 --- a/src/app/wrap/page.tsx +++ b/src/app/wrap/page.tsx @@ -253,7 +253,9 @@ 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, onApprovalSubmitted: (txHash) => { @@ -297,12 +299,28 @@ function WrapPageContent() { 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...', }); }, }); @@ -564,7 +582,7 @@ function WrapPageContent() { {txStep > 0 && (
- {action === 'wrap' && ( + {action === 'wrap' && needsApproval && ( <>
= 2 ? 'completed' : txStep === 1 ? 'active' : ''}`}>
{txStep >= 2 ? : '1'}
@@ -574,12 +592,25 @@ 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
From 157169843303a039b4d6e5e62f3191ae7962c590 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 14:00:57 +0300 Subject: [PATCH 07/18] fix: badge uppercase removed, token names show c-prefix correctly Root cause: .badge CSS class had text-transform:uppercase which turned "cZAMA" into "CZAMA", "cUSDC" into "CUSDC" everywhere badges were used. - Remove text-transform:uppercase from .badge class - Token selector in wrap page: shows "cBRON", "cZAMA" etc when in unwrap mode (From = Confidential), plain "BRON", "ZAMA" in wrap mode - Verified all symbol conventions: - Public ERC-20: ZAMA, USDC, WETH, BRON, etc. - Confidential ERC-7984: cZAMA, cUSDC, cWETH, cBRON, etc. - c is always lowercase per Zama convention --- src/app/globals.css | 3 +-- src/app/wrap/page.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index f51b3d5..9b5da8c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -493,8 +493,7 @@ h4 { font-size: var(--text-xl); } font-weight: 600; padding: 3px 10px; border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.02em; } .badge-default { background: var(--bg-elevated); color: var(--text-secondary); border: 1px solid var(--border); } diff --git a/src/app/wrap/page.tsx b/src/app/wrap/page.tsx index 8bb9021..dd52868 100644 --- a/src/app/wrap/page.tsx +++ b/src/app/wrap/page.tsx @@ -453,7 +453,7 @@ function WrapPageContent() { {wrappers.map(w => ( ))} From 646e38479e3bd2309b6ec0bc1c89bfa278713b50 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Tue, 23 Jun 2026 14:31:42 +0300 Subject: [PATCH 08/18] fix: TVL ranking, 24h period, tooltip text/triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit analytics: - Fix TVL sort: was comparing raw bigints (wrong — ZAMA 18-dec raw >> USDC 6-dec raw for equal human values). Now sorts by tvlHuman (float normalized by decimals). USDC 22.89M now correctly ranks above tGBP 3.86M. - Fix bar width: same normalization applied to bar percentage calculation. - Change period: 5000 blocks (~17h) → 7200 blocks (24h exactly at 12s/block). - Update all "~17h" text to "~24h". registry page: - Shorten all tooltip text significantly (1-2 lines max). - Fix tooltip triggers: Mock badge and Confidential badge now have a separate ? icon (Tooltip standalone), not the badge itself as trigger. --- src/app/analytics/page.tsx | 29 +++++++---- src/app/page.tsx | 102 ++++++------------------------------- 2 files changed, 35 insertions(+), 96 deletions(-) diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx index 0bebf0e..0fdaadd 100644 --- a/src/app/analytics/page.tsx +++ b/src/app/analytics/page.tsx @@ -33,6 +33,7 @@ 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; @@ -161,15 +162,15 @@ function StatCard({ /* ─── TVL bar ────────────────────────────────────────────────────────────────── */ function TVLBar({ token, - maxTvl, + maxTvlHuman, explorerBase, }: { token: TokenTVL; - maxTvl: bigint; + maxTvlHuman: number; explorerBase: string; }) { - const pct = maxTvl > 0n - ? Math.max(Number((token.tvlRaw * 10000n) / maxTvl) / 100, 0.5) + const pct = maxTvlHuman > 0 + ? Math.max((token.tvlHuman / maxTvlHuman) * 100, 0.5) : 0; const volCompact = formatTVLCompact(token.shieldVolume, token.decimals); @@ -318,7 +319,7 @@ function RatioBar({ shields, unshields }: { shields: number; unshields: number } /* ─── Main page ──────────────────────────────────────────────────────────────── */ -const BLOCK_LOOKBACK = 5000n; // ~17 hours on Sepolia (12s blocks) +const BLOCK_LOOKBACK = 7200n; // 24 hours on Sepolia/Mainnet (12s blocks × 7200 = 86400s) const BLOCK_TIME_MS = 12_000; // ~12 seconds per block export default function AnalyticsPage() { @@ -385,10 +386,17 @@ export default function AnalyticsPage() { (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, @@ -444,7 +452,8 @@ export default function AnalyticsPage() { .sort((a, b) => Number(b.blockNumber - a.blockNumber)) .slice(0, 25); - setTvlData(allTvl.sort((a, b) => (b.tvlRaw > a.tvlRaw ? 1 : -1))); + // 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) { @@ -465,7 +474,7 @@ export default function AnalyticsPage() { const uniqueShielders = new Set( activity.filter((e) => e.type === 'shield').map((e) => e.from.toLowerCase()), ).size; - const maxTvl = tvlData.reduce((m, t) => (t.tvlRaw > m ? t.tvlRaw : m), 0n); + 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 @@ -506,7 +515,7 @@ export default function AnalyticsPage() { {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading…'} -  · Last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks (~17h) +  · Last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks (~24h)  · Showing up to 25 recent events
)} @@ -664,7 +673,7 @@ export default function AnalyticsPage() { )}

- Latest shield & unshield events across all tokens · last ~17h · up to 25 shown + Latest shield & unshield events across all tokens · last ~24h · up to 25 shown

{isLoading && activity.length === 0 ? ( diff --git a/src/app/page.tsx b/src/app/page.tsx index d5609b9..4e35c3b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -34,80 +34,14 @@ import { // Centralised here so copy can be revised without hunting through JSX. const TIP = { - erc7984: ( - <> - ERC-7984 Confidential Wrapper -
- A smart contract that wraps a public ERC-20 token and stores balances as - on-chain ciphertext using Zama's Fully Homomorphic Encryption (FHE). - Nobody — including the node operators — can read your balance without - your cryptographic permit. - - ), - confidentialBadge: ( - <> - Confidential token (ERC-7984) -
- Balances and transfer amounts are encrypted on-chain via FHE. Only the - owner can decrypt them by signing an EIP-712 permit with their wallet. - - ), - publicBalance: ( - <> - Public ERC-20 balance -
- Your current unencrypted balance of the underlying token. Visible to - anyone on-chain — shield it to make it private. - - ), - confidentialBalance: ( - <> - Confidential (encrypted) balance -
- Your balance is stored as an encrypted ciphertext on-chain. Click{' '} - Decrypt to sign an EIP-712 permit in your - wallet — this creates a short-lived session key that lets the Zama - Gateway decrypt the value for you locally. Your private key never - leaves your wallet and the plaintext is never stored on-chain. - - ), - mockBadge: ( - <> - Mock token (testnet only) -
- This underlying ERC-20 was deployed by Zama for developer testing. It - has a public mint() function (up to 1 000 000 tokens per - call) so you can request free test tokens from the Faucet page. - - ), - shield: (sym: string) => ( - <> - Shield (Wrap) -
- Approve and deposit your public {sym} tokens into the - ERC-7984 wrapper. The wrapper mints an encrypted confidential balance - — your on-chain amount becomes private. - - ), - unshield: (sym: string) => ( - <> - Unshield (Unwrap) -
- Burn your encrypted c{sym} tokens and retrieve the - equivalent public {sym}. The Zama Gateway processes - the decryption proof before releasing the underlying tokens. - - ), - permit: ( - <> - EIP-712 Permit -
- A typed off-chain signature that authorises the Zama Gateway to decrypt - your encrypted balance for this session. It does not spend any - tokens or approve any contract — it is a read-only authorisation that - expires automatically. - - ), + 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 ──────────────────────────────────────────────────────── @@ -167,11 +101,10 @@ function RegistryTokenRow({
{cleanName} {isMock && ( - - - Mock - - +
+ Mock + +
)} {isRevoked && ( @@ -208,15 +141,12 @@ function RegistryTokenRow({ {/* ── ERC-7984 Wrapper ──────────────────────────────────────────────── */}
- - +
+ Confidential - + +
Date: Tue, 23 Jun 2026 14:44:29 +0300 Subject: [PATCH 09/18] fix: center subtitle text on learn and dev-tools header pages --- src/app/developers/page.tsx | 2 +- src/app/learn/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/developers/page.tsx b/src/app/developers/page.tsx index b16a433..c1328e4 100644 --- a/src/app/developers/page.tsx +++ b/src/app/developers/page.tsx @@ -565,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/learn/page.tsx b/src/app/learn/page.tsx index 513dbbb..e2c895b 100644 --- a/src/app/learn/page.tsx +++ b/src/app/learn/page.tsx @@ -551,7 +551,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)', }} > From 260c8d094f47a8c9689e429e9375d804941b21f4 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Wed, 24 Jun 2026 02:37:35 +0300 Subject: [PATCH 10/18] feat: Crystal Lattice hero section with 3D WebGL scene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hero section added above registry table on home page: - CrystalLattice: R3F 3D scene with morphing icosahedron wireframe (simplex noise displacement), gold activation waves, instanced diamond particles drifting outward, shield prism with MeshPhysicalMaterial (transmission 0.9, IOR 2.2, rainbow refraction) - HeroHeadline: "EVERY BIT. ENCRYPTED." with per-character Framer Motion spring animation (scatter → assemble), prismatic CSS shimmer on second line, golden period, BlurIn subheadline - HeroCTA: chamfered clip-path buttons with prismatic light sweep on hover, center-outward fill on secondary button - Stats row: 8 Token Pairs / FHE / ERC-7984 - Scroll transition: content parallax + opacity fade, lattice shatter (scale explosion), diamond particle burst - Mobile responsive: badges hidden, stacked CTAs, smaller headline - prefers-reduced-motion: all animations disabled, static render - Code-split: CrystalLattice lazy-loaded via next/dynamic (ssr: false) - Theme reactive: reads --accent from CSS custom properties via MutationObserver on data-design-theme attribute changes - New deps: three, @react-three/fiber, @react-three/drei, framer-motion, d3-delaunay, simplex-noise --- package-lock.json | 714 ++++++++++++++++++++++++- package.json | 8 + src/app/globals.css | 313 +++++++++++ src/app/page.tsx | 8 +- src/components/hero/CrystalLattice.tsx | 308 +++++++++++ src/components/hero/HeroCTA.tsx | 39 ++ src/components/hero/HeroHeadline.tsx | 117 ++++ src/components/hero/HeroSection.tsx | 92 ++++ src/hooks/useReducedMotion.ts | 17 + 9 files changed, 1607 insertions(+), 9 deletions(-) create mode 100644 src/components/hero/CrystalLattice.tsx create mode 100644 src/components/hero/HeroCTA.tsx create mode 100644 src/components/hero/HeroHeadline.tsx create mode 100644 src/components/hero/HeroSection.tsx create mode 100644 src/hooks/useReducedMotion.ts diff --git a/package-lock.json b/package-lock.json index 9a6b769..9b7d833 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,22 +8,30 @@ "name": "zamavault", "version": "0.1.0", "dependencies": { + "@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", + "d3-delaunay": "^6.0.4", + "framer-motion": "^12.41.0", "lucide-react": "^1.18.0", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", "react-icons": "^5.6.0", + "simplex-noise": "^4.0.3", + "three": "^0.184.0", "viem": "^2.52.2", "wagmi": "^3.6.16" }, "devDependencies": { "@types/canvas-confetti": "^1.9.0", + "@types/d3-delaunay": "^6.0.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.184.1", "eslint": "^9", "eslint-config-next": "16.2.9", "typescript": "^5", @@ -228,6 +236,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 +293,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", @@ -1083,6 +1106,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,6 +1396,94 @@ "url": "https://github.com/sponsors/Boshen" } }, + "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", @@ -1733,6 +1862,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 +1897,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 +1911,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 +1948,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" @@ -1820,6 +1973,41 @@ "@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 +2657,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", @@ -2988,6 +3194,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 +3226,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 +3293,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 +3377,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", @@ -3225,11 +3497,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 +3533,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 +3669,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", @@ -3392,6 +3710,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", @@ -4236,6 +4560,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 +4646,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", @@ -4508,6 +4865,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", @@ -4632,6 +4995,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 +5031,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 +5372,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 +5534,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 +5569,18 @@ "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/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5322,6 +5734,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 +6071,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 +6111,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", @@ -5737,6 +6183,21 @@ } } }, + "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", @@ -6162,7 +6623,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 +6699,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 +6715,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", @@ -6329,6 +6805,21 @@ "dev": true, "license": "MIT" }, + "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 +6879,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 +6943,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 +7210,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 +7222,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 +7310,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 +7339,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 +7582,15 @@ "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/tfhe": { "version": "1.4.0-alpha.3", "resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz", @@ -7053,6 +7598,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 +7731,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 +7806,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", @@ -7443,6 +8093,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 +8498,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 +8703,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..64e45a7 100644 --- a/package.json +++ b/package.json @@ -10,22 +10,30 @@ "test": "vitest run" }, "dependencies": { + "@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", + "d3-delaunay": "^6.0.4", + "framer-motion": "^12.41.0", "lucide-react": "^1.18.0", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", "react-icons": "^5.6.0", + "simplex-noise": "^4.0.3", + "three": "^0.184.0", "viem": "^2.52.2", "wagmi": "^3.6.16" }, "devDependencies": { "@types/canvas-confetti": "^1.9.0", + "@types/d3-delaunay": "^6.0.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.184.1", "eslint": "^9", "eslint-config-next": "16.2.9", "typescript": "^5", diff --git a/src/app/globals.css b/src/app/globals.css index 9b5da8c..20f3f1d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -66,6 +66,8 @@ table { border-collapse: collapse; } --accent-muted: rgba(56, 189, 248, 0.18); --accent-subtle: rgba(56, 189, 248, 0.05); --accent-glow: rgba(56, 189, 248, 0.2); + --zama-gold: #FFD208; + --zama-gold-glow: rgba(255, 210, 8, 0.15); --text-primary: #f8f9fa; --text-secondary: #a0a5b5; @@ -275,6 +277,317 @@ html[data-theme='light'] { --grid-line: rgba(0, 0, 0, 0.025); } +/* ========================================================================== + HERO SECTION + ========================================================================== */ + +.hero-section { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; + padding: var(--sp-20) var(--sp-4) var(--sp-12); +} + +.hero-gradient-overlay { + position: absolute; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 30% 40%, rgba(56, 189, 248, 0.06) 0%, transparent 60%), + radial-gradient(ellipse 50% 40% at 70% 30%, var(--zama-gold-glow) 0%, transparent 50%), + linear-gradient(to bottom, transparent 70%, var(--bg-base) 100%); + pointer-events: none; + z-index: 1; +} + +.hero-content { + position: relative; + z-index: 2; + text-align: center; + max-width: 800px; + will-change: transform, opacity; +} + +/* ── Headline ── */ +.hero-headline-wrap { + position: relative; +} + +.hero-headline { + font-size: clamp(2.5rem, 6vw, 5rem); + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--text-primary); + margin: 0; +} + +.hero-headline-line { + display: inline-block; +} + +.hero-headline-shimmer { + background: linear-gradient( + 105deg, + var(--text-primary) 35%, + var(--accent) 50%, + var(--text-primary) 65% + ); + background-size: 250% 100%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmerText 6s ease-in-out infinite; +} + +@keyframes shimmerText { + 0%, 100% { background-position: 100% 50%; } + 50% { background-position: 0% 50%; } +} + +.hero-sub { + font-size: var(--text-lg); + color: var(--text-secondary); + max-width: 560px; + margin: 0 auto; + line-height: var(--lh-relaxed); +} + +/* ── Floating badges ── */ +.hero-badges { + position: absolute; + inset: -40px; + pointer-events: none; + z-index: 0; +} + +.hero-badge { + position: absolute; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + color: var(--accent); + opacity: 0.15; + padding: 3px 8px; + border: 1px solid var(--accent); + border-radius: var(--radius-sm); + animation: floatBadge 8s ease-in-out infinite alternate; + backdrop-filter: blur(4px); +} + +@keyframes floatBadge { + 0% { transform: translateY(0) rotate(0deg); opacity: 0.1; } + 50% { opacity: 0.25; } + 100% { transform: translateY(-20px) rotate(3deg); opacity: 0.1; } +} + +/* ── CTA Buttons ── */ +.hero-cta-row { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-4); + margin-top: var(--sp-8); + flex-wrap: wrap; +} + +.hero-btn-primary { + position: relative; + display: inline-flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-4) var(--sp-8); + font-size: var(--text-base); + font-weight: 700; + color: var(--text-inverse); + background: var(--accent); + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + overflow: hidden; + text-decoration: none; + transition: transform var(--t-fast), box-shadow var(--t-fast); + clip-path: polygon(8px 0, calc(100% - 8px) 0, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px), 0 8px); +} + +.hero-btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 25px var(--accent-glow), 0 8px 40px var(--zama-gold-glow); +} + +.hero-btn-primary:active { + transform: scale(0.97); +} + +.hero-btn-shimmer { + position: absolute; + inset: 0; + background: linear-gradient( + 105deg, + transparent 30%, + rgba(255, 255, 255, 0.25) 50%, + transparent 70% + ); + background-size: 300% 100%; + background-position: 200% 0; + transition: background-position 0.6s ease; + pointer-events: none; +} + +.hero-btn-primary:hover .hero-btn-shimmer { + background-position: -100% 0; +} + +.hero-btn-secondary { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-6); + font-size: var(--text-sm); + font-weight: 600; + color: var(--text-secondary); + background: transparent; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + cursor: pointer; + position: relative; + overflow: hidden; + transition: color var(--t-fast), border-color var(--t-fast); + clip-path: polygon(6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px), calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px), 0 6px); +} + +.hero-btn-secondary::before { + content: ''; + position: absolute; + inset: 0; + background: var(--accent-muted); + transform: scaleX(0); + transform-origin: center; + transition: transform 0.3s var(--ease); +} + +.hero-btn-secondary:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.hero-btn-secondary:hover::before { + transform: scaleX(1); +} + +/* ── Stats row ── */ +.hero-stats { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-6); + margin-top: var(--sp-10); + padding-top: var(--sp-8); + border-top: 1px solid var(--border); + opacity: 0; + animation: fadeIn 0.6s var(--ease) 2.2s forwards; +} + +.hero-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.hero-stat-value { + font-size: var(--text-xl); + font-weight: 800; + color: var(--accent); + font-family: var(--font-mono); +} + +.hero-stat-label { + font-size: var(--text-xs); + color: var(--text-muted); + text-transform: lowercase; + letter-spacing: 0.05em; +} + +.hero-stat-divider { + width: 1px; + height: 32px; + background: var(--border); +} + +/* ── Scroll hint ── */ +.hero-scroll-hint { + position: absolute; + bottom: var(--sp-8); + left: 50%; + transform: translateX(-50%); + z-index: 2; +} + +.hero-scroll-line { + width: 1px; + height: 40px; + background: linear-gradient(to bottom, var(--accent), transparent); + animation: scrollPulse 2s ease-in-out infinite; +} + +@keyframes scrollPulse { + 0%, 100% { opacity: 0.3; transform: scaleY(1); } + 50% { opacity: 0.8; transform: scaleY(1.3); } +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .hero-section { + min-height: 85vh; + padding: var(--sp-16) var(--sp-3) var(--sp-8); + } + + .hero-headline { + font-size: clamp(2rem, 8vw, 3rem); + } + + .hero-sub { + font-size: var(--text-base); + } + + .hero-stats { + gap: var(--sp-4); + } + + .hero-badges { + display: none; + } + + .hero-cta-row { + flex-direction: column; + } + + .hero-btn-primary, + .hero-btn-secondary { + width: 100%; + justify-content: center; + } +} + +@media (max-width: 480px) { + .hero-headline { + font-size: clamp(1.75rem, 10vw, 2.5rem); + } + + .hero-stats { + flex-wrap: wrap; + gap: var(--sp-3); + } + + .hero-stat-divider { + display: none; + } +} + /* ---------- BASE STYLES ---------- */ body { font-family: var(--font-sans); diff --git a/src/app/page.tsx b/src/app/page.tsx index 4e35c3b..5dd3bbf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,6 +15,7 @@ import { useRegistryPairs, isMintablePair, type RegistryPairsResult } from '@/li import { type WrapperPair } from '@/config/contracts'; import { ERC20_ABI } from '@/lib/wrapper-abi'; import BlurIn from '@/components/ui/BlurIn'; +import HeroSection from '@/components/hero/HeroSection'; import { useAccount, useReadContract } from 'wagmi'; import { useConfidentialBalance } from '@zama-fhe/react-sdk'; import { @@ -291,7 +292,11 @@ export default function HomePage() { const explorerBase = isTestnet ? 'https://eth-sepolia.blockscout.com' : 'https://eth.blockscout.com'; return ( -
+ <> + {/* ── Hero Section ── */} + + +
{/* Header */}

@@ -451,5 +456,6 @@ export default function HomePage() {
+ ); } diff --git a/src/components/hero/CrystalLattice.tsx b/src/components/hero/CrystalLattice.tsx new file mode 100644 index 0000000..712a25b --- /dev/null +++ b/src/components/hero/CrystalLattice.tsx @@ -0,0 +1,308 @@ +'use client'; + +import React, { useRef, useMemo, useEffect, useState } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { Float } from '@react-three/drei'; +import * as THREE from 'three'; +import { createNoise3D } from 'simplex-noise'; + +/* ─── Voronoi wireframe sphere ──────────────────────────────────────────────── */ + +function LatticeSphere({ + accentColor, + goldColor, + reducedMotion, +}: { + accentColor: string; + goldColor: string; + reducedMotion: boolean; +}) { + const meshRef = useRef(null); + const noise3D = useMemo(() => createNoise3D(), []); + const accentThree = useMemo(() => new THREE.Color(accentColor), [accentColor]); + const goldThree = useMemo(() => new THREE.Color(goldColor), [goldColor]); + + // Generate icosahedron wireframe points (subdivision creates Voronoi-like pattern) + const { geometry, basePositions, colorArray } = useMemo(() => { + const ico = new THREE.IcosahedronGeometry(2.8, 3); // ~320 triangles + const edges = new THREE.EdgesGeometry(ico); + const positions = edges.attributes.position.array as Float32Array; + const base = new Float32Array(positions.length); + base.set(positions); + + // Initialize colors + const colors = new Float32Array(positions.length); + for (let i = 0; i < positions.length; i += 3) { + colors[i] = accentThree.r; + colors[i + 1] = accentThree.g; + colors[i + 2] = accentThree.b; + } + edges.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + + return { geometry: edges, basePositions: base, colorArray: colors }; + }, [accentThree]); + + // Activation wave state + const waveRef = useRef({ time: 0, seed: Math.random() * 100 }); + + useFrame((state, delta) => { + if (!meshRef.current || reducedMotion) return; + const time = state.clock.elapsedTime; + const positions = geometry.attributes.position.array as Float32Array; + + // Morph vertices with noise + for (let i = 0; i < basePositions.length; i += 3) { + const bx = basePositions[i]; + const by = basePositions[i + 1]; + const bz = basePositions[i + 2]; + + const n = noise3D(bx * 0.4 + time * 0.08, by * 0.4, bz * 0.4 + time * 0.05); + const displacement = 1 + n * 0.12; + + positions[i] = bx * displacement; + positions[i + 1] = by * displacement; + positions[i + 2] = bz * displacement; + + // Activation wave: propagate gold flash across surface + const dist = Math.sqrt(bx * bx + by * by + bz * bz); + const wavePos = (time * 0.5 + waveRef.current.seed) % 6; + const waveDist = Math.abs(dist - wavePos); + const waveIntensity = Math.max(0, 1 - waveDist * 2); + + // Blend between accent and gold based on wave + const r = accentThree.r + (goldThree.r - accentThree.r) * waveIntensity; + const g = accentThree.g + (goldThree.g - accentThree.g) * waveIntensity; + const b = accentThree.b + (goldThree.b - accentThree.b) * waveIntensity; + colorArray[i] = r; + colorArray[i + 1] = g; + colorArray[i + 2] = b; + } + + geometry.attributes.position.needsUpdate = true; + geometry.attributes.color.needsUpdate = true; + + // Slow rotation + meshRef.current.rotation.y += delta * 0.06; + meshRef.current.rotation.x += delta * 0.012; + }); + + return ( + + + + ); +} + +/* ─── Diamond particles ─────────────────────────────────────────────────────── */ + +function DiamondParticles({ accentColor, count = 20 }: { accentColor: string; count?: number }) { + const meshRef = useRef(null); + const dummy = useMemo(() => new THREE.Object3D(), []); + const color = useMemo(() => new THREE.Color(accentColor), [accentColor]); + + // Particle state: position, velocity, life + const particles = useMemo(() => { + return Array.from({ length: count }, () => ({ + pos: new THREE.Vector3( + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + ).normalize().multiplyScalar(3 + Math.random()), + vel: new THREE.Vector3( + (Math.random() - 0.5) * 0.01, + (Math.random() - 0.5) * 0.01, + (Math.random() - 0.5) * 0.01, + ), + life: Math.random(), + speed: 0.1 + Math.random() * 0.2, + })); + }, [count]); + + useFrame((_, delta) => { + if (!meshRef.current) return; + + for (let i = 0; i < count; i++) { + const p = particles[i]; + p.life += delta * p.speed; + + if (p.life > 1) { + // Respawn at sphere surface + p.pos.set( + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + ).normalize().multiplyScalar(3); + p.vel.set( + (Math.random() - 0.5) * 0.01, + (Math.random() - 0.5) * 0.01, + (Math.random() - 0.5) * 0.01, + ); + p.life = 0; + } + + // Drift outward + p.pos.add(p.vel); + p.pos.multiplyScalar(1 + delta * 0.05); + + const scale = Math.sin(p.life * Math.PI) * 0.04; + dummy.position.copy(p.pos); + dummy.position.x += 1.5; // Match sphere offset + dummy.scale.setScalar(scale); + dummy.rotation.y += delta; + dummy.updateMatrix(); + meshRef.current.setMatrixAt(i, dummy.matrix); + } + meshRef.current.instanceMatrix.needsUpdate = true; + }); + + return ( + + + + + ); +} + +/* ─── Shield prism ──────────────────────────────────────────────────────────── */ + +function ShieldPrism({ + accentColor, + goldColor, +}: { + accentColor: string; + goldColor: string; +}) { + const prismRef = useRef(null); + const light1Ref = useRef(null); + const light2Ref = useRef(null); + const { pointer } = useThree(); + + useFrame((state) => { + if (!prismRef.current) return; + const t = state.clock.elapsedTime; + + // Gentle bob + rotation + prismRef.current.position.y = Math.sin(t * 0.8) * 0.15; + prismRef.current.rotation.y += 0.003; + prismRef.current.rotation.z = Math.sin(t * 0.5) * 0.05; + + // Lights orbit based on mouse + if (light1Ref.current) { + light1Ref.current.position.x = Math.cos(t * 0.3 + pointer.x * 2) * 3; + light1Ref.current.position.z = Math.sin(t * 0.3 + pointer.y * 2) * 3; + light1Ref.current.position.y = Math.sin(t * 0.2) * 1.5; + } + if (light2Ref.current) { + light2Ref.current.position.x = Math.cos(t * 0.4 + pointer.x) * -2.5; + light2Ref.current.position.z = Math.sin(t * 0.4 + pointer.y) * 2.5; + light2Ref.current.position.y = Math.cos(t * 0.3) * 1; + } + }); + + return ( + + + + + + + + + + + ); +} + +/* ─── Floating hex badges ───────────────────────────────────────────────────── */ + +function FloatingBadge({ text, position }: { text: string; position: [number, number, number] }) { + return ( + + + {/* Using sprite text for simplicity — badges are rendered via CSS overlay instead */} + + + + + + + ); +} + +/* ─── Main 3D scene ─────────────────────────────────────────────────────────── */ + +interface CrystalLatticeProps { + scrollProgress: number; // 0 to 1 + reducedMotion: boolean; +} + +export default function CrystalLattice({ scrollProgress, reducedMotion }: CrystalLatticeProps) { + const [accentColor, setAccentColor] = useState('#38bdf8'); + const goldColor = '#FFD208'; + + // Read theme accent from CSS + useEffect(() => { + const readAccent = () => { + const computed = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(); + if (computed) setAccentColor(computed); + }; + readAccent(); + + // Re-read on theme change + const observer = new MutationObserver(readAccent); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-design-theme', 'data-theme'] }); + return () => observer.disconnect(); + }, []); + + // Shatter effect: scale explosion factor from scroll + const shatterScale = 1 + scrollProgress * 3; + const opacity = Math.max(0, 1 - scrollProgress * 1.5); + + if (reducedMotion || opacity <= 0) { + return null; + } + + return ( +
+ + 1.5 ? [shatterScale, shatterScale, shatterScale] : undefined}> + + + + + + + + + + +
+ ); +} diff --git a/src/components/hero/HeroCTA.tsx b/src/components/hero/HeroCTA.tsx new file mode 100644 index 0000000..9d4babe --- /dev/null +++ b/src/components/hero/HeroCTA.tsx @@ -0,0 +1,39 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { Shield, ArrowDown } from 'lucide-react'; + +interface HeroCTAProps { + reducedMotion: boolean; +} + +export default function HeroCTA({ reducedMotion }: HeroCTAProps) { + const handleScrollToRegistry = () => { + const registry = document.getElementById('registry-section'); + if (registry) { + registry.scrollIntoView({ behavior: 'smooth' }); + } + }; + + return ( + + + + + Shield Your Tokens + + + + + ); +} diff --git a/src/components/hero/HeroHeadline.tsx b/src/components/hero/HeroHeadline.tsx new file mode 100644 index 0000000..7e2f50b --- /dev/null +++ b/src/components/hero/HeroHeadline.tsx @@ -0,0 +1,117 @@ +'use client'; + +import React from 'react'; +import { motion } from 'framer-motion'; +import BlurIn from '@/components/ui/BlurIn'; + +/* ─── Letter animation: each character slides in from random offset ─────── */ + +const HEADLINE_L1 = 'EVERY BIT.'; +const HEADLINE_L2 = 'ENCRYPTED.'; + +function AnimatedLine({ + text, + delay = 0, + reducedMotion, +}: { + text: string; + delay?: number; + reducedMotion: boolean; +}) { + if (reducedMotion) { + return {text}; + } + + return ( + <> + {text.split('').map((char, i) => { + const isLast = i === text.length - 1 && text.endsWith('.'); + return ( + + {char === ' ' ? ' ' : char} + + ); + })} + + ); +} + +/* ─── Main headline component ───────────────────────────────────────────── */ + +interface HeroHeadlineProps { + reducedMotion: boolean; +} + +export default function HeroHeadline({ reducedMotion }: HeroHeadlineProps) { + return ( +
+ {/* Main headline */} +

+ + + +
+ + + +

+ + {/* Subheadline */} +
+ +
+ + {/* Floating hex badges — CSS positioned, not 3D */} + +
+ ); +} diff --git a/src/components/hero/HeroSection.tsx b/src/components/hero/HeroSection.tsx new file mode 100644 index 0000000..2980c41 --- /dev/null +++ b/src/components/hero/HeroSection.tsx @@ -0,0 +1,92 @@ +'use client'; + +import React, { useRef, useState, useEffect, Suspense } from 'react'; +import dynamic from 'next/dynamic'; +import HeroHeadline from './HeroHeadline'; +import HeroCTA from './HeroCTA'; +import { useReducedMotion } from '@/hooks/useReducedMotion'; + +// Lazy-load the 3D scene — registry table loads instantly +const CrystalLattice = dynamic(() => import('./CrystalLattice'), { + ssr: false, + loading: () => null, +}); + +export default function HeroSection() { + const heroRef = useRef(null); + const reducedMotion = useReducedMotion(); + const [scrollProgress, setScrollProgress] = useState(0); + + // Scroll-linked fade/shatter + useEffect(() => { + if (reducedMotion) return; + + const handleScroll = () => { + if (!heroRef.current) return; + const rect = heroRef.current.getBoundingClientRect(); + const heroHeight = heroRef.current.offsetHeight; + // Progress: 0 (fully visible) → 1 (scrolled past) + const progress = Math.max(0, Math.min(1, -rect.top / (heroHeight * 0.6))); + setScrollProgress(progress); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, [reducedMotion]); + + const contentOpacity = Math.max(0, 1 - scrollProgress * 2); + const contentY = scrollProgress * -60; + + return ( +
+ {/* 3D Background */} + + + + + {/* Gradient overlay for depth */} +
+ + {/* Content */} +
+ + + + {/* Stats row */} +
+
+ 8 + Token Pairs +
+
+
+ FHE + Encryption +
+
+
+ ERC-7984 + Standard +
+
+
+ + {/* Scroll indicator */} +
+
+
+
+ ); +} diff --git a/src/hooks/useReducedMotion.ts b/src/hooks/useReducedMotion.ts new file mode 100644 index 0000000..363499b --- /dev/null +++ b/src/hooks/useReducedMotion.ts @@ -0,0 +1,17 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +export function useReducedMotion(): boolean { + const [reduced, setReduced] = useState(false); + + useEffect(() => { + const mql = window.matchMedia('(prefers-reduced-motion: reduce)'); + setReduced(mql.matches); + const handler = (e: MediaQueryListEvent) => setReduced(e.matches); + mql.addEventListener('change', handler); + return () => mql.removeEventListener('change', handler); + }, []); + + return reduced; +} From 16817b7616e96c1eadb293a89a731d7c664d14e2 Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Wed, 24 Jun 2026 02:44:31 +0300 Subject: [PATCH 11/18] revert: remove Crystal Lattice hero section --- .claude/launch.json | 17 ++ src/app/page.tsx | 8 +- src/components/hero/CrystalLattice.tsx | 308 ------------------------- src/components/hero/HeroCTA.tsx | 39 ---- src/components/hero/HeroHeadline.tsx | 117 ---------- src/components/hero/HeroSection.tsx | 92 -------- src/hooks/useReducedMotion.ts | 17 -- 7 files changed, 18 insertions(+), 580 deletions(-) create mode 100644 .claude/launch.json delete mode 100644 src/components/hero/CrystalLattice.tsx delete mode 100644 src/components/hero/HeroCTA.tsx delete mode 100644 src/components/hero/HeroHeadline.tsx delete mode 100644 src/components/hero/HeroSection.tsx delete mode 100644 src/hooks/useReducedMotion.ts diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..c380071 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "ZamaVault Dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + }, + { + "name": "ZamaVault Production Preview", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "start"], + "port": 3000 + } + ] +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 5dd3bbf..4e35c3b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,7 +15,6 @@ import { useRegistryPairs, isMintablePair, type RegistryPairsResult } from '@/li import { type WrapperPair } from '@/config/contracts'; import { ERC20_ABI } from '@/lib/wrapper-abi'; import BlurIn from '@/components/ui/BlurIn'; -import HeroSection from '@/components/hero/HeroSection'; import { useAccount, useReadContract } from 'wagmi'; import { useConfidentialBalance } from '@zama-fhe/react-sdk'; import { @@ -292,11 +291,7 @@ export default function HomePage() { const explorerBase = isTestnet ? 'https://eth-sepolia.blockscout.com' : 'https://eth.blockscout.com'; return ( - <> - {/* ── Hero Section ── */} - - -
+
{/* Header */}

@@ -456,6 +451,5 @@ export default function HomePage() {
- ); } diff --git a/src/components/hero/CrystalLattice.tsx b/src/components/hero/CrystalLattice.tsx deleted file mode 100644 index 712a25b..0000000 --- a/src/components/hero/CrystalLattice.tsx +++ /dev/null @@ -1,308 +0,0 @@ -'use client'; - -import React, { useRef, useMemo, useEffect, useState } from 'react'; -import { Canvas, useFrame, useThree } from '@react-three/fiber'; -import { Float } from '@react-three/drei'; -import * as THREE from 'three'; -import { createNoise3D } from 'simplex-noise'; - -/* ─── Voronoi wireframe sphere ──────────────────────────────────────────────── */ - -function LatticeSphere({ - accentColor, - goldColor, - reducedMotion, -}: { - accentColor: string; - goldColor: string; - reducedMotion: boolean; -}) { - const meshRef = useRef(null); - const noise3D = useMemo(() => createNoise3D(), []); - const accentThree = useMemo(() => new THREE.Color(accentColor), [accentColor]); - const goldThree = useMemo(() => new THREE.Color(goldColor), [goldColor]); - - // Generate icosahedron wireframe points (subdivision creates Voronoi-like pattern) - const { geometry, basePositions, colorArray } = useMemo(() => { - const ico = new THREE.IcosahedronGeometry(2.8, 3); // ~320 triangles - const edges = new THREE.EdgesGeometry(ico); - const positions = edges.attributes.position.array as Float32Array; - const base = new Float32Array(positions.length); - base.set(positions); - - // Initialize colors - const colors = new Float32Array(positions.length); - for (let i = 0; i < positions.length; i += 3) { - colors[i] = accentThree.r; - colors[i + 1] = accentThree.g; - colors[i + 2] = accentThree.b; - } - edges.setAttribute('color', new THREE.BufferAttribute(colors, 3)); - - return { geometry: edges, basePositions: base, colorArray: colors }; - }, [accentThree]); - - // Activation wave state - const waveRef = useRef({ time: 0, seed: Math.random() * 100 }); - - useFrame((state, delta) => { - if (!meshRef.current || reducedMotion) return; - const time = state.clock.elapsedTime; - const positions = geometry.attributes.position.array as Float32Array; - - // Morph vertices with noise - for (let i = 0; i < basePositions.length; i += 3) { - const bx = basePositions[i]; - const by = basePositions[i + 1]; - const bz = basePositions[i + 2]; - - const n = noise3D(bx * 0.4 + time * 0.08, by * 0.4, bz * 0.4 + time * 0.05); - const displacement = 1 + n * 0.12; - - positions[i] = bx * displacement; - positions[i + 1] = by * displacement; - positions[i + 2] = bz * displacement; - - // Activation wave: propagate gold flash across surface - const dist = Math.sqrt(bx * bx + by * by + bz * bz); - const wavePos = (time * 0.5 + waveRef.current.seed) % 6; - const waveDist = Math.abs(dist - wavePos); - const waveIntensity = Math.max(0, 1 - waveDist * 2); - - // Blend between accent and gold based on wave - const r = accentThree.r + (goldThree.r - accentThree.r) * waveIntensity; - const g = accentThree.g + (goldThree.g - accentThree.g) * waveIntensity; - const b = accentThree.b + (goldThree.b - accentThree.b) * waveIntensity; - colorArray[i] = r; - colorArray[i + 1] = g; - colorArray[i + 2] = b; - } - - geometry.attributes.position.needsUpdate = true; - geometry.attributes.color.needsUpdate = true; - - // Slow rotation - meshRef.current.rotation.y += delta * 0.06; - meshRef.current.rotation.x += delta * 0.012; - }); - - return ( - - - - ); -} - -/* ─── Diamond particles ─────────────────────────────────────────────────────── */ - -function DiamondParticles({ accentColor, count = 20 }: { accentColor: string; count?: number }) { - const meshRef = useRef(null); - const dummy = useMemo(() => new THREE.Object3D(), []); - const color = useMemo(() => new THREE.Color(accentColor), [accentColor]); - - // Particle state: position, velocity, life - const particles = useMemo(() => { - return Array.from({ length: count }, () => ({ - pos: new THREE.Vector3( - (Math.random() - 0.5) * 2, - (Math.random() - 0.5) * 2, - (Math.random() - 0.5) * 2, - ).normalize().multiplyScalar(3 + Math.random()), - vel: new THREE.Vector3( - (Math.random() - 0.5) * 0.01, - (Math.random() - 0.5) * 0.01, - (Math.random() - 0.5) * 0.01, - ), - life: Math.random(), - speed: 0.1 + Math.random() * 0.2, - })); - }, [count]); - - useFrame((_, delta) => { - if (!meshRef.current) return; - - for (let i = 0; i < count; i++) { - const p = particles[i]; - p.life += delta * p.speed; - - if (p.life > 1) { - // Respawn at sphere surface - p.pos.set( - (Math.random() - 0.5) * 2, - (Math.random() - 0.5) * 2, - (Math.random() - 0.5) * 2, - ).normalize().multiplyScalar(3); - p.vel.set( - (Math.random() - 0.5) * 0.01, - (Math.random() - 0.5) * 0.01, - (Math.random() - 0.5) * 0.01, - ); - p.life = 0; - } - - // Drift outward - p.pos.add(p.vel); - p.pos.multiplyScalar(1 + delta * 0.05); - - const scale = Math.sin(p.life * Math.PI) * 0.04; - dummy.position.copy(p.pos); - dummy.position.x += 1.5; // Match sphere offset - dummy.scale.setScalar(scale); - dummy.rotation.y += delta; - dummy.updateMatrix(); - meshRef.current.setMatrixAt(i, dummy.matrix); - } - meshRef.current.instanceMatrix.needsUpdate = true; - }); - - return ( - - - - - ); -} - -/* ─── Shield prism ──────────────────────────────────────────────────────────── */ - -function ShieldPrism({ - accentColor, - goldColor, -}: { - accentColor: string; - goldColor: string; -}) { - const prismRef = useRef(null); - const light1Ref = useRef(null); - const light2Ref = useRef(null); - const { pointer } = useThree(); - - useFrame((state) => { - if (!prismRef.current) return; - const t = state.clock.elapsedTime; - - // Gentle bob + rotation - prismRef.current.position.y = Math.sin(t * 0.8) * 0.15; - prismRef.current.rotation.y += 0.003; - prismRef.current.rotation.z = Math.sin(t * 0.5) * 0.05; - - // Lights orbit based on mouse - if (light1Ref.current) { - light1Ref.current.position.x = Math.cos(t * 0.3 + pointer.x * 2) * 3; - light1Ref.current.position.z = Math.sin(t * 0.3 + pointer.y * 2) * 3; - light1Ref.current.position.y = Math.sin(t * 0.2) * 1.5; - } - if (light2Ref.current) { - light2Ref.current.position.x = Math.cos(t * 0.4 + pointer.x) * -2.5; - light2Ref.current.position.z = Math.sin(t * 0.4 + pointer.y) * 2.5; - light2Ref.current.position.y = Math.cos(t * 0.3) * 1; - } - }); - - return ( - - - - - - - - - - - ); -} - -/* ─── Floating hex badges ───────────────────────────────────────────────────── */ - -function FloatingBadge({ text, position }: { text: string; position: [number, number, number] }) { - return ( - - - {/* Using sprite text for simplicity — badges are rendered via CSS overlay instead */} - - - - - - - ); -} - -/* ─── Main 3D scene ─────────────────────────────────────────────────────────── */ - -interface CrystalLatticeProps { - scrollProgress: number; // 0 to 1 - reducedMotion: boolean; -} - -export default function CrystalLattice({ scrollProgress, reducedMotion }: CrystalLatticeProps) { - const [accentColor, setAccentColor] = useState('#38bdf8'); - const goldColor = '#FFD208'; - - // Read theme accent from CSS - useEffect(() => { - const readAccent = () => { - const computed = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(); - if (computed) setAccentColor(computed); - }; - readAccent(); - - // Re-read on theme change - const observer = new MutationObserver(readAccent); - observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-design-theme', 'data-theme'] }); - return () => observer.disconnect(); - }, []); - - // Shatter effect: scale explosion factor from scroll - const shatterScale = 1 + scrollProgress * 3; - const opacity = Math.max(0, 1 - scrollProgress * 1.5); - - if (reducedMotion || opacity <= 0) { - return null; - } - - return ( -
- - 1.5 ? [shatterScale, shatterScale, shatterScale] : undefined}> - - - - - - - - - - -
- ); -} diff --git a/src/components/hero/HeroCTA.tsx b/src/components/hero/HeroCTA.tsx deleted file mode 100644 index 9d4babe..0000000 --- a/src/components/hero/HeroCTA.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; - -import React from 'react'; -import Link from 'next/link'; -import { motion } from 'framer-motion'; -import { Shield, ArrowDown } from 'lucide-react'; - -interface HeroCTAProps { - reducedMotion: boolean; -} - -export default function HeroCTA({ reducedMotion }: HeroCTAProps) { - const handleScrollToRegistry = () => { - const registry = document.getElementById('registry-section'); - if (registry) { - registry.scrollIntoView({ behavior: 'smooth' }); - } - }; - - return ( - - - - - Shield Your Tokens - - - - - ); -} diff --git a/src/components/hero/HeroHeadline.tsx b/src/components/hero/HeroHeadline.tsx deleted file mode 100644 index 7e2f50b..0000000 --- a/src/components/hero/HeroHeadline.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client'; - -import React from 'react'; -import { motion } from 'framer-motion'; -import BlurIn from '@/components/ui/BlurIn'; - -/* ─── Letter animation: each character slides in from random offset ─────── */ - -const HEADLINE_L1 = 'EVERY BIT.'; -const HEADLINE_L2 = 'ENCRYPTED.'; - -function AnimatedLine({ - text, - delay = 0, - reducedMotion, -}: { - text: string; - delay?: number; - reducedMotion: boolean; -}) { - if (reducedMotion) { - return {text}; - } - - return ( - <> - {text.split('').map((char, i) => { - const isLast = i === text.length - 1 && text.endsWith('.'); - return ( - - {char === ' ' ? ' ' : char} - - ); - })} - - ); -} - -/* ─── Main headline component ───────────────────────────────────────────── */ - -interface HeroHeadlineProps { - reducedMotion: boolean; -} - -export default function HeroHeadline({ reducedMotion }: HeroHeadlineProps) { - return ( -
- {/* Main headline */} -

- - - -
- - - -

- - {/* Subheadline */} -
- -
- - {/* Floating hex badges — CSS positioned, not 3D */} - -
- ); -} diff --git a/src/components/hero/HeroSection.tsx b/src/components/hero/HeroSection.tsx deleted file mode 100644 index 2980c41..0000000 --- a/src/components/hero/HeroSection.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client'; - -import React, { useRef, useState, useEffect, Suspense } from 'react'; -import dynamic from 'next/dynamic'; -import HeroHeadline from './HeroHeadline'; -import HeroCTA from './HeroCTA'; -import { useReducedMotion } from '@/hooks/useReducedMotion'; - -// Lazy-load the 3D scene — registry table loads instantly -const CrystalLattice = dynamic(() => import('./CrystalLattice'), { - ssr: false, - loading: () => null, -}); - -export default function HeroSection() { - const heroRef = useRef(null); - const reducedMotion = useReducedMotion(); - const [scrollProgress, setScrollProgress] = useState(0); - - // Scroll-linked fade/shatter - useEffect(() => { - if (reducedMotion) return; - - const handleScroll = () => { - if (!heroRef.current) return; - const rect = heroRef.current.getBoundingClientRect(); - const heroHeight = heroRef.current.offsetHeight; - // Progress: 0 (fully visible) → 1 (scrolled past) - const progress = Math.max(0, Math.min(1, -rect.top / (heroHeight * 0.6))); - setScrollProgress(progress); - }; - - window.addEventListener('scroll', handleScroll, { passive: true }); - return () => window.removeEventListener('scroll', handleScroll); - }, [reducedMotion]); - - const contentOpacity = Math.max(0, 1 - scrollProgress * 2); - const contentY = scrollProgress * -60; - - return ( -
- {/* 3D Background */} - - - - - {/* Gradient overlay for depth */} -
- - {/* Content */} -
- - - - {/* Stats row */} -
-
- 8 - Token Pairs -
-
-
- FHE - Encryption -
-
-
- ERC-7984 - Standard -
-
-
- - {/* Scroll indicator */} -
-
-
-
- ); -} diff --git a/src/hooks/useReducedMotion.ts b/src/hooks/useReducedMotion.ts deleted file mode 100644 index 363499b..0000000 --- a/src/hooks/useReducedMotion.ts +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; - -export function useReducedMotion(): boolean { - const [reduced, setReduced] = useState(false); - - useEffect(() => { - const mql = window.matchMedia('(prefers-reduced-motion: reduce)'); - setReduced(mql.matches); - const handler = (e: MediaQueryListEvent) => setReduced(e.matches); - mql.addEventListener('change', handler); - return () => mql.removeEventListener('change', handler); - }, []); - - return reduced; -} From b83663fc212a12b9862dafd540b5d52b2955ef9f Mon Sep 17 00:00:00 2001 From: hosein-ul Date: Wed, 24 Jun 2026 03:09:46 +0300 Subject: [PATCH 12/18] feat: standalone landing page at /landing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate from dashboard — no app Header/Footer via route group layout. Design: Zama brand (gold #FFD208 primary, clean dark #0a0a0a bg). Sections: - Sticky nav with Launch App CTA - Hero: headline with gold accent + live portfolio card visual - Stats bar: 8 pairs / 2 networks / FHE / ERC-7984 - Features: 3 cards (Registry / Shield / Decrypt) - How it works: 3 numbered steps with code hints - Token grid: all 7 pairs with color dots - CTA section with gold line border - Footer with links --- src/app/(marketing)/landing/page.tsx | 676 +++++++++++++++++++++++++++ src/app/(marketing)/layout.tsx | 38 ++ 2 files changed, 714 insertions(+) create mode 100644 src/app/(marketing)/landing/page.tsx create mode 100644 src/app/(marketing)/layout.tsx diff --git a/src/app/(marketing)/landing/page.tsx b/src/app/(marketing)/landing/page.tsx new file mode 100644 index 0000000..e841327 --- /dev/null +++ b/src/app/(marketing)/landing/page.tsx @@ -0,0 +1,676 @@ +import React from 'react'; +import Link from 'next/link'; + +/* ─── Token data ──────────────────────────────────────────────────────────── */ +const TOKENS = ['USDC', 'USDT', 'WETH', 'ZAMA', 'BRON', 'tGBP', 'XAUt']; + +/* ─── Inline styles as CSS ────────────────────────────────────────────────── */ +const CSS = ` + @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap'); + + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + html { scroll-behavior: smooth; } + body { + font-family: 'Plus Jakarta Sans', -apple-system, sans-serif; + background: #0a0a0a; + color: #fff; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; + } + a { text-decoration: none; color: inherit; } + + /* ── Tokens ── */ + :root { + --gold: #FFD208; + --gold-dim: rgba(255,210,8,.12); + --gold-glow: rgba(255,210,8,.25); + --ink: #0a0a0a; + --surface: #111111; + --surface-2: #171717; + --border: rgba(255,255,255,.08); + --border-strong: rgba(255,255,255,.14); + --text-main: #ffffff; + --text-dim: rgba(255,255,255,.5); + --text-muted: rgba(255,255,255,.28); + --mono: 'JetBrains Mono', monospace; + } + + /* ──────────────────────────────────────────────── + NAV + ──────────────────────────────────────────────── */ + .lp-nav { + position: sticky; top: 0; z-index: 100; + display: flex; align-items: center; justify-content: space-between; + padding: 0 64px; height: 68px; + background: rgba(10,10,10,.85); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + } + .lp-logo { + display: flex; align-items: center; gap: 10px; + font-size: 15px; font-weight: 800; color: #fff; + } + .lp-logo-mark { + width: 30px; height: 30px; border-radius: 7px; + background: var(--gold); + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; + } + .lp-logo-mark svg { display: block; } + .lp-logo-text span { color: var(--gold); } + .lp-nav-links { + display: flex; align-items: center; gap: 32px; + } + .lp-nav-links a { + font-size: 13px; color: var(--text-dim); font-weight: 500; + transition: color .15s; + } + .lp-nav-links a:hover { color: #fff; } + .lp-nav-right { display: flex; align-items: center; gap: 12px; } + .lp-nav-ghost { + padding: 8px 18px; font-size: 13px; font-weight: 600; + color: var(--text-dim); background: transparent; + border: 1px solid var(--border); border-radius: 7px; + cursor: pointer; transition: border-color .15s, color .15s; + } + .lp-nav-ghost:hover { border-color: var(--border-strong); color: #fff; } + .lp-nav-cta { + padding: 9px 22px; font-size: 13px; font-weight: 700; + background: var(--gold); color: #000; + border: none; border-radius: 7px; cursor: pointer; + transition: opacity .15s; + display: inline-flex; align-items: center; gap: 6px; + } + .lp-nav-cta:hover { opacity: .88; } + + /* ──────────────────────────────────────────────── + HERO + ──────────────────────────────────────────────── */ + .lp-hero { + position: relative; + padding: 120px 64px 100px; + max-width: 1280px; margin: 0 auto; + display: grid; grid-template-columns: 1fr 1fr; + gap: 80px; align-items: center; + } + .lp-hero-eyebrow { + display: inline-flex; align-items: center; gap: 8px; + padding: 5px 12px; border-radius: 100px; + border: 1px solid rgba(255,210,8,.25); + background: rgba(255,210,8,.06); + font-size: 11px; font-weight: 700; letter-spacing: .07em; + color: var(--gold); text-transform: uppercase; + margin-bottom: 28px; + } + .lp-hero-eyebrow-dot { + width: 5px; height: 5px; border-radius: 50%; + background: var(--gold); animation: lpBlink 2s ease infinite; + } + @keyframes lpBlink { 0%,100%{opacity:1} 50%{opacity:.25} } + + .lp-headline { + font-size: clamp(44px, 5vw, 72px); + font-weight: 800; line-height: 1.05; + letter-spacing: -.025em; color: #fff; + margin-bottom: 24px; + } + .lp-headline-gold { color: var(--gold); } + + .lp-sub { + font-size: 17px; line-height: 1.7; + color: var(--text-dim); + max-width: 460px; margin-bottom: 40px; + } + .lp-hero-btns { display: flex; gap: 12px; flex-wrap: wrap; } + .lp-btn-primary { + display: inline-flex; align-items: center; gap: 8px; + padding: 14px 28px; font-size: 15px; font-weight: 700; + background: var(--gold); color: #000; + border: none; border-radius: 9px; cursor: pointer; + transition: opacity .15s, transform .15s; + } + .lp-btn-primary:hover { opacity: .88; transform: translateY(-1px); } + .lp-btn-secondary { + display: inline-flex; align-items: center; gap: 8px; + padding: 13px 24px; font-size: 14px; font-weight: 600; + background: transparent; color: var(--text-dim); + border: 1px solid var(--border-strong); border-radius: 9px; cursor: pointer; + transition: color .15s, border-color .15s; + } + .lp-btn-secondary:hover { color: #fff; border-color: rgba(255,255,255,.3); } + + /* Hero visual */ + .lp-hero-visual { + position: relative; + } + .lp-vault-card { + background: var(--surface); + border: 1px solid var(--border-strong); + border-radius: 16px; + padding: 28px; + display: flex; flex-direction: column; gap: 16px; + } + .lp-vault-card-header { + display: flex; align-items: center; justify-content: space-between; + padding-bottom: 16px; border-bottom: 1px solid var(--border); + } + .lp-vault-card-title { font-size: 12px; font-weight: 700; letter-spacing: .05em; color: var(--text-muted); text-transform: uppercase; } + .lp-vault-card-badge { + padding: 3px 10px; border-radius: 100px; + background: rgba(255,210,8,.12); border: 1px solid rgba(255,210,8,.2); + font-size: 10px; font-weight: 700; color: var(--gold); letter-spacing: .05em; + } + .lp-token-row { + display: flex; align-items: center; justify-content: space-between; + padding: 12px 0; border-bottom: 1px solid var(--border); + } + .lp-token-row:last-of-type { border-bottom: none; } + .lp-token-left { display: flex; align-items: center; gap: 12px; } + .lp-token-icon { + width: 36px; height: 36px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 13px; font-weight: 700; color: #000; + flex-shrink: 0; + } + .lp-token-name { font-size: 14px; font-weight: 700; } + .lp-token-wrapped { font-size: 11px; color: var(--text-muted); font-family: var(--mono); } + .lp-token-enc { + font-family: var(--mono); font-size: 12px; + color: var(--text-muted); letter-spacing: 2px; + } + .lp-token-enc-gold { color: var(--gold); letter-spacing: 1px; } + .lp-card-footer { + padding-top: 12px; border-top: 1px solid var(--border); + display: flex; align-items: center; gap: 6px; + font-size: 11px; color: var(--text-muted); font-family: var(--mono); + } + .lp-card-footer-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; flex-shrink: 0; } + + /* ──────────────────────────────────────────────── + STATS BAR + ──────────────────────────────────────────────── */ + .lp-stats { + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + } + .lp-stats-inner { + max-width: 1280px; margin: 0 auto; + display: grid; grid-template-columns: repeat(4,1fr); + } + .lp-stat { + padding: 40px 64px; + border-right: 1px solid var(--border); + } + .lp-stat:last-child { border-right: none; } + .lp-stat-val { + font-size: 40px; font-weight: 800; letter-spacing: -.02em; + color: #fff; line-height: 1; + display: flex; align-items: baseline; gap: 4px; + } + .lp-stat-val sup { font-size: 18px; color: var(--gold); } + .lp-stat-lbl { font-size: 13px; color: var(--text-muted); margin-top: 6px; font-weight: 500; } + + /* ──────────────────────────────────────────────── + FEATURES + ──────────────────────────────────────────────── */ + .lp-features { + max-width: 1280px; margin: 0 auto; + padding: 100px 64px; + } + .lp-section-pre { + font-size: 11px; font-weight: 700; letter-spacing: .1em; + text-transform: uppercase; color: var(--gold); + margin-bottom: 16px; + } + .lp-section-title { + font-size: clamp(28px, 3vw, 42px); font-weight: 800; + letter-spacing: -.02em; color: #fff; + margin-bottom: 64px; max-width: 500px; + } + .lp-features-grid { + display: grid; grid-template-columns: repeat(3,1fr); + gap: 1px; background: var(--border); + border: 1px solid var(--border); + border-radius: 16px; overflow: hidden; + } + .lp-feature { + padding: 48px 40px; + background: var(--ink); + transition: background .2s; + position: relative; + } + .lp-feature:hover { background: var(--surface); } + .lp-feature-icon { + width: 44px; height: 44px; border-radius: 10px; + background: var(--gold-dim); + border: 1px solid rgba(255,210,8,.2); + display: flex; align-items: center; justify-content: center; + margin-bottom: 24px; font-size: 18px; + } + .lp-feature-title { font-size: 17px; font-weight: 700; margin-bottom: 10px; } + .lp-feature-body { font-size: 14px; color: var(--text-dim); line-height: 1.7; } + .lp-feature-tag { + position: absolute; top: 20px; right: 20px; + font-family: var(--mono); font-size: 10px; color: var(--text-muted); + letter-spacing: .06em; + } + + /* ──────────────────────────────────────────────── + HOW IT WORKS + ──────────────────────────────────────────────── */ + .lp-how { + background: var(--surface); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + padding: 100px 64px; + } + .lp-how-inner { max-width: 1280px; margin: 0 auto; } + .lp-steps { + display: grid; grid-template-columns: repeat(3,1fr); + gap: 48px; margin-top: 64px; position: relative; + } + .lp-steps::before { + content: ''; + position: absolute; top: 22px; left: calc(33.33% + 16px); + right: calc(33.33% + 16px); height: 1px; + background: linear-gradient(90deg, var(--border), var(--gold-glow), var(--border)); + } + .lp-step { display: flex; flex-direction: column; gap: 12px; } + .lp-step-num { + width: 44px; height: 44px; border-radius: 50%; + background: var(--gold-dim); border: 1px solid rgba(255,210,8,.25); + display: flex; align-items: center; justify-content: center; + font-size: 14px; font-weight: 800; color: var(--gold); + flex-shrink: 0; + } + .lp-step-title { font-size: 18px; font-weight: 700; padding-top: 12px; } + .lp-step-body { font-size: 14px; color: var(--text-dim); line-height: 1.7; } + .lp-step-code { + font-family: var(--mono); font-size: 11px; + color: var(--text-muted); margin-top: 4px; + letter-spacing: .02em; + } + .lp-step-code span { color: var(--gold); } + + /* ──────────────────────────────────────────────── + TOKEN GRID + ──────────────────────────────────────────────── */ + .lp-tokens { max-width: 1280px; margin: 0 auto; padding: 100px 64px; } + .lp-tokens-grid { + display: flex; gap: 8px; flex-wrap: wrap; + margin-top: 40px; + } + .lp-token-chip { + display: inline-flex; align-items: center; gap: 8px; + padding: 10px 20px; border-radius: 100px; + background: var(--surface); border: 1px solid var(--border); + font-size: 13px; font-weight: 700; + transition: border-color .2s, background .2s; + } + .lp-token-chip:hover { border-color: rgba(255,210,8,.3); background: var(--gold-dim); } + .lp-token-chip-dot { width: 8px; height: 8px; border-radius: 50%; } + .lp-token-chip-wrapped { font-family: var(--mono); font-size: 11px; color: var(--text-muted); } + + /* ──────────────────────────────────────────────── + CTA SECTION + ──────────────────────────────────────────────── */ + .lp-cta { + padding: 120px 64px; text-align: center; + border-top: 1px solid var(--border); + position: relative; overflow: hidden; + } + .lp-cta::before { + content: ''; + position: absolute; top: 0; left: 50%; transform: translateX(-50%); + width: 600px; height: 1px; + background: linear-gradient(90deg, transparent, var(--gold), transparent); + } + .lp-cta-title { + font-size: clamp(36px, 4vw, 60px); font-weight: 800; + letter-spacing: -.03em; margin-bottom: 16px; + line-height: 1.05; + } + .lp-cta-sub { font-size: 16px; color: var(--text-dim); margin-bottom: 48px; } + .lp-cta-note { + margin-top: 20px; font-size: 12px; color: var(--text-muted); + font-family: var(--mono); + } + + /* ──────────────────────────────────────────────── + FOOTER + ──────────────────────────────────────────────── */ + .lp-footer { + border-top: 1px solid var(--border); + padding: 40px 64px; + } + .lp-footer-inner { + max-width: 1280px; margin: 0 auto; + display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; gap: 16px; + } + .lp-footer-logo { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 800; } + .lp-footer-logo-mark { width: 22px; height: 22px; border-radius: 5px; background: var(--gold); display: flex; align-items: center; justify-content: center; } + .lp-footer-copy { font-size: 12px; color: var(--text-muted); margin-top: 2px; } + .lp-footer-links { display: flex; gap: 28px; } + .lp-footer-links a { font-size: 13px; color: var(--text-dim); transition: color .15s; } + .lp-footer-links a:hover { color: #fff; } + + /* ──────────────────────────────────────────────── + RESPONSIVE + ──────────────────────────────────────────────── */ + @media (max-width: 1024px) { + .lp-hero { grid-template-columns: 1fr; gap: 60px; } + .lp-hero-visual { max-width: 480px; } + .lp-features-grid { grid-template-columns: 1fr; } + .lp-stats-inner { grid-template-columns: repeat(2,1fr); } + .lp-stat { border-bottom: 1px solid var(--border); } + .lp-stat:nth-child(2) { border-right: none; } + .lp-stat:nth-child(3) { border-bottom: none; } + .lp-stat:nth-child(4) { border-right: none; border-bottom: none; } + .lp-steps { grid-template-columns: 1fr; } + .lp-steps::before { display: none; } + } + + @media (max-width: 768px) { + .lp-nav { padding: 0 20px; } + .lp-nav-links { display: none; } + .lp-hero { padding: 80px 20px 60px; } + .lp-stat { padding: 28px 20px; } + .lp-features { padding: 60px 20px; } + .lp-feature { padding: 32px 24px; } + .lp-how { padding: 60px 20px; } + .lp-tokens { padding: 60px 20px; } + .lp-cta { padding: 80px 20px; } + .lp-footer { padding: 32px 20px; } + .lp-footer-inner { flex-direction: column; align-items: flex-start; } + } +`; + +/* ─── Token colors ────────────────────────────────────────────────────────── */ +const TOKEN_COLORS: Record = { + USDC: '#2775CA', + USDT: '#26A17B', + WETH: '#627EEA', + ZAMA: '#FFD208', + BRON: '#8B5CF6', + tGBP: '#CF9B20', + XAUt: '#D4AF37', +}; + +export default function LandingPage() { + return ( + <> +