@@ -208,15 +141,12 @@ function RegistryTokenRow({
{/* ── ERC-7984 Wrapper ──────────────────────────────────────────────── */}
-
-
+
+
Confidential
-
+
+
Date: Tue, 23 Jun 2026 14:44:29 +0300
Subject: [PATCH 17/69] 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 18/69] 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
+
+
+
+
+ Explore Registry
+
+
+ );
+}
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 */}
+
+ {['0x7a3f', 'euint64', 'ERC-7984', 'FHE', 'PERMIT'].map((txt, i) => (
+
+ {txt}
+
+ ))}
+
+
+ );
+}
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 19/69] 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
-
-
-
-
- Explore Registry
-
-
- );
-}
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 */}
-
- {['0x7a3f', 'euint64', 'ERC-7984', 'FHE', 'PERMIT'].map((txt, i) => (
-
- {txt}
-
- ))}
-
-
- );
-}
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 20/69] 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 (
+ <>
+
+
+ {/* ── NAV ── */}
+
+
+
+
+
+
+
+
+ {/* ── HERO ── */}
+
+
+
+
+ Live on Ethereum · ERC-7984
+
+
+ Confidential tokens,
+ done right.
+
+
+ The most complete interface for Zama's confidential token ecosystem.
+ Shield ERC-20 tokens into FHE-encrypted wrappers — your balance stays
+ private, on-chain, always.
+
+
+
+
+ {/* Visual: mini portfolio card */}
+
+
+
+ Confidential Portfolio
+ ENCRYPTED
+
+
+ {['USDC', 'WETH', 'ZAMA'].map((sym) => (
+
+
+
+ {sym.charAt(0)}
+
+
+
c{sym}
+
ERC-7984 · FHE
+
+
+
+ {sym === 'USDC' ? (
+ 1,250.00
+ ) : (
+ '••••••'
+ )}
+
+
+ ))}
+
+
+
+ Decrypt with EIP-712 permit
+
+
+
+
+
+ {/* ── STATS BAR ── */}
+
+
+
+
8+
+
Registered token pairs
+
+
+
2
+
Supported networks
+
+
+
FHE
+
On-chain encryption
+
+
+
ERC‑7984
+
Confidential standard
+
+
+
+
+ {/* ── FEATURES ── */}
+
+ Core capabilities
+
+ Everything you need to go confidential.
+
+
+
+
+
registry.live()
+
🔍
+
Live Registry
+
+ Every ERC-7984 wrapper pair, read directly from the on-chain
+ WrappersRegistry. Mainnet and Sepolia. No snapshots, no stale data.
+
+
+
+
+
shield(amount)
+
🔒
+
One-click Shield
+
+ Approve and deposit any registered ERC-20. The wrapper mints an
+ encrypted cToken — your balance becomes FHE ciphertext on-chain.
+
+
+
+
+
decrypt(permit)
+
⚡
+
EIP-712 Decrypt
+
+ One off-chain signature grants a session key. Zama Gateway decrypts
+ your balance locally — never on-chain, never stored.
+
+
+
+
+
+ {/* ── HOW IT WORKS ── */}
+
+
+
Step by step
+
+ From zero to confidential in minutes.
+
+
+
+
+
1
+
Get test tokens
+
+ Use the built-in Faucet to mint free mock tokens on Sepolia —
+ USDC, WETH, ZAMA, and more. Up to 1,000,000 per call.
+
+
faucet.mint(cUSDCMock )
+
+
+
+
2
+
Shield your balance
+
+ Select a wrapper pair, enter an amount, and shield. Your ERC-20
+ is deposited and a confidential cToken is minted via FHE.
+
+
shield(1000n ) → cUSDC
+
+
+
+
3
+
Decrypt & manage
+
+ Sign an EIP-712 permit to view your encrypted balance. Transfer
+ or unshield anytime — your assets, your control.
+
+
decrypt(permit ) → 1,000
+
+
+
+
+
+ {/* ── TOKEN GRID ── */}
+
+ Registered pairs
+
+ All tokens. Both networks.
+
+
+
+
+
+ {/* ── CTA ── */}
+
+
+ Privacy starts with
+ one transaction.
+
+
+ No sign-up. No KYC. No compromise.
+
+
+ Open ZamaVault
+
+
+
+
+ Live on Ethereum Mainnet + Sepolia Testnet
+
+
+ {/* ── FOOTER ── */}
+
+
+
+
+
+ Built for Zama Developer Program Season 3 · 2026
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx
new file mode 100644
index 0000000..64c15af
--- /dev/null
+++ b/src/app/(marketing)/layout.tsx
@@ -0,0 +1,38 @@
+import type { Metadata } from 'next';
+import '../globals.css';
+
+export const metadata: Metadata = {
+ title: 'ZamaVault — Confidential Token Interface',
+ description:
+ 'The most complete interface for Zama\'s confidential token ecosystem. Shield ERC-20 tokens into FHE-encrypted wrappers on Ethereum.',
+};
+
+export default function MarketingLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
+}
From 595bb743b2feaca47cbdc0d94249885cced71ae0 Mon Sep 17 00:00:00 2001
From: hosein-ul
Date: Wed, 24 Jun 2026 03:15:17 +0300
Subject: [PATCH 21/69] revert: remove landing page
---
src/app/(marketing)/landing/page.tsx | 676 ---------------------------
src/app/(marketing)/layout.tsx | 38 --
2 files changed, 714 deletions(-)
delete mode 100644 src/app/(marketing)/landing/page.tsx
delete mode 100644 src/app/(marketing)/layout.tsx
diff --git a/src/app/(marketing)/landing/page.tsx b/src/app/(marketing)/landing/page.tsx
deleted file mode 100644
index e841327..0000000
--- a/src/app/(marketing)/landing/page.tsx
+++ /dev/null
@@ -1,676 +0,0 @@
-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 (
- <>
-
-
- {/* ── NAV ── */}
-
-
-
-
-
-
-
-
- {/* ── HERO ── */}
-
-
-
-
- Live on Ethereum · ERC-7984
-
-
- Confidential tokens,
- done right.
-
-
- The most complete interface for Zama's confidential token ecosystem.
- Shield ERC-20 tokens into FHE-encrypted wrappers — your balance stays
- private, on-chain, always.
-
-
-
-
- {/* Visual: mini portfolio card */}
-
-
-
- Confidential Portfolio
- ENCRYPTED
-
-
- {['USDC', 'WETH', 'ZAMA'].map((sym) => (
-
-
-
- {sym.charAt(0)}
-
-
-
c{sym}
-
ERC-7984 · FHE
-
-
-
- {sym === 'USDC' ? (
- 1,250.00
- ) : (
- '••••••'
- )}
-
-
- ))}
-
-
-
- Decrypt with EIP-712 permit
-
-
-
-
-
- {/* ── STATS BAR ── */}
-
-
-
-
8+
-
Registered token pairs
-
-
-
2
-
Supported networks
-
-
-
FHE
-
On-chain encryption
-
-
-
ERC‑7984
-
Confidential standard
-
-
-
-
- {/* ── FEATURES ── */}
-
- Core capabilities
-
- Everything you need to go confidential.
-
-
-
-
-
registry.live()
-
🔍
-
Live Registry
-
- Every ERC-7984 wrapper pair, read directly from the on-chain
- WrappersRegistry. Mainnet and Sepolia. No snapshots, no stale data.
-
-
-
-
-
shield(amount)
-
🔒
-
One-click Shield
-
- Approve and deposit any registered ERC-20. The wrapper mints an
- encrypted cToken — your balance becomes FHE ciphertext on-chain.
-
-
-
-
-
decrypt(permit)
-
⚡
-
EIP-712 Decrypt
-
- One off-chain signature grants a session key. Zama Gateway decrypts
- your balance locally — never on-chain, never stored.
-
-
-
-
-
- {/* ── HOW IT WORKS ── */}
-
-
-
Step by step
-
- From zero to confidential in minutes.
-
-
-
-
-
1
-
Get test tokens
-
- Use the built-in Faucet to mint free mock tokens on Sepolia —
- USDC, WETH, ZAMA, and more. Up to 1,000,000 per call.
-
-
faucet.mint(cUSDCMock )
-
-
-
-
2
-
Shield your balance
-
- Select a wrapper pair, enter an amount, and shield. Your ERC-20
- is deposited and a confidential cToken is minted via FHE.
-
-
shield(1000n ) → cUSDC
-
-
-
-
3
-
Decrypt & manage
-
- Sign an EIP-712 permit to view your encrypted balance. Transfer
- or unshield anytime — your assets, your control.
-
-
decrypt(permit ) → 1,000
-
-
-
-
-
- {/* ── TOKEN GRID ── */}
-
- Registered pairs
-
- All tokens. Both networks.
-
-
-
-
-
- {/* ── CTA ── */}
-
-
- Privacy starts with
- one transaction.
-
-
- No sign-up. No KYC. No compromise.
-
-
- Open ZamaVault
-
-
-
-
- Live on Ethereum Mainnet + Sepolia Testnet
-
-
- {/* ── FOOTER ── */}
-
-
-
-
-
- Built for Zama Developer Program Season 3 · 2026
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx
deleted file mode 100644
index 64c15af..0000000
--- a/src/app/(marketing)/layout.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { Metadata } from 'next';
-import '../globals.css';
-
-export const metadata: Metadata = {
- title: 'ZamaVault — Confidential Token Interface',
- description:
- 'The most complete interface for Zama\'s confidential token ecosystem. Shield ERC-20 tokens into FHE-encrypted wrappers on Ethereum.',
-};
-
-export default function MarketingLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
-
-
-
-
-
- {children}
-
-
- );
-}
From 19861060e28c37dbfdc03bf447b4bf9891341438 Mon Sep 17 00:00:00 2001
From: hosein-ul
Date: Wed, 24 Jun 2026 11:43:29 +0300
Subject: [PATCH 22/69] docs: add comprehensive agent reference guide
(AGENT_GUIDE.md)
---
AGENT_GUIDE.md | 468 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 468 insertions(+)
create mode 100644 AGENT_GUIDE.md
diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md
new file mode 100644
index 0000000..76abe01
--- /dev/null
+++ b/AGENT_GUIDE.md
@@ -0,0 +1,468 @@
+# ZamaVault — Master Reference Document for AI Agents
+
+> این فایل یک راهنمای کامل برای هر agent هوش مصنوعی است که روی پروژه ZamaVault کار میکند.
+> شامل همه جزئیات معماری، قوانین حیاتی، اشتباهات رایج، و تجربیات کسبشده است.
+
+---
+
+## ۱. بستر پروژه
+
+**ZamaVault** یک رابط کاربری برای اکوسیستم توکن محرمانه Zama است:
+ثبتنام پویای Registry، wrap/unwrap ERC-20، رمزگشایی پورتفولیوی رمزنگاریشده، و فاست Sepolia.
+
+- **مخزن:** https://github.com/hosein-ul/zamavault
+- **شاخه:** `feat/dynamic-registry-finding-1` (PR #1 → main)
+- **Stack:** Next.js 16 (App Router, Turbopack), React 19, Wagmi 3, Viem 2, @zama-fhe/react-sdk 3.0.1, TypeScript 5
+- **هدف:** Zama Developer Program Mainnet Season 3 Bounty Track (deadline: 2026-07-07 AOE)
+- **دایرکتوری:** `C:\NEW work\`
+
+---
+
+## ۲. قوانین حیاتی — هرگز نقض نشوند
+
+### 🚨 قانون ۱: هیچ اشارهای به Claude/Anthropic
+هرگز نام Claude، Claude Code، Anthropic، یا هر چیز مشابه را در کد، commitها، PR، README، یا هر فایل قابل مشاهده توسط داوران قرار ندهید. هیچ `Co-Authored-By` header نزنید. این یک ارسال مسابقه است.
+
+### 🚨 قانون ۲: هرگز permit EIP-712 را auto-fire نکنید
+هر decrypt/permit باید پشت یک کلیک صریح کاربر باشد. الگوی `decryptRequested` در `wrap/page.tsx` دقیقاً برای این است:
+- `decryptRequested` به طور پیشفرض `false` است
+- فقط وقتی کاربر روی "Decrypt" کلیک میکند `true` میشود
+- **سورس باگ رایج:** صدا زدن `refetch()` مستقیم روی `useConfidentialBalance` — TanStack Query متد `refetch()` را از `enabled: false` bypass میکند!
+- **راهحل:** بعد از هر shield/unshield موفق، `setDecryptRequested(false)` صدا بزنید
+- Reset را **به صورت synchronous** در `onChange` کنید، نه در `useEffect` (race condition یک فریمی)
+
+### 🚨 قانون ۳: Decimal Scaling بار حیاتی دارد
+```
+wrapper decimals: همیشه 6 (محدودیت euint64 FHE)
+underlying decimals: 6 یا 18
+
+Shield (wrap): parseAmount(input, underlyingDecimals) ← دسیمال underlying
+Unshield: parseAmount(input, 6) ← همیشه 6
+Display: formatAmount(balance, 6) ← همیشه 6
+
+⚠️ اشتباه رایج: formatAmount(balance, 18) → نمایش صفر برای توکنهای 18 دسیمال
+```
+
+### 🚨 قانون ۴: هرگز secret commit نکنید
+فایلهای `.env` در `.gitignore` هستند. از `.env.example` برای مستندسازی استفاده کنید.
+
+### 🚨 قانون ۵: Blocklist را بدون تأیید تیم حذف نکنید
+`BLOCKLISTED_WRAPPERS` در `registry.ts` دارای مستندات است. `cbbqTGBP` (Mainnet) آدرس vanity مشکوک دارد.
+
+---
+
+## ۳. معماری و ساختار فایل
+
+```
+C:\NEW work\
+├── src/
+│ ├── app/
+│ │ ├── page.tsx ← جدول Registry (صفحه اصلی)
+│ │ ├── wrap/page.tsx ← Shield/Unshield (مهمترین صفحه)
+│ │ ├── portfolio/page.tsx ← پورتفولیو محرمانه با batch decrypt
+│ │ ├── faucet/page.tsx ← فاست توکن mock سپولیا
+│ │ ├── analytics/page.tsx ← داشبورد TVL و activity
+│ │ ├── learn/page.tsx ← آموزش تعاملی ۵ مرحلهای FHE
+│ │ ├── developers/page.tsx ← مولد کد snippet
+│ │ ├── docs/page.tsx ← مستندات developer
+│ │ ├── error.tsx ← مرز خطای Next.js
+│ │ ├── api/registry/route.ts ← REST API عمومی
+│ │ ├── ClientLayout.tsx ← providerهای context (theme/network)
+│ │ ├── layout.tsx ← layout ریشه با تزریق theme SSR
+│ │ └── globals.css ← سیستم طراحی (4 تم Nordic)
+│ ├── components/
+│ │ ├── ui/ ← 13 کامپوننت UI قابل استفاده مجدد
+│ │ │ ├── Badge.tsx ← ⚠️ text-transform:uppercase حذف شد (باگ fix)
+│ │ │ ├── Button.tsx ← forwardRef, variants: primary/secondary/ghost/danger
+│ │ │ ├── Card.tsx ← variants: default/glass/outlined/accent
+│ │ │ ├── CopyButton.tsx ← کپی clipboard با بازخورد چکمارک
+│ │ │ ├── Skeleton.tsx ← props: width, height, variant (نه style!)
+│ │ │ ├── Toast.tsx ← ToastProvider + useToast() hook
+│ │ │ ├── Modal.tsx
+│ │ │ ├── Tooltip.tsx ← Portal-based, HelpCircle پیشفرض trigger
+│ │ │ ├── TokenIcon.tsx ← نقشه symbol→لوگو، حذف prefix "c" و suffix "Mock"
+│ │ │ ├── BlurIn.tsx ← انیمیشن blur-in
+│ │ │ ├── TypingAnimation.tsx
+│ │ │ ├── Spinner.tsx
+│ │ │ └── TransactionSuccessModal.tsx
+│ │ ├── layout/
+│ │ │ ├── Header.tsx ← ۸ مسیر nav، wallet connect، network switcher
+│ │ │ └── Footer.tsx
+│ │ └── PendingUnshieldBanner.tsx ← recovery برای unshield قطعشده
+│ ├── config/
+│ │ ├── contracts.ts ← WrapperPair، KNOWN_WRAPPERS (fallback)، REGISTRY_ADDRESSES
+│ │ ├── chains.ts ← SupportedChainId، explorer Blockscout
+│ │ └── tokens.ts ← متادیتای نمایش توکن (لوگو، رنگ)
+│ ├── lib/
+│ │ ├── registry.ts ← useRegistryPairs، isMintablePair، blocklist
+│ │ ├── errors.ts ← classifyError با 16 کد خطا
+│ │ ├── utils.ts ← formatAmount، parseAmount، formatAddress، cn
+│ │ ├── wrapper-abi.ts ← WRAPPER_ABI، ERC20_ABI
+│ │ └── __tests__/utils.test.ts ← 30 تست Vitest
+│ └── providers/
+│ └── Providers.tsx ← Wagmi + Zama + TanStack Query (⚠️ بسیار حساس)
+├── .github/workflows/ci.yml ← TypeScript → Vitest → Build
+├── .env.example ← همه متغیرهای محیطی اختیاری هستند
+├── vitest.config.ts ← alias @/ → src/
+├── CLAUDE.md ← مستندات پروژه
+└── memory.md ← درسهای آموختهشده
+```
+
+---
+
+## ۴. چرخه SDK Zama (از دیدگاه flow)
+
+### Shield (Wrap)
+```
+useShield({ tokenAddress: erc7984Address })
+→ mutateAsync({ amount, onApprovalSubmitted, onShieldSubmitted })
+→ اگر allowance < amount: approve ERC-20 → onApprovalSubmitted(txHash) → setTxStep(2)
+→ shield tx → onShieldSubmitted(txHash) → setTxStep(4)
+→ res.txHash → setTxStep(5), setDecryptRequested(false)
+```
+
+### Unshield (Unwrap) — دو مرحلهای
+```
+useUnshield(erc7984Address) ← positional در v3.0.1
+→ mutateAsync({ amount, onUnwrapSubmitted, onFinalizing, onFinalizeSubmitted })
+→ unwrap tx on-chain → onUnwrapSubmitted(txHash) → setTxStep(4)
+→ Gateway اثبات رمزگشایی تولید میکند → onFinalizing() → toast "15-40s"
+→ finalize tx → onFinalizeSubmitted(txHash) → setTxStep(5)
+```
+
+### Decrypt Balance — PERMIT GATING
+```
+const [decryptRequested, setDecryptRequested] = useState(false)
+
+useConfidentialBalance(
+ { tokenAddress },
+ { enabled: decryptRequested && !!address }
+)
+
+// ✅ درست: فقط روی کلیک صریح
+ setDecryptRequested(true)}>Decrypt
+
+// ❌ غلط: refetch() مستقیم enabled را bypass میکند!
+refetchWrapperBalance() ← هرگز در success handler نزنید
+
+// ✅ بعد از هر tx موفق:
+setDecryptRequested(false) ← جلوگیری از refetchOnWindowFocus
+```
+
+### Batch Decrypt (Portfolio)
+```
+useConfidentialBalances(
+ { tokenAddresses: requestedAddresses },
+ { enabled: isConnected && requestedAddresses.length > 0 }
+)
+// یک EIP-712 permit، همه توکنها را decrypt میکند
+```
+
+---
+
+## ۵. الگوهای کلیدی React
+
+### الگوی تشخیص Registry
+```typescript
+// useRegistryPairs محاسبه میکند
+const isChainAligned = isConnected && chain?.id === chainId
+
+if (isChainAligned && sdkResult.data?.items?.length > 0) → live data
+else if (isChainAligned && sdkResult.isLoading) → loading + fallback
+else → KNOWN_WRAPPERS fallback (isFromCache: true)
+```
+
+### الگوی جایگزینی Tooltip
+```tsx
+// ✅ درست: ? icon جداگانه
+Confidential
+ {/* HelpCircle پیشفرض نشان میدهد */}
+
+// ❌ غلط: badge بهعنوان trigger
+Confidential
+```
+
+### اشتباه Skeleton Props
+```tsx
+// ✅ درست
+
+
+// ❌ غلط — Skeleton prop style ندارد!
+
+```
+
+### Badge — text-transform حذف شد
+```css
+/* ❌ قبلاً وجود داشت — باگ cZAMA → CZAMA */
+.badge { text-transform: uppercase; }
+
+/* ✅ الان — متن دقیقاً همانطور که هست نمایش داده میشود */
+/* text-transform حذف شد — cZAMA همیشه cZAMA میماند */
+```
+
+---
+
+## ۶. درسهای آموختهشده (از اشتباهات گذشته)
+
+### ۶.۱ باگ Auto-Permit (بارها رخ داد)
+**علت اصلی:** TanStack Query's `refetch()` از `enabled: false` bypass میکند.
+بعد از shield موفق، `refetchWrapperBalance()` صدا میزدیم → permit بدون کلیک کاربر fire میشد → کیف پول ۳ بار prompt میداد.
+**راهحل:** هرگز `refetchWrapperBalance()` در handlerهای موفقیت صدا نزنید. فقط `refetchPublicBalance()` و `refetchAllowance()` مجاز است.
+
+### ۶.۲ باگ Display صفر برای توکنهای ۱۸ دسیمال
+**علت:** `formatAmount(balance, 18)` روی موجودی FHE با ۶ دسیمال → مقدار نزدیک به صفر.
+**راهحل:** همیشه `formatAmount(balance, 6)` برای موجودیهای FHE confidential.
+
+### ۶.۳ باگ Race Condition برای Token Select
+**علت:** `setDecryptRequested(false)` فقط در `useEffect` → یک فریم delay → درخواست قدیمی `true` با آدرس توکن جدید trigger میشد.
+**راهحل:** reset را **synchronously** در `onChange` handler انجام دهید.
+
+### ۶.۴ Tooltip چهطور باید باشد
+**علت:** Tooltip کل button را wrap میکرد → کلیک روی Tooltip کار نمیکرد.
+**راهحل:** همیشه ` ` بدون children (→ ? icon) را **بعد از** button/badge قرار دهید.
+
+### ۶.۵ باگ TVL Ranking
+**علت:** Sort روی raw bigint — ZAMA با ۱۸ دسیمال raw value بزرگتری از USDC با ۶ دسیمال داشت.
+**راهحل:** `tvlHuman` (float نرمالشده) را محاسبه کنید و برای sort و عرض bar استفاده کنید.
+
+### ۶.۶ CZAMA بجای cZAMA
+**علت:** `.badge { text-transform: uppercase }` در CSS.
+**راهحل:** `text-transform` از `.badge` حذف شد. نامگذاری: همیشه lowercase `c` — `cZAMA`، `cUSDC`، `cWETH`.
+
+### ۶.۷ API Domain هاردکد
+**علت:** `zamavault.xyz` مستقیم در کد نوشته شد.
+**راهحل:** از `process.env.NEXT_PUBLIC_APP_URL` استفاده کنید. در Vercel: Settings → Environment Variables.
+
+### ۶.۸ useCallback در PendingUnshieldBanner
+**علت:** `useCallback` با `sdk?.storage` dependency مشکل react-hooks lint داشت.
+**راهحل:** از plain async functions به جای `useCallback` استفاده کنید.
+
+### ۶.۹ Unshield step indicator
+**علت:** UI همیشه Approve step نشان میداد حتی وقتی allowance کافی بود.
+**راهحل:** `setTxStep(needsApproval ? 1 : 3)` — Approve step فقط وقتی `needsApproval === true` نشان داده میشود.
+
+### ۶.۱۰ اسکرول CSS بدون `margin: auto`
+**علت:** `maxWidth: 640` بدون `margin: '... auto 0'` → متن به چپ میرفت.
+**راهحل:** همیشه `margin: 'var(--sp-3) auto 0'` برای centered containers با max-width.
+
+---
+
+## ۷. قراردادها و استانداردها
+
+### نامگذاری توکنها
+```
+ERC-20 عمومی: ZAMA، USDC، USDT، WETH، BRON، tGBP، XAUt
+ERC-7984 محرمانه: cZAMA، cUSDC، cUSDT، cWETH، cBRON، ctGBP، cXAUt
+ ↑ همیشه lowercase c
+توکنهای mock: نمایش: "ZAMA" + badge "Mock" (نه "ZAMAMock")
+```
+
+### Tooltip
+- متن کوتاه: ۱-۲ خط حداکثر
+- هیچوقت badge/button را بهعنوان trigger tooltip wrap نکنید
+- همیشه ` ` standalone (→ ? icon)
+
+### Explorer
+- **Blockscout** (نه Etherscan) — چون FHE/Zama protocol decode را پشتیبانی میکند
+- Sepolia: `https://eth-sepolia.blockscout.com`
+- Mainnet: `https://eth.blockscout.com`
+
+### Commit Messages
+- هیچ اشارهای به AI یا Claude
+- format: `fix(scope): description`، `feat: description`
+
+### CSS Variables
+```css
+--text-xs، --text-sm، --text-base، --text-lg، --text-xl، --text-2xl، --text-3xl
+--sp-1 (4px)، --sp-2 (8px)، --sp-3 (12px)، --sp-4 (16px)، --sp-6 (24px)، --sp-8 (32px)
+--accent، --success، --warning، --error، --info
+--bg-base، --bg-surface، --bg-elevated، --bg-card
+--border، --border-hover، --border-accent
+--radius-sm، --radius-md، --radius-lg، --radius-xl
+```
+
+---
+
+## ۸. SDK Zama — مرجع سریع
+
+### نسخه و API
+- **نسخه:** `@zama-fhe/react-sdk@3.0.1` — هنوز v2 TypeScript API
+- Migration guide برای ۳.۱.x است — تا وقتی از `@^3.0` استفاده میکنیم، نیازی به migration نیست
+- `WagmiSigner`، `RelayerWeb`، `indexedDBStorage` هنوز در v3.0.1 export میشوند
+
+### Hookهای مهم
+| Hook | Package | نحوه فراخوانی |
+|------|---------|-------------|
+| `useListPairs({ page, pageSize, metadata })` | react-sdk | config object |
+| `useShield({ tokenAddress })` | react-sdk | config object |
+| `useUnshield(tokenAddress)` | react-sdk | positional در v3! |
+| `useConfidentialBalance({ tokenAddress }, { enabled })` | react-sdk | 2 arg |
+| `useConfidentialBalances({ tokenAddresses }, { enabled })` | react-sdk | 2 arg |
+| `useResumeUnshield({ tokenAddress })` | react-sdk | config object |
+| `useRevokeSession()` | react-sdk | no args |
+| `useZamaSDK()` | react-sdk | SDK instance |
+| `loadPendingUnshield(storage, addr)` | react-sdk | function |
+| `clearPendingUnshield(storage, addr)` | react-sdk | function |
+| `matchZamaError(err, handlers)` | @zama-fhe/sdk | function |
+
+### کدهای خطا
+`SIGNING_REJECTED`، `SIGNING_FAILED`، `ENCRYPTION_FAILED`، `DECRYPTION_FAILED`، `TRANSACTION_REVERTED`، `INVALID_KEYPAIR`، `KEYPAIR_EXPIRED`، `NO_CIPHERTEXT`، `RELAYER_REQUEST_FAILED`، `CONFIGURATION`، `INSUFFICIENT_CONFIDENTIAL_BALANCE`، `INSUFFICIENT_ERC20_BALANCE`، `BALANCE_CHECK_UNAVAILABLE`، `ERC20_READ_FAILED`، `ACL_PAUSED`، `APPROVAL_FAILED`
+
+---
+
+## ۹. آدرسهای قرارداد
+
+### WrappersRegistry
+- Sepolia: `0x2f0750Bbb0A246059d80e94c454586a7F27a128e`
+- Mainnet: `0xeb5015fF021DB115aCe010f23F55C2591059bBA0`
+
+### Sepolia — ۸ pair (۷ mock + ۱ restricted)
+| Symbol | ERC-20 | ERC-7984 | Decimals |
+|--------|--------|----------|---------|
+| USDC | 0x9b5C...FfFF | 0x7c5B...3639 | 6/6 |
+| USDT | 0xa7dA...b0 | 0x4E7B...491 | 6/6 |
+| WETH | 0xff54...F3F | 0x4620...158 | 18/6 |
+| ZAMA | 0x7535...F57 | 0xf2D6...bFB | 18/6 |
+| BRON | 0xFf02...E | 0xaa56...891 | 18/6 |
+| tGBP | 0x93c9...442 | 0xfCE5...7CC | 18/6 |
+| XAUt | 0x2437...940 | 0xe4Fc...0C7 | 6/6 |
+| ctGBP (restricted) | 0x167D...A208 | — | 18/6 |
+
+### Mainnet — ۷ pair + ۱ blocklisted
+| Symbol | ERC-20 | ERC-7984 | Decimals |
+|--------|--------|----------|---------|
+| USDC | 0xa0b8...48 | 0xe978...2B2 | 6/6 |
+| USDT | 0xdAC1...c7 | 0xAe02...c50 | 6/6 |
+| WETH | 0xc02a...c2 | 0xda93...893 | 18/6 |
+| ZAMA | 0xA12C...A3 | 0x80CB...071 | 18/6 |
+| BRON | 0xBA2C...83 | 0x85dE...bc | 18/6 |
+| tGBP | 0x27f6...87 | 0xa873...DD9 | 18/6 |
+| XAUt | 0x6874...38 | 0x73cc...Ef1 | 6/6 |
+| cbbqTGBP | **BLOCKLISTED** | آدرس vanity مشکوک | — |
+
+---
+
+## ۱۰. صفحات و routing
+
+| مسیر | فایل | توضیح |
+|------|------|-------|
+| `/` | `app/page.tsx` | جدول Registry با موجودیهای live |
+| `/wrap` | `app/wrap/page.tsx` | Shield/Unshield با query `?token=SYMBOL&action=wrap` |
+| `/portfolio` | `app/portfolio/page.tsx` | batch decrypt، activity feed |
+| `/faucet` | `app/faucet/page.tsx` | فقط Sepolia، فقط mock tokenها |
+| `/analytics` | `app/analytics/page.tsx` | TVL، ratio، activity (24h) |
+| `/learn` | `app/learn/page.tsx` | ۵ مرحله تعاملی |
+| `/developers` | `app/developers/page.tsx` | snippet generator |
+| `/docs` | `app/docs/page.tsx` | مستندات کامل |
+| `/api/registry` | `app/api/registry/route.ts` | `?chain=sepolia\|mainnet` |
+
+**Nav items در Header (به ترتیب):**
+Registry → Wrap → Portfolio → Faucet (TESTNET) → Learn → Dev Tools → Analytics → Docs
+
+---
+
+## ۱۱. متغیرهای محیطی
+
+همه اختیاری هستند — app fallback عمومی دارد:
+
+```env
+NEXT_PUBLIC_SEPOLIA_RPC= # Alchemy یا RPC دیگر (پیشفرض: publicnode)
+NEXT_PUBLIC_MAINNET_RPC= # Alchemy یا RPC دیگر (پیشفرض: publicnode)
+NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # اگر نبود، فقط injected wallets
+NEXT_PUBLIC_APP_URL= # URL deployment برای docs/API (مثال: https://zamavault.vercel.app)
+```
+
+**تنظیم در Vercel:** Settings → Environment Variables → Add → Deploy دوباره
+
+---
+
+## ۱۲. CI/CD و دستورات
+
+```bash
+# توسعه
+npm run dev # سرور dev روی localhost:3000
+
+# بررسی کیفیت
+npm test # Vitest — 30 تست
+npx tsc --noEmit # TypeScript type-check (باید clean باشد)
+npm run lint # ESLint (advisory — برخی خطاهای pre-existing وجود دارند)
+
+# تولید
+npx next build # باید موفق باشد
+```
+
+**GitHub Actions:** Push به `main` یا `feat/**` → TypeScript → Vitest → Next build
+ESLint advisory است (نه blocking) — برخی خطاهای pre-existing در AUDIT_REPORT.md ردیابی شدهاند.
+
+---
+
+## ۱۳. الگوهای Error Handling
+
+### در هر catch block
+```typescript
+} catch (err: unknown) { // ← نه err: any
+ const classified = classifyError(err);
+ addToast({
+ variant: classified.retryable ? 'warning' : 'error',
+ title: classified.title,
+ message: classified.message,
+ });
+}
+```
+
+### در REST API
+```typescript
+} catch (err) {
+ // fallback به KNOWN_WRAPPERS
+ return NextResponse.json({ pairs: fallback, source: 'cached-snapshot', warning: '...' })
+}
+```
+
+---
+
+## ۱۴. تمها و طراحی
+
+**۴ تم dark Nordic:**
+- Charcoal (پیشفرض): accent `#38bdf8` (sky blue)
+- Midnight: accent `#f4f4f5` (white)
+- Frost: accent `#60a5fa` (ice blue)
+- Aurora: accent `#2dd4bf` (teal)
+
+**Light mode:** accent `#09090b` (black/inverse)
+
+**فونتها:**
+- Sans: Plus Jakarta Sans
+- Mono: JetBrains Mono
+
+---
+
+## ۱۵. کارهای باقیمانده
+
+### کاربر باید انجام دهد
+- D1: Deploy Vercel + تنظیم `NEXT_PUBLIC_APP_URL`
+- D2: Mainnet Relayer API key (اختیاری، برای بهتر شدن UX)
+
+### کارهای آینده
+- Phase 2.2: npm package `@zamavault/sdk`
+- Phase 5.1: README rewrite کامل
+- Phase 4.4: تست mobile responsive در 360px
+
+---
+
+## ۱۶. URL های مهم مستندات Zama
+
+- SDK overview: https://docs.zama.org/protocol/sdk/overview
+- useShield: https://docs.zama.org/protocol/sdk/api-references/react/useshield
+- useUnshield: https://docs.zama.org/protocol/sdk/api-references/react/useunshield
+- useConfidentialBalance: https://docs.zama.org/protocol/sdk/api-references/react/useconfidentialbalance
+- useResumeUnshield: https://docs.zama.org/protocol/sdk/api-references/react/useresumeunshield
+- matchZamaError/errors: https://docs.zama.org/protocol/sdk/api-references/sdk/errors
+- WrappersRegistry: https://docs.zama.org/protocol/sdk/api-references/sdk/wrappersregistry
+- Sepolia addresses: https://docs.zama.org/protocol/protocol-apps/addresses/testnet/sepolia
+- Mainnet addresses: https://docs.zama.org/protocol/protocol-apps/addresses/mainnet/ethereum
+- Migration v2→v3: https://docs.zama.org/protocol/sdk/migration/migrate-v2-to-v3
+- Relayer API keys: https://docs.zama.org/protocol/sdk/guides/authentication
+
+---
+
+*آخرین بروزرسانی: 2026-06-24 | branch: feat/dynamic-registry-finding-1*
From f433ab4356046648471c3b982fb5abba6898a81f Mon Sep 17 00:00:00 2001
From: hosein-ul
Date: Thu, 25 Jun 2026 15:08:18 +0300
Subject: [PATCH 23/69] feat: landing page at /, app moved to /app/*, Phase A
polish
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Architecture
- Move the application from / to a /app/* subtree so the marketing
landing can live independently at /. Page renames preserve history.
- Two design systems, hermetically separated:
* src/app/layout.tsx is now CSS-free at the root (only fonts +
theme bootstrap).
* src/app/page.tsx imports landing.css (Tailwind v4 + @theme tokens).
* src/app/app/layout.tsx imports globals.css and wraps ClientLayout.
- Tailwind v4 (@tailwindcss/postcss) + Magic UI primitives installed
under src/components/magic/.
- New landing sections under src/components/landing/ (hero, features,
trust rail, stats, how-it-works, FHE explainer, final CTA, footer).
- Fonts: Fraunces (display) + Plus Jakarta Sans (body) via next/font.
- All internal links rewritten to the /app/* prefix.
- src/lib/utils.ts cn() upgraded to clsx + twMerge for shadcn compat.
Wrap page polish
- Primary Shield/Unshield button is now always in the action slot;
tx-info card and gateway-delay warning render BELOW it (never replace).
- Card max-width widened to 580px so all sections stay visible during
the transaction state.
- Pulse-gold border halo while txStep is in flight (1..4) via the new
.swap-card-pending rule + @keyframes in globals.css.
- Approval flow: pass approvalStrategy "skip" when existing allowance
is already sufficient, "exact" when not. Avoids the SDK's default
approve(0) + approve(amount) double-prompt on every shield.
Landing polish
- Features grid is now a clean 3×2 with no row-span / no orphan tile.
MagicCard orb softened to 0.45 opacity / 260 size.
- Hero text reveal: three single TextAnimate lines at default Magic UI
timing (by="word"), wrapped in a motion.div opacity safety fade so
content reads instantly even on slow devices.
- BFCache recovery: new client component reloads
the page on `pageshow` with event.persisted === true. Fixes the
invisible-content bug after returning via browser back from an
external link.
Analytics + Learn + misc.
- Analytics: rename TVL → TVS (Total Value Shielded), labels switched
to "last 24h", block lookback now calculated from 00:00 UTC today.
- Learn page: confetti on tutorial completion (only after the explicit
"Mark as Done" on step 5), Go-To buttons resized down so they don't
outweigh nav buttons.
---
.claude/launch.json | 9 +-
components.json | 24 +
package-lock.json | 2561 +++++++++++++++--
package.json | 9 +
postcss.config.mjs | 14 +
src/app/{ => app}/analytics/page.tsx | 33 +-
src/app/{ => app}/developers/page.tsx | 0
src/app/{ => app}/docs/page.tsx | 2 +-
src/app/{ => app}/faucet/page.tsx | 0
src/app/app/layout.tsx | 16 +
src/app/{ => app}/learn/page.tsx | 116 +-
src/app/app/page.tsx | 455 +++
src/app/{ => app}/portfolio/page.tsx | 4 +-
src/app/{ => app}/wrap/page.tsx | 152 +-
src/app/error.tsx | 2 +-
src/app/globals.css | 28 +
src/app/landing.css | 159 +
src/app/layout.tsx | 53 +-
src/app/page.tsx | 488 +---
src/components/landing/aurora-bg.tsx | 38 +
src/components/landing/bfcache-recovery.tsx | 32 +
src/components/landing/features.tsx | 166 ++
src/components/landing/fhe-explainer.tsx | 89 +
src/components/landing/final-cta.tsx | 69 +
src/components/landing/footer.tsx | 107 +
src/components/landing/hero.tsx | 121 +
src/components/landing/how-it-works.tsx | 79 +
src/components/landing/nav.tsx | 55 +
src/components/landing/stats.tsx | 38 +
src/components/landing/trust-rail.tsx | 60 +
src/components/layout/Header.tsx | 18 +-
.../magic/animated-grid-pattern.tsx | 177 ++
src/components/magic/aurora-text.tsx | 43 +
src/components/magic/bento-grid.tsx | 109 +
src/components/magic/blur-fade.tsx | 94 +
src/components/magic/border-beam.tsx | 107 +
src/components/magic/button.tsx | 64 +
src/components/magic/dot-pattern.tsx | 158 +
src/components/magic/magic-card.tsx | 222 ++
src/components/magic/marquee.tsx | 74 +
src/components/magic/number-ticker.tsx | 74 +
src/components/magic/shimmer-button.tsx | 96 +
src/components/magic/shiny-button.tsx | 74 +
src/components/magic/text-animate.tsx | 445 +++
src/components/magic/typing-animation.tsx | 236 ++
src/components/magic/warp-background.tsx | 152 +
src/lib/utils.ts | 18 +-
47 files changed, 6334 insertions(+), 806 deletions(-)
create mode 100644 components.json
create mode 100644 postcss.config.mjs
rename src/app/{ => app}/analytics/page.tsx (96%)
rename src/app/{ => app}/developers/page.tsx (100%)
rename src/app/{ => app}/docs/page.tsx (99%)
rename src/app/{ => app}/faucet/page.tsx (100%)
create mode 100644 src/app/app/layout.tsx
rename src/app/{ => app}/learn/page.tsx (90%)
create mode 100644 src/app/app/page.tsx
rename src/app/{ => app}/portfolio/page.tsx (99%)
rename src/app/{ => app}/wrap/page.tsx (96%)
create mode 100644 src/app/landing.css
create mode 100644 src/components/landing/aurora-bg.tsx
create mode 100644 src/components/landing/bfcache-recovery.tsx
create mode 100644 src/components/landing/features.tsx
create mode 100644 src/components/landing/fhe-explainer.tsx
create mode 100644 src/components/landing/final-cta.tsx
create mode 100644 src/components/landing/footer.tsx
create mode 100644 src/components/landing/hero.tsx
create mode 100644 src/components/landing/how-it-works.tsx
create mode 100644 src/components/landing/nav.tsx
create mode 100644 src/components/landing/stats.tsx
create mode 100644 src/components/landing/trust-rail.tsx
create mode 100644 src/components/magic/animated-grid-pattern.tsx
create mode 100644 src/components/magic/aurora-text.tsx
create mode 100644 src/components/magic/bento-grid.tsx
create mode 100644 src/components/magic/blur-fade.tsx
create mode 100644 src/components/magic/border-beam.tsx
create mode 100644 src/components/magic/button.tsx
create mode 100644 src/components/magic/dot-pattern.tsx
create mode 100644 src/components/magic/magic-card.tsx
create mode 100644 src/components/magic/marquee.tsx
create mode 100644 src/components/magic/number-ticker.tsx
create mode 100644 src/components/magic/shimmer-button.tsx
create mode 100644 src/components/magic/shiny-button.tsx
create mode 100644 src/components/magic/text-animate.tsx
create mode 100644 src/components/magic/typing-animation.tsx
create mode 100644 src/components/magic/warp-background.tsx
diff --git a/.claude/launch.json b/.claude/launch.json
index c380071..9f93da6 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -5,13 +5,14 @@
"name": "ZamaVault Dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
- "port": 3000
+ "port": 3000,
+ "autoPort": true
},
{
"name": "ZamaVault Production Preview",
- "runtimeExecutable": "npm",
- "runtimeArgs": ["run", "start"],
- "port": 3000
+ "runtimeExecutable": "npx",
+ "runtimeArgs": ["next", "start", "-p", "3030"],
+ "port": 3030
}
]
}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..58eed5f
--- /dev/null
+++ b/components.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/app/landing.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/magic",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {
+ "@magicui": "https://magicui.design/r/{name}"
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 9b7d833..b44ca42 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,24 +8,32 @@
"name": "zamavault",
"version": "0.1.0",
"dependencies": {
+ "@radix-ui/react-icons": "^1.3.2",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@tanstack/react-query": "^5.101.0",
"@zama-fhe/react-sdk": "^3.0.1",
"canvas-confetti": "^1.9.4",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"d3-delaunay": "^6.0.4",
"framer-motion": "^12.41.0",
"lucide-react": "^1.18.0",
+ "motion": "^12.41.0",
"next": "16.2.9",
+ "next-themes": "^0.4.6",
+ "radix-ui": "^1.6.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-icons": "^5.6.0",
"simplex-noise": "^4.0.3",
+ "tailwind-merge": "^3.6.0",
"three": "^0.184.0",
"viem": "^2.52.2",
"wagmi": "^3.6.16"
},
"devDependencies": {
+ "@tailwindcss/postcss": "^4.3.1",
"@types/canvas-confetti": "^1.9.0",
"@types/d3-delaunay": "^6.0.4",
"@types/node": "^20",
@@ -34,6 +42,7 @@
"@types/three": "^0.184.1",
"eslint": "^9",
"eslint-config-next": "16.2.9",
+ "tailwindcss": "^4.3.1",
"typescript": "^5",
"vitest": "^4.1.9"
}
@@ -44,6 +53,19 @@
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
"license": "MIT"
},
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -476,6 +498,44 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1396,192 +1456,1694 @@
"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==",
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz",
+ "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz",
+ "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.10.tgz",
+ "integrity": "sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ==",
"license": "MIT",
"dependencies": {
- "@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"
+ "@radix-ui/react-visually-hidden": "1.2.6"
},
"peerDependencies": {
- "@react-three/fiber": "^9.0.0",
- "react": "^19",
- "react-dom": "^19",
- "three": ">=0.159"
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
- "react-dom": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/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==",
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.14.tgz",
+ "integrity": "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==",
"license": "MIT",
"dependencies": {
- "@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"
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collapsible": "1.1.14",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
},
"peerDependencies": {
- "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"
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
- "expo": {
+ "@types/react": {
"optional": true
},
- "expo-asset": {
+ "@types/react-dom": {
"optional": true
- },
- "expo-file-system": {
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.17.tgz",
+ "integrity": "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dialog": "1.1.17",
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
"optional": true
},
- "expo-gl": {
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz",
+ "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
"optional": true
},
- "react-dom": {
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.10.tgz",
+ "integrity": "sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
"optional": true
},
- "react-native": {
+ "@types/react-dom": {
"optional": true
}
}
},
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
- "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.0.tgz",
+ "integrity": "sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
- "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.5.tgz",
+ "integrity": "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==",
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
- "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz",
+ "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==",
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
- "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz",
+ "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==",
"license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
- "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz",
+ "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
- "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz",
+ "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==",
"license": "MIT",
- "optional": true,
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.1.tgz",
+ "integrity": "sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-menu": "2.1.18",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz",
+ "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz",
+ "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz",
+ "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.18.tgz",
+ "integrity": "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.18",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
+ "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz",
+ "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.10.tgz",
+ "integrity": "sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-label": "2.1.10",
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.17.tgz",
+ "integrity": "sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-icons": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+ "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
+ "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.10.tgz",
+ "integrity": "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.18.tgz",
+ "integrity": "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.18.tgz",
+ "integrity": "sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.18",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.16.tgz",
+ "integrity": "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.10.tgz",
+ "integrity": "sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.5.tgz",
+ "integrity": "sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-is-hydrated": "0.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz",
+ "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz",
+ "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-rect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz",
+ "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz",
+ "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
+ "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.10.tgz",
+ "integrity": "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.1.tgz",
+ "integrity": "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz",
+ "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz",
+ "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz",
+ "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.6",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.10.tgz",
+ "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.1.tgz",
+ "integrity": "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
+ "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.1.tgz",
+ "integrity": "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz",
+ "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.17.tgz",
+ "integrity": "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.12.tgz",
+ "integrity": "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.13.tgz",
+ "integrity": "sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-toggle": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.13.tgz",
+ "integrity": "sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-separator": "1.1.10",
+ "@radix-ui/react-toggle-group": "1.1.13"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz",
+ "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-visually-hidden": "1.2.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
+ "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz",
+ "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz",
+ "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz",
+ "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz",
+ "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz",
+ "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz",
+ "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz",
+ "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz",
+ "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz",
+ "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
+ "license": "MIT"
+ },
+ "node_modules/@react-three/drei": {
+ "version": "10.7.7",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
+ "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mediapipe/tasks-vision": "0.10.17",
+ "@monogrid/gainmap-js": "^3.0.6",
+ "@use-gesture/react": "^10.3.1",
+ "camera-controls": "^3.1.0",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.56",
+ "glsl-noise": "^0.0.0",
+ "hls.js": "^1.5.17",
+ "maath": "^0.10.8",
+ "meshline": "^3.3.1",
+ "stats-gl": "^2.2.8",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.8.3",
+ "three-stdlib": "^2.35.6",
+ "troika-three-text": "^0.52.4",
+ "tunnel-rat": "^0.1.2",
+ "use-sync-external-store": "^1.4.0",
+ "utility-types": "^3.11.0",
+ "zustand": "^5.0.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": "^9.0.0",
+ "react": "^19",
+ "react-dom": "^19",
+ "three": ">=0.159"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz",
+ "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^2.0.0",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.27.0",
+ "suspend-react": "^0.1.3",
+ "use-sync-external-store": "^1.4.0",
+ "zustand": "^5.0.3"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=19 <19.3",
+ "react-dom": ">=19 <19.3",
+ "react-native": ">=0.78",
+ "three": ">=0.156"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
"os": [
"linux"
],
@@ -1789,51 +3351,363 @@
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
- "funding": {
- "url": "https://paulmillr.com/funding/"
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+ "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/curves": "~1.9.0",
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip39": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+ "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
}
},
- "node_modules/@scure/bip32": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
- "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@noble/curves": "~1.9.0",
- "@noble/hashes": "~1.8.0",
- "@scure/base": "~1.2.5"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
}
},
- "node_modules/@scure/bip39": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
- "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.1.tgz",
+ "integrity": "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@noble/hashes": "~1.8.0",
- "@scure/base": "~1.2.5"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.3.1",
+ "@tailwindcss/oxide": "4.3.1",
+ "postcss": "8.5.15",
+ "tailwindcss": "4.3.1"
}
},
- "node_modules/@standard-schema/spec": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
- "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "node_modules/@tailwindcss/postcss/node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/@swc/helpers": {
- "version": "0.5.15",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
- "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
- "license": "Apache-2.0",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
"dependencies": {
- "tslib": "^2.8.0"
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
}
},
"node_modules/@tanstack/query-core": {
@@ -1967,7 +3841,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -2954,6 +4828,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -3447,12 +5333,33 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3697,6 +5604,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3745,6 +5658,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -4777,6 +6704,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -4884,6 +6820,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -5581,6 +7524,16 @@
"react": "^19.0.0"
}
},
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6183,6 +8136,32 @@
}
}
},
+ "node_modules/motion": {
+ "version": "12.41.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.41.0.tgz",
+ "integrity": "sha512-avEDKE22rFPJqDr3Ttk7gMQpeaOmNik60NoJ5T0tj+RBCNvz21D3ArY3l4uitoeQ7eIpDqueWaO3pPYFv8JOVA==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.41.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/motion-dom": {
"version": "12.41.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.41.0.tgz",
@@ -6299,6 +8278,16 @@
}
}
},
+ "node_modules/next-themes": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
+ "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/node-addon-api": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
@@ -6768,6 +8757,83 @@
],
"license": "MIT"
},
+ "node_modules/radix-ui": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.0.tgz",
+ "integrity": "sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-accessible-icon": "1.1.10",
+ "@radix-ui/react-accordion": "1.2.14",
+ "@radix-ui/react-alert-dialog": "1.1.17",
+ "@radix-ui/react-arrow": "1.1.10",
+ "@radix-ui/react-aspect-ratio": "1.1.10",
+ "@radix-ui/react-avatar": "1.2.0",
+ "@radix-ui/react-checkbox": "1.3.5",
+ "@radix-ui/react-collapsible": "1.1.14",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-context-menu": "2.3.1",
+ "@radix-ui/react-dialog": "1.1.17",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-dropdown-menu": "2.1.18",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-form": "0.1.10",
+ "@radix-ui/react-hover-card": "1.1.17",
+ "@radix-ui/react-label": "2.1.10",
+ "@radix-ui/react-menu": "2.1.18",
+ "@radix-ui/react-menubar": "1.1.18",
+ "@radix-ui/react-navigation-menu": "1.2.16",
+ "@radix-ui/react-one-time-password-field": "0.1.10",
+ "@radix-ui/react-password-toggle-field": "0.1.5",
+ "@radix-ui/react-popover": "1.1.17",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-progress": "1.1.10",
+ "@radix-ui/react-radio-group": "1.4.1",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-scroll-area": "1.2.12",
+ "@radix-ui/react-select": "2.3.1",
+ "@radix-ui/react-separator": "1.1.10",
+ "@radix-ui/react-slider": "1.4.1",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-switch": "1.3.1",
+ "@radix-ui/react-tabs": "1.1.15",
+ "@radix-ui/react-toast": "1.2.17",
+ "@radix-ui/react-toggle": "1.1.12",
+ "@radix-ui/react-toggle-group": "1.1.13",
+ "@radix-ui/react-toolbar": "1.1.13",
+ "@radix-ui/react-tooltip": "1.2.10",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-escape-keydown": "1.1.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -6805,6 +8871,75 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-use-measure": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
@@ -7591,6 +9726,37 @@
"react": ">=17.0"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
"node_modules/tfhe": {
"version": "1.4.0-alpha.3",
"resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz",
@@ -8077,6 +10243,49 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
diff --git a/package.json b/package.json
index 64e45a7..1d38274 100644
--- a/package.json
+++ b/package.json
@@ -10,24 +10,32 @@
"test": "vitest run"
},
"dependencies": {
+ "@radix-ui/react-icons": "^1.3.2",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@tanstack/react-query": "^5.101.0",
"@zama-fhe/react-sdk": "^3.0.1",
"canvas-confetti": "^1.9.4",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"d3-delaunay": "^6.0.4",
"framer-motion": "^12.41.0",
"lucide-react": "^1.18.0",
+ "motion": "^12.41.0",
"next": "16.2.9",
+ "next-themes": "^0.4.6",
+ "radix-ui": "^1.6.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-icons": "^5.6.0",
"simplex-noise": "^4.0.3",
+ "tailwind-merge": "^3.6.0",
"three": "^0.184.0",
"viem": "^2.52.2",
"wagmi": "^3.6.16"
},
"devDependencies": {
+ "@tailwindcss/postcss": "^4.3.1",
"@types/canvas-confetti": "^1.9.0",
"@types/d3-delaunay": "^6.0.4",
"@types/node": "^20",
@@ -36,6 +44,7 @@
"@types/three": "^0.184.1",
"eslint": "^9",
"eslint-config-next": "16.2.9",
+ "tailwindcss": "^4.3.1",
"typescript": "^5",
"vitest": "^4.1.9"
}
diff --git a/postcss.config.mjs b/postcss.config.mjs
new file mode 100644
index 0000000..f39673a
--- /dev/null
+++ b/postcss.config.mjs
@@ -0,0 +1,14 @@
+/**
+ * PostCSS config — required by Tailwind v4.
+ *
+ * Tailwind only emits utilities/preflight for stylesheets that explicitly
+ * include `@import "tailwindcss";`. Our app stylesheet (globals.css) does NOT
+ * import tailwind, so it passes through untouched. Only landing.css opts in.
+ */
+const config = {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+};
+
+export default config;
diff --git a/src/app/analytics/page.tsx b/src/app/app/analytics/page.tsx
similarity index 96%
rename from src/app/analytics/page.tsx
rename to src/app/app/analytics/page.tsx
index 0fdaadd..de8a8b3 100644
--- a/src/app/analytics/page.tsx
+++ b/src/app/app/analytics/page.tsx
@@ -319,9 +319,17 @@ function RatioBar({ shields, unshields }: { shields: number; unshields: number }
/* ─── Main page ──────────────────────────────────────────────────────────────── */
-const BLOCK_LOOKBACK = 7200n; // 24 hours on Sepolia/Mainnet (12s blocks × 7200 = 86400s)
const BLOCK_TIME_MS = 12_000; // ~12 seconds per block
+function getBlocksSinceMidnightUTC(): bigint {
+ const now = Date.now();
+ const midnightUTC = new Date();
+ midnightUTC.setUTCHours(0, 0, 0, 0);
+ const msSinceMidnight = now - midnightUTC.getTime();
+ const blocks = Math.ceil(msSinceMidnight / BLOCK_TIME_MS);
+ return BigInt(Math.max(blocks, 1));
+}
+
export default function AnalyticsPage() {
const { activeChainId, isTestnet } = useActiveNetwork();
const { pairs } = useRegistryPairs(activeChainId);
@@ -344,8 +352,9 @@ export default function AnalyticsPage() {
try {
const latestBlock = await client.getBlockNumber();
const fetchTimestamp = Date.now();
- const fromBlock = latestBlock > BLOCK_LOOKBACK
- ? latestBlock - BLOCK_LOOKBACK
+ const blockLookback = getBlocksSinceMidnightUTC();
+ const fromBlock = latestBlock > blockLookback
+ ? latestBlock - blockLookback
: 0n;
const tokenResults = await Promise.all(
@@ -515,7 +524,7 @@ export default function AnalyticsPage() {
{lastUpdated
? `Updated ${lastUpdated.toLocaleTimeString()}`
: 'Loading…'}
- · Last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks (~24h)
+ · Since 00:00 UTC today
· Showing up to 25 recent events
}
- label="Shields (last 17h)"
+ label="Shields (last 24h)"
value={isLoading && tvlData.length === 0 ? '…' : totalShields.toLocaleString()}
color="var(--success)"
loading={isLoading && tvlData.length === 0}
/>
}
- label="Unshields (last 17h)"
+ label="Unshields (last 24h)"
value={isLoading && tvlData.length === 0 ? '…' : totalUnshields.toLocaleString()}
color="var(--warning)"
loading={isLoading && tvlData.length === 0}
@@ -597,11 +606,11 @@ export default function AnalyticsPage() {
{mostActive.symbol}
{(mostActive.shieldCount + mostActive.unshieldCount).toLocaleString()} txs ·{' '}
- TVL {mostActive.tvlCompact}
+ TVS {mostActive.tvlCompact}
- #{tvlData.indexOf(mostActive) + 1} TVL rank
+ #{tvlData.indexOf(mostActive) + 1} TVS rank
) : (
@@ -626,7 +635,7 @@ export default function AnalyticsPage() {
}}
>
- TVL by Token
+ TVS by Token
{isLoading && tvlData.length === 0 ? (
@@ -646,7 +655,7 @@ export default function AnalyticsPage() {
)}
- TVL = underlying ERC-20 held by wrapper. Bar = relative share.
+ TVS (Total Value Shielded) = underlying ERC-20 held by wrapper. Bar = relative share.
↑ shield count ·{' '}
↓ unshield count · vol = period volume
@@ -673,7 +682,7 @@ export default function AnalyticsPage() {
)}
- Latest shield & unshield events across all tokens · last ~24h · up to 25 shown
+ Latest shield & unshield events across all tokens · since 00:00 UTC · up to 25 shown
{isLoading && activity.length === 0 ? (
@@ -682,7 +691,7 @@ export default function AnalyticsPage() {
) : activity.length === 0 ? (
- No shield/unshield events in the last {Number(BLOCK_LOOKBACK).toLocaleString()} blocks.
+ No shield/unshield events since 00:00 UTC today.
) : (
diff --git a/src/app/developers/page.tsx b/src/app/app/developers/page.tsx
similarity index 100%
rename from src/app/developers/page.tsx
rename to src/app/app/developers/page.tsx
diff --git a/src/app/docs/page.tsx b/src/app/app/docs/page.tsx
similarity index 99%
rename from src/app/docs/page.tsx
rename to src/app/app/docs/page.tsx
index 6d19eea..b5fd9f5 100644
--- a/src/app/docs/page.tsx
+++ b/src/app/app/docs/page.tsx
@@ -1132,7 +1132,7 @@ try {
REST API (Sepolia)
-
+
Interactive Tutorial
diff --git a/src/app/faucet/page.tsx b/src/app/app/faucet/page.tsx
similarity index 100%
rename from src/app/faucet/page.tsx
rename to src/app/app/faucet/page.tsx
diff --git a/src/app/app/layout.tsx b/src/app/app/layout.tsx
new file mode 100644
index 0000000..072b6df
--- /dev/null
+++ b/src/app/app/layout.tsx
@@ -0,0 +1,16 @@
+import '../globals.css';
+import ClientLayout from '../ClientLayout';
+
+/**
+ * App layout. Owns the application's design system + provider chain.
+ *
+ * - imports globals.css so it is bundled ONLY for /app/* routes
+ * - wraps everything in ClientLayout (Wagmi, ZamaSDK, theme/network ctx,
+ * toast provider, Header, Footer)
+ *
+ * The landing page at / has no globals.css and no ClientLayout, which keeps
+ * the two design systems hermetically separated.
+ */
+export default function AppLayout({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}
diff --git a/src/app/learn/page.tsx b/src/app/app/learn/page.tsx
similarity index 90%
rename from src/app/learn/page.tsx
rename to src/app/app/learn/page.tsx
index e2c895b..75e0dd0 100644
--- a/src/app/learn/page.tsx
+++ b/src/app/app/learn/page.tsx
@@ -1,12 +1,13 @@
'use client';
-import React, { useState } from 'react';
+import React, { useState, useRef, useEffect, useCallback } from 'react';
import Link from 'next/link';
import Card from '@/components/ui/Card';
import Badge from '@/components/ui/Badge';
import Button from '@/components/ui/Button';
import Tooltip from '@/components/ui/Tooltip';
import BlurIn from '@/components/ui/BlurIn';
+import confetti from 'canvas-confetti';
import {
BookOpen,
Shield,
@@ -21,6 +22,7 @@ import {
Cpu,
ArrowRight,
Sparkles,
+ PartyPopper,
} from 'lucide-react';
/* ─── Step definitions ──────────────────────────────────────────────────────── */
@@ -209,10 +211,10 @@ function StepFaucet() {
-
-
- Go to Faucet
-
+
+
+ Go to Faucet
+
@@ -224,7 +226,8 @@ function StepShield() {
return (
- Shielding (also called "wrapping") converts your public
+ Shielding {' '}
+ (also called "wrapping") converts your public
ERC-20 tokens into confidential ERC-7984 tokens. Your balance becomes
encrypted on-chain — invisible to everyone except you.
@@ -282,10 +285,10 @@ function StepShield() {
-
-
- Go to Wrap / Unwrap
-
+
+
+ Go to Wrap / Unwrap
+
@@ -297,8 +300,8 @@ function StepDecrypt() {
return (
- Your confidential balance is encrypted on-chain. To view it, you need to
- sign an EIP-712 permit — a typed off-chain signature
+ Your confidential balance is encrypted on-chain. To view it, you need to{' '}
+ sign an EIP-712 permit — a typed off-chain signature
that authorizes the Zama Gateway to decrypt your balance and return the
plaintext to your browser.
@@ -314,8 +317,8 @@ function StepDecrypt() {
title="Sign the EIP-712 permit in your wallet"
description={
<>
- Your wallet will show a typed data signature request. This creates a
-
temporary session key that the Zama Gateway uses to
+ Your wallet will show a typed data signature request. This creates a{' '}
+
temporary session key that the Zama Gateway uses to
decrypt.
Your private key never leaves your wallet.
>
}
@@ -359,7 +362,8 @@ function StepUnshield() {
return (
- Unshielding (also called "unwrapping") converts your
+ Unshielding {' '}
+ (also called "unwrapping") converts your
confidential ERC-7984 tokens back into public ERC-20 tokens. This is a
two-step process: an on-chain request followed by finalization.
@@ -417,16 +421,16 @@ function StepUnshield() {
-
-
- Go to Wrap / Unwrap
-
+
+
+ Go to Wrap / Unwrap
+
-
-
+
+
View Portfolio
-
+
@@ -493,7 +497,7 @@ function InstructionStep({
return (
{number}
-
+
{title}
@@ -510,31 +514,58 @@ function InstructionStep({
export default function LearnPage() {
const [activeStep, setActiveStep] = useState(1);
const [completedSteps, setCompletedSteps] = useState
>(new Set());
+ const hasConfettiFired = useRef(false);
const currentStep = STEPS.find((s) => s.id === activeStep)!;
const goTo = (id: number) => {
- // Mark current step as completed when navigating forward
- if (id > activeStep) {
- setCompletedSteps((prev) => new Set([...prev, activeStep]));
- }
setActiveStep(id);
};
const goNext = () => {
- if (activeStep < STEPS.length) goTo(activeStep + 1);
+ if (activeStep < STEPS.length) {
+ setCompletedSteps((prev) => new Set([...prev, activeStep]));
+ setActiveStep(activeStep + 1);
+ }
};
const goPrev = () => {
if (activeStep > 1) setActiveStep(activeStep - 1);
};
- const markComplete = () => {
- setCompletedSteps((prev) => new Set([...prev, activeStep]));
- if (activeStep < STEPS.length) goTo(activeStep + 1);
+ const allComplete = completedSteps.size >= STEPS.length;
+
+ const fireConfetti = useCallback(() => {
+ if (hasConfettiFired.current) return;
+ hasConfettiFired.current = true;
+ const end = Date.now() + 1500;
+ const colors = ['#ffd208', '#e6b800', '#10b981', '#a78bfa', '#f59e0b'];
+ (function frame() {
+ confetti({
+ particleCount: 4,
+ angle: 60,
+ spread: 55,
+ origin: { x: 0 },
+ colors,
+ });
+ confetti({
+ particleCount: 4,
+ angle: 120,
+ spread: 55,
+ origin: { x: 1 },
+ colors,
+ });
+ if (Date.now() < end) requestAnimationFrame(frame);
+ })();
+ }, []);
+
+ const markAllComplete = () => {
+ setCompletedSteps(new Set(STEPS.map((s) => s.id)));
};
- const allComplete = completedSteps.size >= STEPS.length;
+ useEffect(() => {
+ if (allComplete) fireConfetti();
+ }, [allComplete, fireConfetti]);
return (
@@ -602,7 +633,7 @@ export default function LearnPage() {
}}
>
-
+
Tutorial Complete!
@@ -613,17 +644,17 @@ export default function LearnPage() {
-
+
Get Test Tokens
-
+
Start Shielding
-
+
Explore Registry
@@ -672,15 +703,6 @@ export default function LearnPage() {
- {!completedSteps.has(activeStep) && (
-
- Mark as Done
-
- )}
{activeStep < STEPS.length ? (
- Complete Tutorial
+ Mark as Done
) : null}
diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx
new file mode 100644
index 0000000..643b7f7
--- /dev/null
+++ b/src/app/app/page.tsx
@@ -0,0 +1,455 @@
+'use client';
+
+import React, { useState, useMemo } from 'react';
+import Link from 'next/link';
+import Card from '@/components/ui/Card';
+import Badge from '@/components/ui/Badge';
+import Button from '@/components/ui/Button';
+import CopyButton from '@/components/ui/CopyButton';
+import TokenIcon from '@/components/ui/TokenIcon';
+import Skeleton from '@/components/ui/Skeleton';
+import Tooltip from '@/components/ui/Tooltip';
+import { formatAddress, formatAmount } from '@/lib/utils';
+import { useActiveNetwork } from '@/app/ClientLayout';
+import { useRegistryPairs, isMintablePair, type RegistryPairsResult } from '@/lib/registry';
+import { type WrapperPair } from '@/config/contracts';
+import { ERC20_ABI } from '@/lib/wrapper-abi';
+import BlurIn from '@/components/ui/BlurIn';
+import { useAccount, useReadContract } from 'wagmi';
+import { useConfidentialBalance } from '@zama-fhe/react-sdk';
+import {
+ Search,
+ Lock,
+ Unlock,
+ Shield,
+ ExternalLink,
+ Info,
+ AlertTriangle,
+ AlertCircle,
+ Database,
+ Settings2,
+} from 'lucide-react';
+
+// ─── Tooltip content constants ────────────────────────────────────────────────
+// Centralised here so copy can be revised without hunting through JSX.
+
+const TIP = {
+ erc7984: 'ERC-7984 wrapper stores your balance as on-chain ciphertext via FHE — unreadable by anyone without your cryptographic permit.',
+ confidentialBadge: 'Balances are encrypted on-chain via FHE. Only you can decrypt them by signing an EIP-712 permit.',
+ publicBalance: 'Your unencrypted ERC-20 balance, visible to anyone on-chain. Shield it to make it private.',
+ confidentialBalance: 'Encrypted balance. Click Decrypt to sign a read-only EIP-712 permit — no tokens are spent, your private key stays in your wallet.',
+ mockBadge: 'Testnet mock token deployed by Zama. Has a public mint() — get free tokens from the Faucet page.',
+ shield: (sym: string) => `Convert public ${sym} into encrypted c${sym}. Requires ERC-20 approval then the shield transaction.`,
+ unshield: (sym: string) => `Burn encrypted c${sym} and retrieve public ${sym}. Two-step: on-chain unwrap + Gateway proof finalization.`,
+ permit: 'Read-only off-chain signature (EIP-712). Authorises Zama Gateway to decrypt your balance for this session. Does not spend tokens or approve contracts.',
+};
+
+// ─── Per-row component ────────────────────────────────────────────────────────
+
+function RegistryTokenRow({
+ wrapper,
+ explorerBase,
+ isTestnet,
+}: {
+ wrapper: WrapperPair;
+ explorerBase: string;
+ isTestnet: boolean;
+}) {
+ const { address, isConnected } = useAccount();
+ const [decryptRequested, setDecryptRequested] = useState(false);
+
+ // Public ERC-20 balance
+ const { data: rawPublicBalance } = useReadContract({
+ abi: ERC20_ABI,
+ address: wrapper.erc20Address,
+ functionName: 'balanceOf',
+ args: address ? [address] : undefined,
+ query: { enabled: isConnected && !!address },
+ });
+ const publicBalance = rawPublicBalance as bigint | undefined;
+
+ // Confidential balance — only fires after explicit user click
+ const {
+ data: confidentialBalance,
+ isLoading: isDecrypting,
+ error: decryptError,
+ refetch: refetchConfidential,
+ } = useConfidentialBalance(
+ { tokenAddress: wrapper.erc7984Address },
+ { enabled: decryptRequested && isConnected && !!address },
+ );
+
+ const isRevoked = wrapper.isValid === false;
+ const cleanName = wrapper.name.replace(/\s*\(Mock\)\s*/gi, '').trim();
+ const isMock = isMintablePair(wrapper) && isTestnet;
+ const confidentialSymbol = `c${wrapper.symbol}`;
+
+ const handleDecrypt = () => {
+ setDecryptRequested(true);
+ refetchConfidential();
+ };
+
+ return (
+
+
+ {/* ── Token ─────────────────────────────────────────────────────────── */}
+
+
+
+
+ {/* Name + badges on one line */}
+
+ {cleanName}
+ {isMock && (
+
+ Mock
+
+
+ )}
+ {isRevoked && (
+
+ Revoked
+
+ )}
+
+
{wrapper.symbol}
+
+
+
+
+ {/* ── ERC-20 Address ────────────────────────────────────────────────── */}
+
+
+
+
+ {/* ── ERC-7984 Wrapper ──────────────────────────────────────────────── */}
+
+
+
+
+ Confidential
+
+
+
+
+
{confidentialSymbol}
+
+
+
+ {/* ── Public Balance ────────────────────────────────────────────────── */}
+
+ {!isConnected ? (
+ —
+ ) : publicBalance !== undefined ? (
+
+ {formatAmount(publicBalance, wrapper.decimals)}{' '}
+ {wrapper.symbol}
+
+ ) : (
+
+ )}
+
+
+ {/* ── Confidential Balance ──────────────────────────────────────────── */}
+
+ {!isConnected ? (
+ —
+ ) : confidentialBalance !== undefined && confidentialBalance !== null ? (
+
+ {formatAmount(confidentialBalance, wrapper.wrapperDecimals)}{' '}
+ {confidentialSymbol}
+
+
+ ) : isDecrypting ? (
+ Awaiting signature…
+ ) : decryptError ? (
+
+ Retry
+
+ ) : (
+
+
+ Decrypt
+
+
+
+ )}
+
+
+ {/* ── Actions ───────────────────────────────────────────────────────── */}
+
+ {isRevoked ? (
+
+ Unavailable
+
+ ) : (
+
+
+
+ Shield
+
+
+
+
+
+ Unshield
+
+
+
+
+
+ Manage
+
+
+
+ )}
+
+
+ );
+}
+
+// ─── Page ─────────────────────────────────────────────────────────────────────
+
+export default function HomePage() {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [showRevoked, setShowRevoked] = useState(false);
+ const { isTestnet, activeChainId } = useActiveNetwork();
+
+ const { pairs, isLoading, isFromCache, total }: RegistryPairsResult =
+ useRegistryPairs(activeChainId);
+
+ const visibleWrappers = useMemo(
+ () => (showRevoked ? pairs : pairs.filter(p => p.isValid !== false)),
+ [pairs, showRevoked],
+ );
+ const revokedCount = useMemo(() => pairs.filter(p => p.isValid === false).length, [pairs]);
+
+ const filteredWrappers = useMemo(() => {
+ if (!searchQuery) return visibleWrappers;
+ const q = searchQuery.toLowerCase();
+ return visibleWrappers.filter(
+ w =>
+ w.name.toLowerCase().includes(q) ||
+ w.symbol.toLowerCase().includes(q) ||
+ w.erc20Address.toLowerCase().includes(q) ||
+ w.erc7984Address.toLowerCase().includes(q),
+ );
+ }, [visibleWrappers, searchQuery]);
+
+ const explorerBase = isTestnet ? 'https://eth-sepolia.blockscout.com' : 'https://eth.blockscout.com';
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Cached-snapshot banner */}
+ {isFromCache && (
+
+
+
+
+
+
+ Showing a cached snapshot. Connect a wallet on{' '}
+ {isTestnet ? 'Sepolia' : 'Ethereum Mainnet'} to read the live on-chain
+ WrappersRegistry — the live list may include newer pairs.
+
+
+
+ )}
+
+ {/* Stats */}
+
+
+ Registered Pairs
+
+ {isLoading && pairs.length === 0 ? : total}
+
+ {revokedCount > 0 && {revokedCount} revoked
}
+
+
+ Active Network
+
+ {isTestnet ? 'Sepolia Testnet' : 'Ethereum Mainnet'}
+
+
+
+ FHE Security
+
+ ERC-7984 Standard
+
+
+
+
+
+ {/* Search */}
+
+
+ {/* Table */}
+
+
+
+
+ Token
+ ERC-20 Address
+
+
+ ERC-7984 Wrapper
+
+
+
+
+
+ Public Balance
+
+
+
+
+
+ Confidential Balance
+
+
+
+ Actions
+
+
+
+ {isLoading && filteredWrappers.length === 0 ? (
+ Array.from({ length: 5 }).map((_, i) => (
+
+
+
+ ))
+ ) : filteredWrappers.length === 0 ? (
+
+
+
+
+
+
+
+ {searchQuery ? 'No tokens match your search query' : 'No registered wrappers found on this network'}
+
+
+
+
+ ) : (
+ filteredWrappers.map(wrapper => (
+
+ ))
+ )}
+
+
+
+
+ {/* Info Banner */}
+
+
+
+
+
+
+
+ Underlying Mechanism
+
+
+
+ Confidential wrappers convert standard public tokens into ERC-7984 tokens
+ utilizing Fully Homomorphic Encryption (FHE) on the fhEVM. On-chain values
+ (like account balances and transfer amounts) are encrypted into cryptographic
+ handles — protecting transaction details from public ledger scraping.
+
+
+
+
+
+ );
+}
diff --git a/src/app/portfolio/page.tsx b/src/app/app/portfolio/page.tsx
similarity index 99%
rename from src/app/portfolio/page.tsx
rename to src/app/app/portfolio/page.tsx
index 9f10bfa..8107528 100644
--- a/src/app/portfolio/page.tsx
+++ b/src/app/app/portfolio/page.tsx
@@ -148,7 +148,7 @@ function TokenPositionCard({
variant="secondary"
fullWidth
size="sm"
- onClick={() => (window.location.href = `/wrap?token=${wrapper.symbol}&action=unwrap`)}
+ onClick={() => (window.location.href = `/app/wrap?token=${wrapper.symbol}&action=unwrap`)}
>
Unshield
@@ -259,7 +259,7 @@ function WalletActivityFeed({
Refresh
-
+
Protocol Analytics
diff --git a/src/app/wrap/page.tsx b/src/app/app/wrap/page.tsx
similarity index 96%
rename from src/app/wrap/page.tsx
rename to src/app/app/wrap/page.tsx
index dd52868..dc93151 100644
--- a/src/app/wrap/page.tsx
+++ b/src/app/app/wrap/page.tsx
@@ -258,6 +258,7 @@ function WrapPageContent() {
setTxStep(needsApproval ? 1 : 3);
const res = await shield({
amount: parsedInputAmount,
+ approvalStrategy: needsApproval ? 'exact' : 'skip',
onApprovalSubmitted: (txHash) => {
setTxStep(2); // Approve mining
setActiveTxHash(txHash);
@@ -377,7 +378,7 @@ function WrapPageContent() {
{/* Pending unshield banner for the currently selected token */}
{selectedWrapper && (
-
+
-
+
+
0 && txStep < 5 ? 'swap-card-pending' : ''}`}
+ >
{/* From Panel */}
@@ -617,77 +622,7 @@ function WrapPageContent() {
)}
- {/* Transaction Link */}
- {(activeTxHash || finalTxHash) && (
-
-
-
-
- {txStep === 5
- ? `${action === 'wrap' ? 'Shield' : 'Unshield'} Successful`
- : txStep === 2
- ? 'Approval Pending...'
- : txStep === 4
- ? `${action === 'wrap' ? 'Shielding' : 'Unshielding'} Pending...`
- : 'Transaction Submitted'}
-
-
- {formatAddress(finalTxHash || activeTxHash || '')}
-
-
-
- View Explorer
-
-
-
- {/* Coprocessor / Gateway Delay Explanation */}
- {txStep > 0 && txStep < 5 && (
-
- ⚠️
-
- FHE transactions require Zama Gateway & Coprocessor proof generation, which takes 15–40 seconds to confirm on-chain. Please keep this window open.
-
-
- )}
-
- )}
-
- {/* Submit Button */}
+ {/* Primary Action Button — ALWAYS in this slot */}
{!isConnected ? (
setIsConnectModalOpen(true)}>
@@ -756,6 +691,75 @@ function WrapPageContent() {
)}
+
+ {/* Transaction Info — appears BELOW the primary button, never replaces it */}
+ {(activeTxHash || finalTxHash) && (
+
+
+
+
+ {txStep === 5
+ ? `${action === 'wrap' ? 'Shield' : 'Unshield'} Successful`
+ : txStep === 2
+ ? 'Approval Pending...'
+ : txStep === 4
+ ? `${action === 'wrap' ? 'Shielding' : 'Unshielding'} Pending...`
+ : 'Transaction Submitted'}
+
+
+ {formatAddress(finalTxHash || activeTxHash || '')}
+
+
+
+ View Explorer
+
+
+
+ {txStep > 0 && txStep < 5 && (
+
+ ⚠️
+
+ FHE transactions require Zama Gateway & Coprocessor proof generation, which takes 15–40 seconds to confirm on-chain. Please keep this window open.
+
+
+ )}
+
+ )}
{/* Info */}
diff --git a/src/app/error.tsx b/src/app/error.tsx
index c3be582..55e0297 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -66,7 +66,7 @@ export default function ErrorBoundary({
Try again
*/
+ --font-display: var(--font-fraunces, 'Fraunces'), Georgia, serif;
+ --font-sans: var(--font-jakarta, 'Plus Jakarta Sans'), system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
+
+ /* shadcn/MagicCard compat — defaults to light; overridden by .section-dark */
+ --color-background: #f8f5f0;
+ --color-border: rgba(15, 14, 12, 0.10);
+
+ /* Motion */
+ --animate-aurora: aurora 20s ease-in-out infinite alternate;
+ --animate-marquee: marquee var(--duration, 30s) linear infinite;
+ --animate-marquee-vertical: marquee-vertical var(--duration, 30s) linear infinite;
+ --animate-shimmer-slide: shimmer-slide var(--speed, 2s) ease-in-out infinite alternate;
+ --animate-spin-around: spin-around calc(var(--speed, 2s) * 2) infinite linear;
+ --animate-shine: shine var(--duration, 8s) ease-in-out infinite;
+ --animate-grid: grid 15s linear infinite;
+ --animate-border-beam: border-beam calc(var(--duration, 8s) * 1s) infinite linear;
+ --animate-blink-cursor: blink-cursor 1.2s step-end infinite;
+}
+
+/* ---------------------------------------------------------------------------
+ * Keyframes
+ * -------------------------------------------------------------------------- */
+@keyframes aurora {
+ 0% { background-position: 0% 50%; transform: rotate(-5deg) scale(0.9); }
+ 25% { background-position: 50% 100%; transform: rotate(5deg) scale(1.1); }
+ 50% { background-position: 100% 50%; transform: rotate(-3deg) scale(0.95); }
+ 75% { background-position: 50% 0%; transform: rotate(3deg) scale(1.05); }
+ 100% { background-position: 0% 50%; transform: rotate(-5deg) scale(0.9); }
+}
+@keyframes marquee {
+ from { transform: translateX(0); }
+ to { transform: translateX(calc(-100% - var(--gap, 1rem))); }
+}
+@keyframes marquee-vertical {
+ from { transform: translateY(0); }
+ to { transform: translateY(calc(-100% - var(--gap, 1rem))); }
+}
+@keyframes shimmer-slide {
+ to { transform: translate(calc(100cqw - 100%), 0); }
+}
+@keyframes spin-around {
+ 0% { transform: translateZ(0) rotate(0); }
+ 15%, 35% { transform: translateZ(0) rotate(90deg); }
+ 65%, 85% { transform: translateZ(0) rotate(270deg); }
+ 100% { transform: translateZ(0) rotate(360deg); }
+}
+@keyframes shine {
+ 0% { background-position: 0% 0%; }
+ 50% { background-position: 100% 100%; }
+ 100% { background-position: 0% 0%; }
+}
+@keyframes grid {
+ 0% { transform: translateY(-50%); }
+ 100% { transform: translateY(0); }
+}
+@keyframes border-beam {
+ 100% { offset-distance: 100%; }
+}
+@keyframes blink-cursor {
+ 0%, 49% { opacity: 1; }
+ 50%, 100% { opacity: 0; }
+}
+
+/* ---------------------------------------------------------------------------
+ * Landing root — light cream default. Never bleeds outside .landing-root.
+ * -------------------------------------------------------------------------- */
+.landing-root {
+ font-family: var(--font-sans);
+ background: var(--color-cream-100);
+ color: var(--color-ink-900);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ min-height: 100vh;
+}
+
+.landing-root ::selection {
+ background: var(--color-gold-500);
+ color: var(--color-ink-950);
+}
+
+.landing-root h1,
+.landing-root h2,
+.landing-root h3 {
+ font-family: var(--font-display);
+ letter-spacing: -0.02em;
+}
+
+/* Dark variant — used on FHE explainer, final CTA, footer */
+.section-dark {
+ --color-background: #050504;
+ --color-border: rgba(255, 255, 255, 0.08);
+}
+
+/* Grain overlay for hero only */
+.landing-grain::before {
+ content: '';
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 1;
+ opacity: 0.03;
+ mix-blend-mode: multiply;
+ background-image: url("data:image/svg+xml;utf8,
");
+}
+
+/* Gold rule divider */
+.landing-rule {
+ height: 1px;
+ background: linear-gradient(
+ to right,
+ transparent 0%,
+ var(--color-gold-500) 50%,
+ transparent 100%
+ );
+ opacity: 0.4;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 7694c1a..b993e2b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,36 +1,41 @@
-import type { Metadata } from "next";
-import "./globals.css";
-import ClientLayout from "./ClientLayout";
+import type { Metadata } from 'next';
+import { Fraunces, Plus_Jakarta_Sans } from 'next/font/google';
+
+/* Fraunces — optical-size variable serif. Distinctive editorial feel,
+ completely unlike Zama's clean tech sans. Used for hero + section h2. */
+const fraunces = Fraunces({
+ subsets: ['latin'],
+ variable: '--font-fraunces',
+ display: 'swap',
+});
+
+/* Plus Jakarta Sans — humanist rounded sans. Modern, not overused in crypto.
+ Used for body text, nav, UI labels. */
+const jakarta = Plus_Jakarta_Sans({
+ subsets: ['latin'],
+ variable: '--font-jakarta',
+ display: 'swap',
+});
export const metadata: Metadata = {
- title: "ZamaVault — Confidential Wrapper Registry",
+ title: 'ZamaVault — Confidential Tokens, Made Simple',
description:
- "Discover, wrap, and manage confidential ERC-7984 tokens on the Zama Protocol. The canonical interface for converting ERC-20 tokens to their encrypted counterparts using Fully Homomorphic Encryption.",
+ "Discover, shield, and manage confidential ERC-7984 tokens on Zama's fully-homomorphic encryption protocol. The canonical interface for the Confidential Wrappers Registry.",
keywords: [
- "Zama",
- "FHE",
- "ERC-7984",
- "confidential tokens",
- "wrapper registry",
- "privacy",
- "Ethereum",
- "DeFi",
+ 'Zama', 'FHE', 'fully homomorphic encryption', 'ERC-7984',
+ 'confidential tokens', 'wrapper registry', 'privacy', 'Ethereum', 'DeFi',
],
openGraph: {
- title: "ZamaVault — Confidential Wrapper Registry",
+ title: 'ZamaVault — Confidential Tokens, Made Simple',
description:
- "The definitive interface for managing confidential ERC-7984 tokens powered by Fully Homomorphic Encryption.",
- type: "website",
+ "The definitive interface for managing confidential ERC-7984 tokens powered by Zama's Fully Homomorphic Encryption.",
+ type: 'website',
},
};
-export default function RootLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
+export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
+
-
-
{children}
-
+ {children}
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4e35c3b..5b342e2 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,455 +1,41 @@
-'use client';
-
-import React, { useState, useMemo } from 'react';
-import Link from 'next/link';
-import Card from '@/components/ui/Card';
-import Badge from '@/components/ui/Badge';
-import Button from '@/components/ui/Button';
-import CopyButton from '@/components/ui/CopyButton';
-import TokenIcon from '@/components/ui/TokenIcon';
-import Skeleton from '@/components/ui/Skeleton';
-import Tooltip from '@/components/ui/Tooltip';
-import { formatAddress, formatAmount } from '@/lib/utils';
-import { useActiveNetwork } from '@/app/ClientLayout';
-import { useRegistryPairs, isMintablePair, type RegistryPairsResult } from '@/lib/registry';
-import { type WrapperPair } from '@/config/contracts';
-import { ERC20_ABI } from '@/lib/wrapper-abi';
-import BlurIn from '@/components/ui/BlurIn';
-import { useAccount, useReadContract } from 'wagmi';
-import { useConfidentialBalance } from '@zama-fhe/react-sdk';
-import {
- Search,
- Lock,
- Unlock,
- Shield,
- ExternalLink,
- Info,
- AlertTriangle,
- AlertCircle,
- Database,
- Settings2,
-} from 'lucide-react';
-
-// ─── Tooltip content constants ────────────────────────────────────────────────
-// Centralised here so copy can be revised without hunting through JSX.
-
-const TIP = {
- erc7984: 'ERC-7984 wrapper stores your balance as on-chain ciphertext via FHE — unreadable by anyone without your cryptographic permit.',
- confidentialBadge: 'Balances are encrypted on-chain via FHE. Only you can decrypt them by signing an EIP-712 permit.',
- publicBalance: 'Your unencrypted ERC-20 balance, visible to anyone on-chain. Shield it to make it private.',
- confidentialBalance: 'Encrypted balance. Click Decrypt to sign a read-only EIP-712 permit — no tokens are spent, your private key stays in your wallet.',
- mockBadge: 'Testnet mock token deployed by Zama. Has a public mint() — get free tokens from the Faucet page.',
- shield: (sym: string) => `Convert public ${sym} into encrypted c${sym}. Requires ERC-20 approval then the shield transaction.`,
- unshield: (sym: string) => `Burn encrypted c${sym} and retrieve public ${sym}. Two-step: on-chain unwrap + Gateway proof finalization.`,
- permit: 'Read-only off-chain signature (EIP-712). Authorises Zama Gateway to decrypt your balance for this session. Does not spend tokens or approve contracts.',
-};
-
-// ─── Per-row component ────────────────────────────────────────────────────────
-
-function RegistryTokenRow({
- wrapper,
- explorerBase,
- isTestnet,
-}: {
- wrapper: WrapperPair;
- explorerBase: string;
- isTestnet: boolean;
-}) {
- const { address, isConnected } = useAccount();
- const [decryptRequested, setDecryptRequested] = useState(false);
-
- // Public ERC-20 balance
- const { data: rawPublicBalance } = useReadContract({
- abi: ERC20_ABI,
- address: wrapper.erc20Address,
- functionName: 'balanceOf',
- args: address ? [address] : undefined,
- query: { enabled: isConnected && !!address },
- });
- const publicBalance = rawPublicBalance as bigint | undefined;
-
- // Confidential balance — only fires after explicit user click
- const {
- data: confidentialBalance,
- isLoading: isDecrypting,
- error: decryptError,
- refetch: refetchConfidential,
- } = useConfidentialBalance(
- { tokenAddress: wrapper.erc7984Address },
- { enabled: decryptRequested && isConnected && !!address },
- );
-
- const isRevoked = wrapper.isValid === false;
- const cleanName = wrapper.name.replace(/\s*\(Mock\)\s*/gi, '').trim();
- const isMock = isMintablePair(wrapper) && isTestnet;
- const confidentialSymbol = `c${wrapper.symbol}`;
-
- const handleDecrypt = () => {
- setDecryptRequested(true);
- refetchConfidential();
- };
-
+/**
+ * Landing page (route: /).
+ *
+ * Completely separated from the app. Imports its own Tailwind v4 stylesheet
+ * (`landing.css`); does NOT touch globals.css or ClientLayout. The /app/*
+ * subtree has its own layout that brings in the app design system.
+ *
+ * Composition is one section per file under `src/components/landing/*` so
+ * each beat can be tuned independently.
+ */
+import './landing.css';
+
+import { BFCacheRecovery } from '@/components/landing/bfcache-recovery';
+import { LandingNav } from '@/components/landing/nav';
+import { Hero } from '@/components/landing/hero';
+import { TrustRail } from '@/components/landing/trust-rail';
+import { Stats } from '@/components/landing/stats';
+import { Features } from '@/components/landing/features';
+import { HowItWorks } from '@/components/landing/how-it-works';
+import { FheExplainer } from '@/components/landing/fhe-explainer';
+import { FinalCta } from '@/components/landing/final-cta';
+import { LandingFooter } from '@/components/landing/footer';
+
+export default function LandingPage() {
return (
-
-
- {/* ── Token ─────────────────────────────────────────────────────────── */}
-
-
-
-
- {/* Name + badges on one line */}
-
- {cleanName}
- {isMock && (
-
- Mock
-
-
- )}
- {isRevoked && (
-
- Revoked
-
- )}
-
-
{wrapper.symbol}
-
-
-
-
- {/* ── ERC-20 Address ────────────────────────────────────────────────── */}
-
-
-
-
- {/* ── ERC-7984 Wrapper ──────────────────────────────────────────────── */}
-
-
-
-
- Confidential
-
-
-
-
-
{confidentialSymbol}
-
-
-
- {/* ── Public Balance ────────────────────────────────────────────────── */}
-
- {!isConnected ? (
- —
- ) : publicBalance !== undefined ? (
-
- {formatAmount(publicBalance, wrapper.decimals)}{' '}
- {wrapper.symbol}
-
- ) : (
-
- )}
-
-
- {/* ── Confidential Balance ──────────────────────────────────────────── */}
-
- {!isConnected ? (
- —
- ) : confidentialBalance !== undefined && confidentialBalance !== null ? (
-
- {formatAmount(confidentialBalance, wrapper.wrapperDecimals)}{' '}
- {confidentialSymbol}
-
-
- ) : isDecrypting ? (
- Awaiting signature…
- ) : decryptError ? (
-
- Retry
-
- ) : (
-
-
- Decrypt
-
-
-
- )}
-
-
- {/* ── Actions ───────────────────────────────────────────────────────── */}
-
- {isRevoked ? (
-
- Unavailable
-
- ) : (
-
-
-
- Shield
-
-
-
-
-
- Unshield
-
-
-
-
-
- Manage
-
-
-
- )}
-
-
- );
-}
-
-// ─── Page ─────────────────────────────────────────────────────────────────────
-
-export default function HomePage() {
- const [searchQuery, setSearchQuery] = useState('');
- const [showRevoked, setShowRevoked] = useState(false);
- const { isTestnet, activeChainId } = useActiveNetwork();
-
- const { pairs, isLoading, isFromCache, total }: RegistryPairsResult =
- useRegistryPairs(activeChainId);
-
- const visibleWrappers = useMemo(
- () => (showRevoked ? pairs : pairs.filter(p => p.isValid !== false)),
- [pairs, showRevoked],
- );
- const revokedCount = useMemo(() => pairs.filter(p => p.isValid === false).length, [pairs]);
-
- const filteredWrappers = useMemo(() => {
- if (!searchQuery) return visibleWrappers;
- const q = searchQuery.toLowerCase();
- return visibleWrappers.filter(
- w =>
- w.name.toLowerCase().includes(q) ||
- w.symbol.toLowerCase().includes(q) ||
- w.erc20Address.toLowerCase().includes(q) ||
- w.erc7984Address.toLowerCase().includes(q),
- );
- }, [visibleWrappers, searchQuery]);
-
- const explorerBase = isTestnet ? 'https://eth-sepolia.blockscout.com' : 'https://eth.blockscout.com';
-
- return (
-
- {/* Header */}
-
-
- {/* Cached-snapshot banner */}
- {isFromCache && (
-
-
-
-
-
-
- Showing a cached snapshot. Connect a wallet on{' '}
- {isTestnet ? 'Sepolia' : 'Ethereum Mainnet'} to read the live on-chain
- WrappersRegistry — the live list may include newer pairs.
-
-
-
- )}
-
- {/* Stats */}
-
-
- Registered Pairs
-
- {isLoading && pairs.length === 0 ? : total}
-
- {revokedCount > 0 && {revokedCount} revoked
}
-
-
- Active Network
-
- {isTestnet ? 'Sepolia Testnet' : 'Ethereum Mainnet'}
-
-
-
- FHE Security
-
- ERC-7984 Standard
-
-
-
-
-
- {/* Search */}
-
-
- {/* Table */}
-
-
-
-
- Token
- ERC-20 Address
-
-
- ERC-7984 Wrapper
-
-
-
-
-
- Public Balance
-
-
-
-
-
- Confidential Balance
-
-
-
- Actions
-
-
-
- {isLoading && filteredWrappers.length === 0 ? (
- Array.from({ length: 5 }).map((_, i) => (
-
-
-
- ))
- ) : filteredWrappers.length === 0 ? (
-
-
-
-
-
-
-
- {searchQuery ? 'No tokens match your search query' : 'No registered wrappers found on this network'}
-
-
-
-
- ) : (
- filteredWrappers.map(wrapper => (
-
- ))
- )}
-
-
-
-
- {/* Info Banner */}
-
-
-
-
-
-
-
- Underlying Mechanism
-
-
-
- Confidential wrappers convert standard public tokens into ERC-7984 tokens
- utilizing Fully Homomorphic Encryption (FHE) on the fhEVM. On-chain values
- (like account balances and transfer amounts) are encrypted into cryptographic
- handles — protecting transaction details from public ledger scraping.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/landing/aurora-bg.tsx b/src/components/landing/aurora-bg.tsx
new file mode 100644
index 0000000..7ed772f
--- /dev/null
+++ b/src/components/landing/aurora-bg.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+
+/**
+ * AuroraBg — light-mode gold aurora on cream background.
+ * Two very soft gold radial glows that slowly drift, giving the hero
+ * an atmospheric warmth without overpowering the dark text.
+ */
+export function AuroraBg({ className }: { className?: string }) {
+ return (
+
+ {/* Soft gold halo — top-left */}
+
+
+ {/* Very subtle dot grid — dark lines at low opacity on cream */}
+
+
+ );
+}
diff --git a/src/components/landing/bfcache-recovery.tsx b/src/components/landing/bfcache-recovery.tsx
new file mode 100644
index 0000000..78a120d
--- /dev/null
+++ b/src/components/landing/bfcache-recovery.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import { useEffect } from 'react';
+
+/**
+ * BFCache recovery.
+ *
+ * Symptom: returning to the landing via the browser back-button (after
+ * visiting an external link with target="_blank" + then navigating, or a
+ * normal back from an internal link) leaves the page nearly blank.
+ *
+ * Cause: every section is wrapped in
which mounts with
+ * initial="hidden" (opacity:0 + blur). Visibility is triggered by an
+ * IntersectionObserver inside `useInView({ once: true })`. When the browser
+ * restores the page from the back-forward cache the DOM is hot but JS does
+ * NOT re-execute, so the observer never re-fires and every wrapped node
+ * stays at opacity:0.
+ *
+ * Fix: listen for `pageshow` with `event.persisted === true` (the canonical
+ * BFCache signal) and force a clean reload. Trade-off is one extra request
+ * on back-navigation, which is acceptable for a static page.
+ */
+export function BFCacheRecovery() {
+ useEffect(() => {
+ const handler = (event: PageTransitionEvent) => {
+ if (event.persisted) window.location.reload();
+ };
+ window.addEventListener('pageshow', handler);
+ return () => window.removeEventListener('pageshow', handler);
+ }, []);
+ return null;
+}
diff --git a/src/components/landing/features.tsx b/src/components/landing/features.tsx
new file mode 100644
index 0000000..efc739a
--- /dev/null
+++ b/src/components/landing/features.tsx
@@ -0,0 +1,166 @@
+'use client';
+
+import Link from 'next/link';
+import {
+ Search,
+ Shield,
+ Wallet,
+ Droplets,
+ Code2,
+ BarChart3,
+ ArrowUpRight,
+} from 'lucide-react';
+import { BlurFade } from '@/components/magic/blur-fade';
+import { BorderBeam } from '@/components/magic/border-beam';
+import { MagicCard } from '@/components/magic/magic-card';
+import { TypingAnimation } from '@/components/magic/typing-animation';
+import { cn } from '@/lib/utils';
+
+interface CardDef {
+ title: string;
+ desc: string;
+ icon: React.ElementType;
+ href: string;
+ span: string;
+ accent?: boolean;
+}
+
+const CARDS: CardDef[] = [
+ {
+ title: 'Live registry, every pair',
+ desc: 'Every confidential wrapper as the on-chain WrappersRegistry sees it — Sepolia and Mainnet, in real time.',
+ icon: Search,
+ href: '/app',
+ span: 'md:col-span-1',
+ accent: true,
+ },
+ {
+ title: 'Shield in two clicks',
+ desc: 'Approve and wrap any ERC-20 into its ERC-7984 form. Decimal scaling handled automatically.',
+ icon: Shield,
+ href: '/app/wrap',
+ span: 'md:col-span-1',
+ },
+ {
+ title: 'Confidential portfolio',
+ desc: 'Decrypt all your balances in one EIP-712 permit. Re-shield, unshield, withdraw.',
+ icon: Wallet,
+ href: '/app/portfolio',
+ span: 'md:col-span-1',
+ },
+ {
+ title: 'Testnet faucet',
+ desc: 'Mint mock USDC, WETH, ZAMA & more on Sepolia — wired to the official Zama mocks.',
+ icon: Droplets,
+ href: '/app/faucet',
+ span: 'md:col-span-1',
+ },
+ {
+ title: 'REST + code snippets',
+ desc: 'Public /api/registry endpoint and copy-paste React / viem / ethers snippets.',
+ icon: Code2,
+ href: '/app/developers',
+ span: 'md:col-span-1',
+ },
+ {
+ title: 'Protocol analytics',
+ desc: 'TVL by token, 24-hour shield/unshield volume, ranking — pulled straight from on-chain events.',
+ icon: BarChart3,
+ href: '/app/analytics',
+ span: 'md:col-span-1',
+ },
+];
+
+function FeatureCard({ card, idx }: { card: CardDef; idx: number }) {
+ const Icon = card.icon;
+ return (
+
+
+
+
+ {card.accent && (
+
+ )}
+
+
+
+
+
+
+
+ {card.title}
+
+
+ {card.desc}
+
+
+
+
+
+
+
+
+ );
+}
+
+export function Features() {
+ return (
+
+
+
+
+
+ ─ Built for the registry
+
+
+ Everything you need to{' '}
+
+
+
+ Six tools, one consistent design. Whether you're shielding a balance,
+ auditing a wrapper, or shipping a confidential dApp — start here.
+
+
+
+
+
+ {CARDS.map((c, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/fhe-explainer.tsx b/src/components/landing/fhe-explainer.tsx
new file mode 100644
index 0000000..54cbf9d
--- /dev/null
+++ b/src/components/landing/fhe-explainer.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { BlurFade } from '@/components/magic/blur-fade';
+import { DotPattern } from '@/components/magic/dot-pattern';
+import { TextAnimate } from '@/components/magic/text-animate';
+import { Lock, Eye, KeyRound, Cpu } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+const POINTS = [
+ {
+ icon: Lock,
+ title: 'Encrypted on-chain',
+ body: 'Balances stored as euint64 ciphertext. Public block explorers cannot read them.',
+ },
+ {
+ icon: KeyRound,
+ title: 'You hold the key',
+ body: 'Decryption requires a wallet-signed EIP-712 permit. No custody, no central decryptor.',
+ },
+ {
+ icon: Cpu,
+ title: 'Computed homomorphically',
+ body: 'Transfers, allowances, comparisons — all execute on ciphertext via fhEVM.',
+ },
+ {
+ icon: Eye,
+ title: 'Auditable when needed',
+ body: 'Selectively reveal balances per session, per contract, or grant scoped read access.',
+ },
+];
+
+export function FheExplainer() {
+ return (
+
+
+
+
+
+
+ ─ Privacy, not pseudonymity
+
+
+ Real on-chain confidentiality.
+
+
+ Public chains expose every balance and transfer to anyone watching. Zama's
+ Fully Homomorphic Encryption (FHE) protocol changes that — balances live
+ on-chain, but only you can read them.
+
+
+ ZamaVault wraps every official ERC-7984 token into a clean UX:
+ shield, transfer, decrypt, unshield. No node operator, no validator,
+ no block-explorer crawler sees your balance.
+
+
+
+
+ {POINTS.map((p, i) => {
+ const Icon = p.icon;
+ return (
+
+
+
+
+
+
+ {p.title}
+
+
+ {p.body}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/components/landing/final-cta.tsx b/src/components/landing/final-cta.tsx
new file mode 100644
index 0000000..e466063
--- /dev/null
+++ b/src/components/landing/final-cta.tsx
@@ -0,0 +1,69 @@
+'use client';
+
+import Link from 'next/link';
+import { ArrowUpRight } from 'lucide-react';
+import { SiGithub } from 'react-icons/si';
+import { BlurFade } from '@/components/magic/blur-fade';
+import { ShimmerButton } from '@/components/magic/shimmer-button';
+
+/**
+ * Final CTA — short, declarative, one primary action.
+ * Sits inside a contained "card" with a faint gold edge to draw the eye
+ * without making the whole page feel like an ad.
+ */
+export function FinalCta() {
+ return (
+
+
+
+
+ {/* Subtle gold halo */}
+
+
+
+
+ Your tokens.
+
+
+ Your privacy.
+
+
+
+ Stop leaking your portfolio to every block-explorer indexer.
+ Shield, transact, decrypt — all from one clean interface.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/footer.tsx b/src/components/landing/footer.tsx
new file mode 100644
index 0000000..da5a11a
--- /dev/null
+++ b/src/components/landing/footer.tsx
@@ -0,0 +1,107 @@
+import Link from 'next/link';
+import { ExternalLink } from 'lucide-react';
+import { SiGithub } from 'react-icons/si';
+
+const COLS: { title: string; links: { label: string; href: string; external?: boolean }[] }[] = [
+ {
+ title: 'App',
+ links: [
+ { label: 'Registry', href: '/app' },
+ { label: 'Wrap', href: '/app/wrap' },
+ { label: 'Portfolio', href: '/app/portfolio' },
+ { label: 'Analytics', href: '/app/analytics' },
+ { label: 'Faucet', href: '/app/faucet' },
+ ],
+ },
+ {
+ title: 'Developers',
+ links: [
+ { label: 'Documentation', href: '/app/docs' },
+ { label: 'Code snippets', href: '/app/developers' },
+ { label: 'REST API', href: '/api/registry?chain=sepolia' },
+ { label: 'GitHub', href: 'https://github.com/hosein-ul/zamavault', external: true },
+ ],
+ },
+ {
+ title: 'Resources',
+ links: [
+ { label: 'Tutorial — 5-min', href: '/app/learn' },
+ { label: 'Zama Protocol docs', href: 'https://docs.zama.org/protocol', external: true },
+ { label: 'ERC-7984 spec', href: 'https://eips.ethereum.org/', external: true },
+ ],
+ },
+];
+
+export function LandingFooter() {
+ return (
+
+ );
+}
diff --git a/src/components/landing/hero.tsx b/src/components/landing/hero.tsx
new file mode 100644
index 0000000..41d5dca
--- /dev/null
+++ b/src/components/landing/hero.tsx
@@ -0,0 +1,121 @@
+'use client';
+
+import Link from 'next/link';
+import { motion } from 'motion/react';
+import { ArrowUpRight, BookOpen } from 'lucide-react';
+import { BlurFade } from '@/components/magic/blur-fade';
+import { TextAnimate } from '@/components/magic/text-animate';
+import { AuroraBg } from './aurora-bg';
+
+/**
+ * Hero — cream background, gold aurora, smooth word-by-word reveal.
+ *
+ * Each headline line uses a single at default Magic UI timing
+ * (animation="blurInUp" by="word" once). No custom duration overrides — the
+ * library's defaults are tuned and the spacing-out is done purely with `delay`.
+ * Whole headline finishes in ~1.2s; CTAs/subhead follow with BlurFade.
+ *
+ * The entire hero is wrapped in a motion.div that fades from 0.95 → 1 so even
+ * on a slow device with delayed JS the content is readable instantly.
+ */
+export function Hero() {
+ return (
+
+
+
+
+
+ {/* Eyebrow */}
+
+
+
+ Fully Homomorphic Encryption · ERC-7984 · Live on Mainnet
+
+
+
+ {/* Headline — three lines, each a single TextAnimate at default timing */}
+
+
+ Your balance, invisible
+
+
+ to the world.
+
+
+ Visible only to you.
+
+
+
+ {/* Subhead */}
+
+
+ ZamaVault is the canonical interface for the Zama Confidential Wrappers Registry.
+ Browse every ERC-20 ↔ ERC-7984 token pair across Ethereum and Sepolia, shield your
+ balance, and decrypt with a single EIP-712 signature — only you can read it.
+
+
+
+ {/* CTAs */}
+
+
+
+ Launch ZamaVault
+
+
+
+
+ Learn in 5 minutes
+
+
+
+
+ {/* Trust strip */}
+
+
+
+
+ Live on Sepolia & Ethereum mainnet
+
+
·
+
Open source · MIT licensed
+
·
+
No custody · No tracking
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/how-it-works.tsx b/src/components/landing/how-it-works.tsx
new file mode 100644
index 0000000..e092cde
--- /dev/null
+++ b/src/components/landing/how-it-works.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { BlurFade } from '@/components/magic/blur-fade';
+import { TextAnimate } from '@/components/magic/text-animate';
+
+const STEPS = [
+ {
+ n: '01',
+ title: 'Discover',
+ body: 'Browse every registered ERC-20 ↔ ERC-7984 pair pulled live from the on-chain WrappersRegistry.',
+ },
+ {
+ n: '02',
+ title: 'Shield',
+ body: 'Approve once, wrap your tokens. The SDK auto-handles allowance, scaling, and the shield transaction.',
+ },
+ {
+ n: '03',
+ title: 'Decrypt',
+ body: 'Sign a single EIP-712 permit — your balance decrypts client-side. Nothing is stored on-chain in cleartext.',
+ },
+ {
+ n: '04',
+ title: 'Unshield',
+ body: 'Burn the encrypted balance, wait for the gateway proof, receive the underlying ERC-20 back in your wallet.',
+ },
+];
+
+export function HowItWorks() {
+ return (
+
+
+
+
+
+ ─ Four steps. That's it.
+
+
+ From public balance to private, and back, in under a minute.
+
+
+
+
+
+
+
+ {STEPS.map((s, i) => (
+
+
+
+
+ ●
+
+
+ {s.n}
+
+
+
+
+ {s.title}
+
+
+ {s.body}
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/nav.tsx b/src/components/landing/nav.tsx
new file mode 100644
index 0000000..e6138d1
--- /dev/null
+++ b/src/components/landing/nav.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import Link from 'next/link';
+import { ArrowUpRight } from 'lucide-react';
+import { SiGithub } from 'react-icons/si';
+
+/**
+ * Landing nav — sticky glass bar on cream background.
+ * Logo left, anchor links center, GitHub + Launch CTA right.
+ */
+export function LandingNav() {
+ return (
+
+ );
+}
diff --git a/src/components/landing/stats.tsx b/src/components/landing/stats.tsx
new file mode 100644
index 0000000..9fa2d92
--- /dev/null
+++ b/src/components/landing/stats.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { NumberTicker } from '@/components/magic/number-ticker';
+import { BlurFade } from '@/components/magic/blur-fade';
+
+const STATS = [
+ { value: 15, label: 'Confidential pairs registered', suffix: '' },
+ { value: 2, label: 'Networks supported', suffix: '' },
+ { value: 6, label: 'Decimals · always · for FHE', suffix: '' },
+ { value: 100, label: 'On-chain, on-the-fly decrypt', suffix: '%' },
+];
+
+export function Stats() {
+ return (
+
+
+
+ {STATS.map((s, i) => (
+
+
+
+
+ {s.suffix && {s.suffix} }
+
+
+ {s.label}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/trust-rail.tsx b/src/components/landing/trust-rail.tsx
new file mode 100644
index 0000000..39c9701
--- /dev/null
+++ b/src/components/landing/trust-rail.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { Marquee } from '@/components/magic/marquee';
+import { cn } from '@/lib/utils';
+
+const TOKENS = [
+ { sym: 'cUSDC', name: 'USD Coin', logo: 'https://s2.coinmarketcap.com/static/img/coins/64x64/3408.png' },
+ { sym: 'cUSDT', name: 'Tether USD', logo: 'https://s2.coinmarketcap.com/static/img/coins/64x64/825.png' },
+ { sym: 'cWETH', name: 'Wrapped Ether', logo: 'https://s2.coinmarketcap.com/static/img/coins/64x64/2396.png' },
+ { sym: 'cZAMA', name: 'Zama', logo: 'https://s2.coinmarketcap.com/static/img/coins/64x64/39332.png' },
+ { sym: 'cBRON', name: 'Bron', logo: null },
+ { sym: 'ctGBP', name: 'Tokenised GBP', logo: 'https://s2.coinmarketcap.com/static/img/coins/64x64/38935.png' },
+ { sym: 'cXAUt', name: 'Tether Gold', logo: 'https://s2.coinmarketcap.com/static/img/coins/64x64/5176.png' },
+];
+
+function Pill({ sym, name, logo }: typeof TOKENS[0]) {
+ return (
+
+ {logo ? (
+
+ ) : (
+
+ B
+
+ )}
+
{sym}
+
{name}
+
+ );
+}
+
+export function TrustRail() {
+ return (
+
+
+
+ Confidential token pairs supported
+
+
+ {TOKENS.map((t) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index c9131b5..9c56ee2 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -22,14 +22,14 @@ import {
} from 'lucide-react';
const NAV_ITEMS = [
- { href: '/', label: 'Registry' },
- { href: '/wrap', label: 'Wrap' },
- { href: '/portfolio', label: 'Portfolio' },
- { href: '/faucet', label: 'Faucet' },
- { href: '/learn', label: 'Learn' },
- { href: '/developers', label: 'Dev Tools' },
- { href: '/analytics', label: 'Analytics' },
- { href: '/docs', label: 'Docs' },
+ { href: '/app', label: 'Registry' },
+ { href: '/app/wrap', label: 'Wrap' },
+ { href: '/app/portfolio', label: 'Portfolio' },
+ { href: '/app/faucet', label: 'Faucet' },
+ { href: '/app/learn', label: 'Learn' },
+ { href: '/app/developers', label: 'Dev Tools' },
+ { href: '/app/analytics', label: 'Analytics' },
+ { href: '/app/docs', label: 'Docs' },
];
const THEME_OPTIONS: { value: DesignTheme; label: string }[] = [
@@ -83,7 +83,7 @@ export default function Header() {
{/* Logo */}
-
+
diff --git a/src/components/magic/animated-grid-pattern.tsx b/src/components/magic/animated-grid-pattern.tsx
new file mode 100644
index 0000000..249c19d
--- /dev/null
+++ b/src/components/magic/animated-grid-pattern.tsx
@@ -0,0 +1,177 @@
+"use client"
+
+import {
+ useCallback,
+ useEffect,
+ useId,
+ useRef,
+ useState,
+ type ComponentPropsWithoutRef,
+} from "react"
+import { motion } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+export interface AnimatedGridPatternProps extends ComponentPropsWithoutRef<"svg"> {
+ width?: number
+ height?: number
+ x?: number
+ y?: number
+ strokeDasharray?: number
+ numSquares?: number
+ maxOpacity?: number
+ duration?: number
+ repeatDelay?: number
+}
+
+type Square = {
+ id: number
+ pos: [number, number]
+ iteration: number
+}
+
+export function AnimatedGridPattern({
+ width = 40,
+ height = 40,
+ x = -1,
+ y = -1,
+ strokeDasharray = 0,
+ numSquares = 50,
+ className,
+ maxOpacity = 0.5,
+ duration = 4,
+ repeatDelay = 0.5,
+ ...props
+}: AnimatedGridPatternProps) {
+ const id = useId()
+ const containerRef = useRef(null)
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
+ const [squares, setSquares] = useState>([])
+
+ const getPos = useCallback((): [number, number] => {
+ return [
+ Math.floor((Math.random() * dimensions.width) / width),
+ Math.floor((Math.random() * dimensions.height) / height),
+ ]
+ }, [dimensions.height, dimensions.width, height, width])
+
+ const generateSquares = useCallback(
+ (count: number) => {
+ return Array.from({ length: count }, (_, i) => ({
+ id: i,
+ pos: getPos(),
+ iteration: 0,
+ }))
+ },
+ [getPos]
+ )
+
+ const updateSquarePosition = useCallback(
+ (squareId: number) => {
+ setSquares((currentSquares) => {
+ const current = currentSquares[squareId]
+ if (!current || current.id !== squareId) return currentSquares
+
+ const nextSquares = currentSquares.slice()
+ nextSquares[squareId] = {
+ ...current,
+ pos: getPos(),
+ iteration: current.iteration + 1,
+ }
+
+ return nextSquares
+ })
+ },
+ [getPos]
+ )
+
+ useEffect(() => {
+ if (dimensions.width && dimensions.height) {
+ setSquares(generateSquares(numSquares))
+ }
+ }, [dimensions.width, dimensions.height, generateSquares, numSquares])
+
+ useEffect(() => {
+ const element = containerRef.current
+ let resizeObserver: ResizeObserver | null = null
+
+ if (element) {
+ resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ setDimensions((currentDimensions) => {
+ const nextWidth = entry.contentRect.width
+ const nextHeight = entry.contentRect.height
+ if (
+ currentDimensions.width === nextWidth &&
+ currentDimensions.height === nextHeight
+ ) {
+ return currentDimensions
+ }
+ return { width: nextWidth, height: nextHeight }
+ })
+ }
+ })
+
+ resizeObserver.observe(element)
+ }
+
+ return () => {
+ if (resizeObserver) {
+ resizeObserver.disconnect()
+ }
+ }
+ }, [])
+
+ return (
+
+
+
+
+
+
+
+
+ {squares.map(({ pos: [squareX, squareY], id, iteration }, index) => (
+ updateSquarePosition(id)}
+ key={`${id}-${iteration}`}
+ width={width - 1}
+ height={height - 1}
+ x={squareX * width + 1}
+ y={squareY * height + 1}
+ fill="currentColor"
+ strokeWidth="0"
+ />
+ ))}
+
+
+ )
+}
diff --git a/src/components/magic/aurora-text.tsx b/src/components/magic/aurora-text.tsx
new file mode 100644
index 0000000..85857c1
--- /dev/null
+++ b/src/components/magic/aurora-text.tsx
@@ -0,0 +1,43 @@
+"use client"
+
+import React, { memo } from "react"
+
+interface AuroraTextProps {
+ children: React.ReactNode
+ className?: string
+ colors?: string[]
+ speed?: number
+}
+
+export const AuroraText = memo(
+ ({
+ children,
+ className = "",
+ colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
+ speed = 1,
+ }: AuroraTextProps) => {
+ const gradientStyle = {
+ backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
+ colors[0]
+ })`,
+ WebkitBackgroundClip: "text",
+ WebkitTextFillColor: "transparent",
+ animationDuration: `${10 / speed}s`,
+ }
+
+ return (
+
+ {children}
+
+ {children}
+
+
+ )
+ }
+)
+
+AuroraText.displayName = "AuroraText"
diff --git a/src/components/magic/bento-grid.tsx b/src/components/magic/bento-grid.tsx
new file mode 100644
index 0000000..13fa896
--- /dev/null
+++ b/src/components/magic/bento-grid.tsx
@@ -0,0 +1,109 @@
+import { type ComponentPropsWithoutRef, type ReactNode } from "react"
+import { ArrowRightIcon } from "@radix-ui/react-icons"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/magic/button"
+
+interface BentoGridProps extends ComponentPropsWithoutRef<"div"> {
+ children: ReactNode
+ className?: string
+}
+
+interface BentoCardProps extends ComponentPropsWithoutRef<"div"> {
+ name: string
+ className: string
+ background: ReactNode
+ Icon: React.ElementType
+ description: string
+ href: string
+ cta: string
+}
+
+const BentoGrid = ({ children, className, ...props }: BentoGridProps) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const BentoCard = ({
+ name,
+ className,
+ background,
+ Icon,
+ description,
+ href,
+ cta,
+ ...props
+}: BentoCardProps) => (
+
+
{background}
+
+
+
+
+ {name}
+
+
{description}
+
+
+
+
+
+
+
+
+
+)
+
+export { BentoCard, BentoGrid }
diff --git a/src/components/magic/blur-fade.tsx b/src/components/magic/blur-fade.tsx
new file mode 100644
index 0000000..fe7ca1e
--- /dev/null
+++ b/src/components/magic/blur-fade.tsx
@@ -0,0 +1,94 @@
+"use client"
+
+import { useRef } from "react"
+import {
+ AnimatePresence,
+ motion,
+ useInView,
+ type MotionProps,
+ type UseInViewOptions,
+ type Variants,
+} from "motion/react"
+
+type MarginType = UseInViewOptions["margin"]
+
+interface BlurFadeProps extends MotionProps {
+ children: React.ReactNode
+ className?: string
+ variant?: {
+ hidden: { y: number }
+ visible: { y: number }
+ }
+ duration?: number
+ delay?: number
+ offset?: number
+ direction?: "up" | "down" | "left" | "right"
+ inView?: boolean
+ inViewMargin?: MarginType
+ blur?: string
+}
+
+const getFilter = (v: Variants[string]) =>
+ typeof v === "function" ? undefined : v.filter
+
+export function BlurFade({
+ children,
+ className,
+ variant,
+ duration = 0.4,
+ delay = 0,
+ offset = 6,
+ direction = "down",
+ inView = false,
+ inViewMargin = "-50px",
+ blur = "6px",
+ ...props
+}: BlurFadeProps) {
+ const ref = useRef(null)
+ const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
+ const isInView = !inView || inViewResult
+ const defaultVariants: Variants = {
+ hidden: {
+ [direction === "left" || direction === "right" ? "x" : "y"]:
+ direction === "right" || direction === "down" ? -offset : offset,
+ opacity: 0,
+ filter: `blur(${blur})`,
+ },
+ visible: {
+ [direction === "left" || direction === "right" ? "x" : "y"]: 0,
+ opacity: 1,
+ filter: `blur(0px)`,
+ },
+ }
+ const combinedVariants = variant ?? defaultVariants
+
+ const hiddenFilter = getFilter(combinedVariants.hidden)
+ const visibleFilter = getFilter(combinedVariants.visible)
+
+ const shouldTransitionFilter =
+ hiddenFilter != null &&
+ visibleFilter != null &&
+ hiddenFilter !== visibleFilter
+
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/magic/border-beam.tsx b/src/components/magic/border-beam.tsx
new file mode 100644
index 0000000..1f594d1
--- /dev/null
+++ b/src/components/magic/border-beam.tsx
@@ -0,0 +1,107 @@
+"use client"
+
+import { motion, MotionStyle, Transition } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+interface BorderBeamProps {
+ /**
+ * The size of the border beam.
+ */
+ size?: number
+ /**
+ * The duration of the border beam.
+ */
+ duration?: number
+ /**
+ * The delay of the border beam.
+ */
+ delay?: number
+ /**
+ * The color of the border beam from.
+ */
+ colorFrom?: string
+ /**
+ * The color of the border beam to.
+ */
+ colorTo?: string
+ /**
+ * The motion transition of the border beam.
+ */
+ transition?: Transition
+ /**
+ * The class name of the border beam.
+ */
+ className?: string
+ /**
+ * The style of the border beam.
+ */
+ style?: React.CSSProperties
+ /**
+ * Whether to reverse the animation direction.
+ */
+ reverse?: boolean
+ /**
+ * The initial offset position (0-100).
+ */
+ initialOffset?: number
+ /**
+ * The border width of the beam.
+ */
+ borderWidth?: number
+}
+
+export const BorderBeam = ({
+ className,
+ size = 50,
+ delay = 0,
+ duration = 6,
+ colorFrom = "#ffaa40",
+ colorTo = "#9c40ff",
+ transition,
+ style,
+ reverse = false,
+ initialOffset = 0,
+ borderWidth = 1,
+}: BorderBeamProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/src/components/magic/button.tsx b/src/components/magic/button.tsx
new file mode 100644
index 0000000..4d38506
--- /dev/null
+++ b/src/components/magic/button.tsx
@@ -0,0 +1,64 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot.Root : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/magic/dot-pattern.tsx b/src/components/magic/dot-pattern.tsx
new file mode 100644
index 0000000..db8eb89
--- /dev/null
+++ b/src/components/magic/dot-pattern.tsx
@@ -0,0 +1,158 @@
+"use client"
+
+import React, { useEffect, useId, useRef, useState } from "react"
+import { motion } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+/**
+ * DotPattern Component Props
+ *
+ * @param {number} [width=16] - The horizontal spacing between dots
+ * @param {number} [height=16] - The vertical spacing between dots
+ * @param {number} [x=0] - The x-offset of the entire pattern
+ * @param {number} [y=0] - The y-offset of the entire pattern
+ * @param {number} [cx=1] - The x-offset of individual dots
+ * @param {number} [cy=1] - The y-offset of individual dots
+ * @param {number} [cr=1] - The radius of each dot
+ * @param {string} [className] - Additional CSS classes to apply to the SVG container
+ * @param {boolean} [glow=false] - Whether dots should have a glowing animation effect
+ */
+interface DotPatternProps extends React.SVGProps {
+ width?: number
+ height?: number
+ x?: number
+ y?: number
+ cx?: number
+ cy?: number
+ cr?: number
+ className?: string
+ glow?: boolean
+ [key: string]: unknown
+}
+
+/**
+ * DotPattern Component
+ *
+ * A React component that creates an animated or static dot pattern background using SVG.
+ * The pattern automatically adjusts to fill its container and can optionally display glowing dots.
+ *
+ * @component
+ *
+ * @see DotPatternProps for the props interface.
+ *
+ * @example
+ * // Basic usage
+ *
+ *
+ * // With glowing effect and custom spacing
+ *
+ *
+ * @notes
+ * - The component is client-side only ("use client")
+ * - Automatically responds to container size changes
+ * - When glow is enabled, dots will animate with random delays and durations
+ * - Uses Motion for animations
+ * - Dots color can be controlled via the text color utility classes
+ */
+
+export function DotPattern({
+ width = 16,
+ height = 16,
+ x = 0,
+ y = 0,
+ cx = 1,
+ cy = 1,
+ cr = 1,
+ className,
+ glow = false,
+ ...props
+}: DotPatternProps) {
+ const id = useId()
+ const containerRef = useRef(null)
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
+
+ useEffect(() => {
+ const updateDimensions = () => {
+ if (containerRef.current) {
+ const { width, height } = containerRef.current.getBoundingClientRect()
+ setDimensions({ width, height })
+ }
+ }
+
+ updateDimensions()
+ window.addEventListener("resize", updateDimensions)
+ return () => window.removeEventListener("resize", updateDimensions)
+ }, [])
+
+ const dots = Array.from(
+ {
+ length:
+ Math.ceil(dimensions.width / width) *
+ Math.ceil(dimensions.height / height),
+ },
+ (_, i) => {
+ const col = i % Math.ceil(dimensions.width / width)
+ const row = Math.floor(i / Math.ceil(dimensions.width / width))
+ return {
+ x: col * width + cx + x,
+ y: row * height + cy + y,
+ delay: Math.random() * 5,
+ duration: Math.random() * 3 + 2,
+ }
+ }
+ )
+
+ return (
+
+
+
+
+
+
+
+ {dots.map((dot) => (
+
+ ))}
+
+ )
+}
diff --git a/src/components/magic/magic-card.tsx b/src/components/magic/magic-card.tsx
new file mode 100644
index 0000000..3b022df
--- /dev/null
+++ b/src/components/magic/magic-card.tsx
@@ -0,0 +1,222 @@
+"use client"
+
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import {
+ motion,
+ useMotionTemplate,
+ useMotionValue,
+ useSpring,
+} from "motion/react"
+import { useTheme } from "next-themes"
+
+import { cn } from "@/lib/utils"
+
+interface MagicCardBaseProps {
+ children?: React.ReactNode
+ className?: string
+ gradientSize?: number
+ gradientFrom?: string
+ gradientTo?: string
+}
+
+interface MagicCardGradientProps extends MagicCardBaseProps {
+ mode?: "gradient"
+
+ gradientColor?: string
+ gradientOpacity?: number
+
+ glowFrom?: never
+ glowTo?: never
+ glowAngle?: never
+ glowSize?: never
+ glowBlur?: never
+ glowOpacity?: never
+}
+
+interface MagicCardOrbProps extends MagicCardBaseProps {
+ mode: "orb"
+
+ glowFrom?: string
+ glowTo?: string
+ glowAngle?: number
+ glowSize?: number
+ glowBlur?: number
+ glowOpacity?: number
+
+ gradientColor?: never
+ gradientOpacity?: never
+}
+
+type MagicCardProps = MagicCardGradientProps | MagicCardOrbProps
+type ResetReason = "enter" | "leave" | "global" | "init"
+
+function isOrbMode(props: MagicCardProps): props is MagicCardOrbProps {
+ return props.mode === "orb"
+}
+
+export function MagicCard(props: MagicCardProps) {
+ const {
+ children,
+ className,
+ gradientSize = 200,
+ gradientColor = "#262626",
+ gradientOpacity = 0.8,
+ gradientFrom = "#9E7AFF",
+ gradientTo = "#FE8BBB",
+ mode = "gradient",
+ } = props
+
+ const glowFrom = isOrbMode(props) ? (props.glowFrom ?? "#ee4f27") : "#ee4f27"
+ const glowTo = isOrbMode(props) ? (props.glowTo ?? "#6b21ef") : "#6b21ef"
+ const glowAngle = isOrbMode(props) ? (props.glowAngle ?? 90) : 90
+ const glowSize = isOrbMode(props) ? (props.glowSize ?? 420) : 420
+ const glowBlur = isOrbMode(props) ? (props.glowBlur ?? 60) : 60
+ const glowOpacity = isOrbMode(props) ? (props.glowOpacity ?? 0.9) : 0.9
+ const { theme, systemTheme } = useTheme()
+ const [mounted, setMounted] = useState(false)
+
+ useEffect(() => setMounted(true), [])
+
+ const isDarkTheme = useMemo(() => {
+ if (!mounted) return true
+ const currentTheme = theme === "system" ? systemTheme : theme
+ return currentTheme === "dark"
+ }, [theme, systemTheme, mounted])
+
+ const mouseX = useMotionValue(-gradientSize)
+ const mouseY = useMotionValue(-gradientSize)
+
+ const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 })
+ const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 })
+ const orbVisible = useSpring(0, { stiffness: 300, damping: 35 })
+
+ const modeRef = useRef(mode)
+ const glowOpacityRef = useRef(glowOpacity)
+ const gradientSizeRef = useRef(gradientSize)
+
+ useEffect(() => {
+ modeRef.current = mode
+ }, [mode])
+
+ useEffect(() => {
+ glowOpacityRef.current = glowOpacity
+ }, [glowOpacity])
+
+ useEffect(() => {
+ gradientSizeRef.current = gradientSize
+ }, [gradientSize])
+
+ const reset = useCallback(
+ (reason: ResetReason = "leave") => {
+ const currentMode = modeRef.current
+
+ if (currentMode === "orb") {
+ if (reason === "enter") orbVisible.set(glowOpacityRef.current)
+ else orbVisible.set(0)
+ return
+ }
+
+ const off = -gradientSizeRef.current
+ mouseX.set(off)
+ mouseY.set(off)
+ },
+ [mouseX, mouseY, orbVisible]
+ )
+
+ const handlePointerMove = useCallback(
+ (e: React.PointerEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect()
+ mouseX.set(e.clientX - rect.left)
+ mouseY.set(e.clientY - rect.top)
+ },
+ [mouseX, mouseY]
+ )
+
+ useEffect(() => {
+ reset("init")
+ }, [reset])
+
+ useEffect(() => {
+ const handleGlobalPointerOut = (e: PointerEvent) => {
+ if (!e.relatedTarget) reset("global")
+ }
+ const handleBlur = () => reset("global")
+ const handleVisibility = () => {
+ if (document.visibilityState !== "visible") reset("global")
+ }
+
+ window.addEventListener("pointerout", handleGlobalPointerOut)
+ window.addEventListener("blur", handleBlur)
+ document.addEventListener("visibilitychange", handleVisibility)
+
+ return () => {
+ window.removeEventListener("pointerout", handleGlobalPointerOut)
+ window.removeEventListener("blur", handleBlur)
+ document.removeEventListener("visibilitychange", handleVisibility)
+ }
+ }, [reset])
+
+ return (
+ reset("leave")}
+ onPointerEnter={() => reset("enter")}
+ style={{
+ background: useMotionTemplate`
+ linear-gradient(var(--color-background) 0 0) padding-box,
+ radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px,
+ ${gradientFrom},
+ ${gradientTo},
+ var(--color-border) 100%
+ ) border-box
+ `,
+ }}
+ >
+
+
+ {mode === "gradient" && (
+
+ )}
+
+ {mode === "orb" && (
+
+ )}
+ {children}
+
+ )
+}
diff --git a/src/components/magic/marquee.tsx b/src/components/magic/marquee.tsx
new file mode 100644
index 0000000..c86ab57
--- /dev/null
+++ b/src/components/magic/marquee.tsx
@@ -0,0 +1,74 @@
+import { type ComponentPropsWithoutRef } from "react"
+
+import { cn } from "@/lib/utils"
+
+interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
+ /**
+ * Optional CSS class name to apply custom styles
+ */
+ className?: string
+ /**
+ * Whether to reverse the animation direction
+ * @default false
+ */
+ reverse?: boolean
+ /**
+ * Whether to pause the animation on hover
+ * @default false
+ */
+ pauseOnHover?: boolean
+ /**
+ * Content to be displayed in the marquee
+ */
+ children: React.ReactNode
+ /**
+ * Whether to animate vertically instead of horizontally
+ * @default false
+ */
+ vertical?: boolean
+ /**
+ * Number of times to repeat the content
+ * @default 4
+ */
+ repeat?: number
+}
+
+export function Marquee({
+ className,
+ reverse = false,
+ pauseOnHover = false,
+ children,
+ vertical = false,
+ repeat = 4,
+ ...props
+}: MarqueeProps) {
+ return (
+
+ {Array(repeat)
+ .fill(0)
+ .map((_, i) => (
+
+ {children}
+
+ ))}
+
+ )
+}
diff --git a/src/components/magic/number-ticker.tsx b/src/components/magic/number-ticker.tsx
new file mode 100644
index 0000000..99ef26e
--- /dev/null
+++ b/src/components/magic/number-ticker.tsx
@@ -0,0 +1,74 @@
+"use client"
+
+import { useEffect, useRef, type ComponentPropsWithoutRef } from "react"
+import { useInView, useMotionValue, useSpring } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
+ value: number
+ startValue?: number
+ direction?: "up" | "down"
+ delay?: number
+ decimalPlaces?: number
+}
+
+export function NumberTicker({
+ value,
+ startValue = 0,
+ direction = "up",
+ delay = 0,
+ className,
+ decimalPlaces = 0,
+ ...props
+}: NumberTickerProps) {
+ const ref = useRef(null)
+ const motionValue = useMotionValue(direction === "down" ? value : startValue)
+ const springValue = useSpring(motionValue, {
+ damping: 60,
+ stiffness: 100,
+ })
+ const isInView = useInView(ref, { once: true, margin: "0px" })
+
+ useEffect(() => {
+ let timer: ReturnType | null = null
+
+ if (isInView) {
+ timer = setTimeout(() => {
+ motionValue.set(direction === "down" ? startValue : value)
+ }, delay * 1000)
+ }
+
+ return () => {
+ if (timer !== null) {
+ clearTimeout(timer)
+ }
+ }
+ }, [motionValue, isInView, delay, value, direction, startValue])
+
+ useEffect(
+ () =>
+ springValue.on("change", (latest) => {
+ if (ref.current) {
+ ref.current.textContent = Intl.NumberFormat("en-US", {
+ minimumFractionDigits: decimalPlaces,
+ maximumFractionDigits: decimalPlaces,
+ }).format(Number(latest.toFixed(decimalPlaces)))
+ }
+ }),
+ [springValue, decimalPlaces]
+ )
+
+ return (
+
+ {startValue}
+
+ )
+}
diff --git a/src/components/magic/shimmer-button.tsx b/src/components/magic/shimmer-button.tsx
new file mode 100644
index 0000000..d675cc0
--- /dev/null
+++ b/src/components/magic/shimmer-button.tsx
@@ -0,0 +1,96 @@
+import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> {
+ shimmerColor?: string
+ shimmerSize?: string
+ borderRadius?: string
+ shimmerDuration?: string
+ background?: string
+ className?: string
+ children?: React.ReactNode
+}
+
+export const ShimmerButton = React.forwardRef<
+ HTMLButtonElement,
+ ShimmerButtonProps
+>(
+ (
+ {
+ shimmerColor = "#ffffff",
+ shimmerSize = "0.05em",
+ shimmerDuration = "3s",
+ borderRadius = "100px",
+ background = "rgba(0, 0, 0, 1)",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+ {/* spark container */}
+
+ {/* spark */}
+
+ {/* spark before */}
+
+
+
+ {children}
+
+ {/* Highlight */}
+
+
+ {/* backdrop */}
+
+
+ )
+ }
+)
+
+ShimmerButton.displayName = "ShimmerButton"
diff --git a/src/components/magic/shiny-button.tsx b/src/components/magic/shiny-button.tsx
new file mode 100644
index 0000000..191108a
--- /dev/null
+++ b/src/components/magic/shiny-button.tsx
@@ -0,0 +1,74 @@
+"use client"
+
+import React from "react"
+import { motion, type MotionProps } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+const animationProps: MotionProps = {
+ initial: { "--x": "100%", scale: 0.8 },
+ animate: { "--x": "-100%", scale: 1 },
+ whileTap: { scale: 0.95 },
+ transition: {
+ repeat: Infinity,
+ repeatType: "loop",
+ repeatDelay: 1,
+ type: "spring",
+ stiffness: 20,
+ damping: 15,
+ mass: 2,
+ scale: {
+ type: "spring",
+ stiffness: 200,
+ damping: 5,
+ mass: 0.5,
+ },
+ },
+}
+
+interface ShinyButtonProps
+ extends
+ Omit, keyof MotionProps>,
+ MotionProps {
+ children: React.ReactNode
+ className?: string
+}
+
+export const ShinyButton = React.forwardRef<
+ HTMLButtonElement,
+ ShinyButtonProps
+>(({ children, className, ...props }, ref) => {
+ return (
+
+
+ {children}
+
+
+
+ )
+})
+
+ShinyButton.displayName = "ShinyButton"
diff --git a/src/components/magic/text-animate.tsx b/src/components/magic/text-animate.tsx
new file mode 100644
index 0000000..ce2b453
--- /dev/null
+++ b/src/components/magic/text-animate.tsx
@@ -0,0 +1,445 @@
+"use client"
+
+import { memo } from "react"
+import {
+ AnimatePresence,
+ motion,
+ Variants,
+ type DOMMotionComponents,
+ type MotionProps,
+} from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+type AnimationType = "text" | "word" | "character" | "line"
+type AnimationVariant =
+ | "fadeIn"
+ | "blurIn"
+ | "blurInUp"
+ | "blurInDown"
+ | "slideUp"
+ | "slideDown"
+ | "slideLeft"
+ | "slideRight"
+ | "scaleUp"
+ | "scaleDown"
+
+const motionElements = {
+ article: motion.article,
+ div: motion.div,
+ h1: motion.h1,
+ h2: motion.h2,
+ h3: motion.h3,
+ h4: motion.h4,
+ h5: motion.h5,
+ h6: motion.h6,
+ li: motion.li,
+ p: motion.p,
+ section: motion.section,
+ span: motion.span,
+} as const
+
+type MotionElementType = Extract<
+ keyof DOMMotionComponents,
+ keyof typeof motionElements
+>
+
+interface TextAnimateProps extends Omit {
+ /**
+ * The text content to animate
+ */
+ children: string
+ /**
+ * The class name to be applied to the component
+ */
+ className?: string
+ /**
+ * The class name to be applied to each segment
+ */
+ segmentClassName?: string
+ /**
+ * The delay before the animation starts
+ */
+ delay?: number
+ /**
+ * The duration of the animation
+ */
+ duration?: number
+ /**
+ * Custom motion variants for the animation
+ */
+ variants?: Variants
+ /**
+ * The element type to render
+ */
+ as?: MotionElementType
+ /**
+ * How to split the text ("text", "word", "character")
+ */
+ by?: AnimationType
+ /**
+ * Whether to start animation when component enters viewport
+ */
+ startOnView?: boolean
+ /**
+ * Whether to animate only once
+ */
+ once?: boolean
+ /**
+ * The animation preset to use
+ */
+ animation?: AnimationVariant
+ /**
+ * Whether to enable accessibility features (default: true)
+ */
+ accessible?: boolean
+}
+
+const staggerTimings: Record = {
+ text: 0.06,
+ word: 0.05,
+ character: 0.03,
+ line: 0.06,
+}
+
+const defaultContainerVariants = {
+ hidden: { opacity: 1 },
+ show: {
+ opacity: 1,
+ transition: {
+ delayChildren: 0,
+ staggerChildren: 0.05,
+ },
+ },
+ exit: {
+ opacity: 0,
+ transition: {
+ staggerChildren: 0.05,
+ staggerDirection: -1,
+ },
+ },
+}
+
+const defaultItemVariants: Variants = {
+ hidden: { opacity: 0 },
+ show: {
+ opacity: 1,
+ },
+ exit: {
+ opacity: 0,
+ },
+}
+
+const defaultItemAnimationVariants: Record<
+ AnimationVariant,
+ { container: Variants; item: Variants }
+> = {
+ fadeIn: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { opacity: 0, y: 20 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.3,
+ },
+ },
+ exit: {
+ opacity: 0,
+ y: 20,
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+ blurIn: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { opacity: 0, filter: "blur(10px)" },
+ show: {
+ opacity: 1,
+ filter: "blur(0px)",
+ transition: {
+ duration: 0.3,
+ },
+ },
+ exit: {
+ opacity: 0,
+ filter: "blur(10px)",
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+ blurInUp: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { opacity: 0, filter: "blur(10px)", y: 20 },
+ show: {
+ opacity: 1,
+ filter: "blur(0px)",
+ y: 0,
+ transition: {
+ y: { duration: 0.3 },
+ opacity: { duration: 0.4 },
+ filter: { duration: 0.3 },
+ },
+ },
+ exit: {
+ opacity: 0,
+ filter: "blur(10px)",
+ y: 20,
+ transition: {
+ y: { duration: 0.3 },
+ opacity: { duration: 0.4 },
+ filter: { duration: 0.3 },
+ },
+ },
+ },
+ },
+ blurInDown: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { opacity: 0, filter: "blur(10px)", y: -20 },
+ show: {
+ opacity: 1,
+ filter: "blur(0px)",
+ y: 0,
+ transition: {
+ y: { duration: 0.3 },
+ opacity: { duration: 0.4 },
+ filter: { duration: 0.3 },
+ },
+ },
+ },
+ },
+ slideUp: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { y: 20, opacity: 0 },
+ show: {
+ y: 0,
+ opacity: 1,
+ transition: {
+ duration: 0.3,
+ },
+ },
+ exit: {
+ y: -20,
+ opacity: 0,
+ transition: {
+ duration: 0.3,
+ },
+ },
+ },
+ },
+ slideDown: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { y: -20, opacity: 0 },
+ show: {
+ y: 0,
+ opacity: 1,
+ transition: { duration: 0.3 },
+ },
+ exit: {
+ y: 20,
+ opacity: 0,
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+ slideLeft: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { x: 20, opacity: 0 },
+ show: {
+ x: 0,
+ opacity: 1,
+ transition: { duration: 0.3 },
+ },
+ exit: {
+ x: -20,
+ opacity: 0,
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+ slideRight: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { x: -20, opacity: 0 },
+ show: {
+ x: 0,
+ opacity: 1,
+ transition: { duration: 0.3 },
+ },
+ exit: {
+ x: 20,
+ opacity: 0,
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+ scaleUp: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { scale: 0.5, opacity: 0 },
+ show: {
+ scale: 1,
+ opacity: 1,
+ transition: {
+ duration: 0.3,
+ scale: {
+ type: "spring",
+ damping: 15,
+ stiffness: 300,
+ },
+ },
+ },
+ exit: {
+ scale: 0.5,
+ opacity: 0,
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+ scaleDown: {
+ container: defaultContainerVariants,
+ item: {
+ hidden: { scale: 1.5, opacity: 0 },
+ show: {
+ scale: 1,
+ opacity: 1,
+ transition: {
+ duration: 0.3,
+ scale: {
+ type: "spring",
+ damping: 15,
+ stiffness: 300,
+ },
+ },
+ },
+ exit: {
+ scale: 1.5,
+ opacity: 0,
+ transition: { duration: 0.3 },
+ },
+ },
+ },
+}
+
+const TextAnimateBase = ({
+ children,
+ delay = 0,
+ duration = 0.3,
+ variants,
+ className,
+ segmentClassName,
+ as: Component = "p",
+ startOnView = true,
+ once = false,
+ by = "word",
+ animation = "fadeIn",
+ accessible = true,
+ ...props
+}: TextAnimateProps) => {
+ const MotionComponent = motionElements[Component]
+
+ let segments: string[] = []
+ switch (by) {
+ case "word":
+ segments = children.split(/(\s+)/)
+ break
+ case "character":
+ segments = children.split("")
+ break
+ case "line":
+ segments = children.split("\n")
+ break
+ case "text":
+ default:
+ segments = [children]
+ break
+ }
+
+ const finalVariants = variants
+ ? {
+ container: {
+ hidden: { opacity: 0 },
+ show: {
+ opacity: 1,
+ transition: {
+ opacity: { duration: 0.01, delay },
+ delayChildren: delay,
+ staggerChildren: duration / segments.length,
+ },
+ },
+ exit: {
+ opacity: 0,
+ transition: {
+ staggerChildren: duration / segments.length,
+ staggerDirection: -1,
+ },
+ },
+ },
+ item: variants,
+ }
+ : animation
+ ? {
+ container: {
+ ...defaultItemAnimationVariants[animation].container,
+ show: {
+ ...defaultItemAnimationVariants[animation].container.show,
+ transition: {
+ delayChildren: delay,
+ staggerChildren: duration / segments.length,
+ },
+ },
+ exit: {
+ ...defaultItemAnimationVariants[animation].container.exit,
+ transition: {
+ staggerChildren: duration / segments.length,
+ staggerDirection: -1,
+ },
+ },
+ },
+ item: defaultItemAnimationVariants[animation].item,
+ }
+ : { container: defaultContainerVariants, item: defaultItemVariants }
+
+ return (
+
+
+ {accessible && {children} }
+ {segments.map((segment, i) => (
+
+ {segment}
+
+ ))}
+
+
+ )
+}
+
+// Export the memoized version
+export const TextAnimate = memo(TextAnimateBase)
diff --git a/src/components/magic/typing-animation.tsx b/src/components/magic/typing-animation.tsx
new file mode 100644
index 0000000..73f52a4
--- /dev/null
+++ b/src/components/magic/typing-animation.tsx
@@ -0,0 +1,236 @@
+"use client"
+
+import {
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ComponentType,
+ type RefAttributes,
+ type RefObject,
+} from "react"
+import {
+ motion,
+ useInView,
+ type DOMMotionComponents,
+ type HTMLMotionProps,
+ type MotionProps,
+} from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+const motionElements = {
+ article: motion.article,
+ div: motion.div,
+ h1: motion.h1,
+ h2: motion.h2,
+ h3: motion.h3,
+ h4: motion.h4,
+ h5: motion.h5,
+ h6: motion.h6,
+ li: motion.li,
+ p: motion.p,
+ section: motion.section,
+ span: motion.span,
+} as const
+
+type MotionElementType = Extract<
+ keyof DOMMotionComponents,
+ keyof typeof motionElements
+>
+type TypingAnimationMotionComponent = ComponentType<
+ Omit, "ref"> & RefAttributes
+>
+
+interface TypingAnimationProps extends Omit {
+ children?: string
+ words?: string[]
+ className?: string
+ duration?: number
+ typeSpeed?: number
+ deleteSpeed?: number
+ delay?: number
+ pauseDelay?: number
+ loop?: boolean
+ as?: MotionElementType
+ startOnView?: boolean
+ showCursor?: boolean
+ blinkCursor?: boolean
+ cursorStyle?: "line" | "block" | "underscore"
+}
+
+export function TypingAnimation({
+ children,
+ words,
+ className,
+ duration = 100,
+ typeSpeed,
+ deleteSpeed,
+ delay = 0,
+ pauseDelay = 1000,
+ loop = false,
+ as: Component = "span",
+ startOnView = true,
+ showCursor = true,
+ blinkCursor = true,
+ cursorStyle = "line",
+ ...props
+}: TypingAnimationProps) {
+ const MotionComponent = motionElements[
+ Component
+ ] as TypingAnimationMotionComponent
+
+ const [displayedText, setDisplayedText] = useState("")
+ const [currentWordIndex, setCurrentWordIndex] = useState(0)
+ const [currentCharIndex, setCurrentCharIndex] = useState(0)
+ const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing")
+ const elementRef = useRef(null)
+ const isInView = useInView(elementRef as RefObject, {
+ amount: 0.3,
+ once: true,
+ })
+
+ const wordsToAnimate = useMemo(
+ () => words ?? (children ? [children] : []),
+ [words, children]
+ )
+ const hasMultipleWords = wordsToAnimate.length > 1
+
+ const typingSpeed = typeSpeed ?? duration
+ const deletingSpeed = deleteSpeed ?? typingSpeed / 2
+
+ const shouldStart = startOnView ? isInView : true
+ const animationSourceKey = useMemo(
+ () => (words ? words.join("\u0000") : (children ?? "")),
+ [words, children]
+ )
+
+ useEffect(() => {
+ setDisplayedText("")
+ setCurrentWordIndex(0)
+ setCurrentCharIndex(0)
+ setPhase("typing")
+ }, [animationSourceKey])
+
+ useEffect(() => {
+ let timeout: ReturnType | null = null
+
+ if (shouldStart && wordsToAnimate.length > 0) {
+ const timeoutDelay =
+ delay > 0 && displayedText === ""
+ ? delay
+ : phase === "typing"
+ ? typingSpeed
+ : phase === "deleting"
+ ? deletingSpeed
+ : pauseDelay
+
+ timeout = setTimeout(() => {
+ const currentWord = wordsToAnimate[currentWordIndex] || ""
+ const graphemes = Array.from(currentWord)
+
+ switch (phase) {
+ case "typing":
+ if (currentCharIndex < graphemes.length) {
+ setDisplayedText(
+ graphemes.slice(0, currentCharIndex + 1).join("")
+ )
+ setCurrentCharIndex(currentCharIndex + 1)
+ } else {
+ if (hasMultipleWords || loop) {
+ const isLastWord =
+ currentWordIndex === wordsToAnimate.length - 1
+ if (!isLastWord || loop) {
+ setPhase("pause")
+ }
+ }
+ }
+ break
+
+ case "pause":
+ setPhase("deleting")
+ break
+
+ case "deleting":
+ if (currentCharIndex > 0) {
+ setDisplayedText(
+ graphemes.slice(0, currentCharIndex - 1).join("")
+ )
+ setCurrentCharIndex(currentCharIndex - 1)
+ } else {
+ const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
+ setCurrentWordIndex(nextIndex)
+ setPhase("typing")
+ }
+ break
+ }
+ }, timeoutDelay)
+ }
+
+ return () => {
+ if (timeout !== null) {
+ clearTimeout(timeout)
+ }
+ }
+ }, [
+ shouldStart,
+ phase,
+ currentCharIndex,
+ currentWordIndex,
+ displayedText,
+ wordsToAnimate,
+ hasMultipleWords,
+ loop,
+ typingSpeed,
+ deletingSpeed,
+ pauseDelay,
+ delay,
+ ])
+
+ const currentWordGraphemes = Array.from(
+ wordsToAnimate[currentWordIndex] || ""
+ )
+ const isComplete =
+ !loop &&
+ currentWordIndex === wordsToAnimate.length - 1 &&
+ currentCharIndex >= currentWordGraphemes.length &&
+ phase !== "deleting"
+
+ const shouldShowCursor =
+ showCursor &&
+ !isComplete &&
+ (hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
+
+ const getCursorChar = () => {
+ switch (cursorStyle) {
+ case "block":
+ return "▌"
+ case "underscore":
+ return "_"
+ case "line":
+ default:
+ return "|"
+ }
+ }
+
+ return (
+
+ {displayedText}
+ {shouldShowCursor && (
+
+ {getCursorChar()}
+
+ )}
+
+ )
+}
diff --git a/src/components/magic/warp-background.tsx b/src/components/magic/warp-background.tsx
new file mode 100644
index 0000000..9e05319
--- /dev/null
+++ b/src/components/magic/warp-background.tsx
@@ -0,0 +1,152 @@
+"use client"
+
+import React, { HTMLAttributes, useCallback, useMemo } from "react"
+import { motion } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+interface WarpBackgroundProps extends HTMLAttributes {
+ children: React.ReactNode
+ perspective?: number
+ beamsPerSide?: number
+ beamSize?: number
+ beamDelayMax?: number
+ beamDelayMin?: number
+ beamDuration?: number
+ gridColor?: string
+}
+
+const Beam = ({
+ width,
+ x,
+ delay,
+ duration,
+}: {
+ width: string | number
+ x: string | number
+ delay: number
+ duration: number
+}) => {
+ const hue = Math.floor(Math.random() * 360)
+ const ar = Math.floor(Math.random() * 10) + 1
+
+ return (
+
+ )
+}
+
+export const WarpBackground: React.FC = ({
+ children,
+ perspective = 100,
+ className,
+ beamsPerSide = 3,
+ beamSize = 5,
+ beamDelayMax = 3,
+ beamDelayMin = 0,
+ beamDuration = 3,
+ gridColor = "var(--border)",
+ ...props
+}) => {
+ const generateBeams = useCallback(() => {
+ const beams = []
+ const cellsPerSide = Math.floor(100 / beamSize)
+ const step = cellsPerSide / beamsPerSide
+
+ for (let i = 0; i < beamsPerSide; i++) {
+ const x = Math.floor(i * step)
+ const delay = Math.random() * (beamDelayMax - beamDelayMin) + beamDelayMin
+ beams.push({ x, delay })
+ }
+ return beams
+ }, [beamsPerSide, beamSize, beamDelayMax, beamDelayMin])
+
+ const topBeams = useMemo(() => generateBeams(), [generateBeams])
+ const rightBeams = useMemo(() => generateBeams(), [generateBeams])
+ const bottomBeams = useMemo(() => generateBeams(), [generateBeams])
+ const leftBeams = useMemo(() => generateBeams(), [generateBeams])
+
+ return (
+
+
+ {/* top side */}
+
+ {topBeams.map((beam, index) => (
+
+ ))}
+
+ {/* bottom side */}
+
+ {bottomBeams.map((beam, index) => (
+
+ ))}
+
+ {/* left side */}
+
+ {leftBeams.map((beam, index) => (
+
+ ))}
+
+ {/* right side */}
+
+ {rightBeams.map((beam, index) => (
+
+ ))}
+
+
+
{children}
+
+ )
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 94aea3f..2ebf780 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,3 +1,5 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
import { getExplorerUrl } from '@/config/chains';
/**
@@ -55,11 +57,19 @@ export function getExplorerAddressUrl(chainId: number, address: string): string
}
/**
- * Join class names, filtering out falsy values.
- * Usage: cn('base', isActive && 'active', variant === 'primary' && 'btn-primary')
+ * Class-name combiner. Accepts strings, objects, arrays, and falsy values
+ * (the clsx/shadcn input shape) and runs the result through `twMerge` so
+ * conflicting Tailwind utilities collapse correctly on the landing page.
+ *
+ * Backward compatible — old callers passing only strings keep working.
+ *
+ * Usage:
+ * cn('base', isActive && 'active')
+ * cn('base', { 'is-open': open, hidden: !visible })
+ * cn('px-2 py-1', 'px-4') // → 'py-1 px-4' (twMerge resolves conflict)
*/
-export function cn(...classes: (string | undefined | false | null)[]): string {
- return classes.filter(Boolean).join(' ');
+export function cn(...classes: ClassValue[]): string {
+ return twMerge(clsx(classes));
}
/**
From 9be6512cfa4ed35bb6395ae64ef8a68376cb1b2a Mon Sep 17 00:00:00 2001
From: hosein-ul
Date: Thu, 25 Jun 2026 15:08:34 +0300
Subject: [PATCH 24/69] docs: rewrite README to reflect current architecture
The previous README was significantly out of date:
- claimed a single-page app at /
- named four design themes that no longer exist
(Nordic Clean / Cyber / Nebula / Emerald)
- omitted every page added since (Landing, Analytics, Learn,
Dev-Tools, Docs, REST API endpoint)
- mis-described the unshield flow as a single transaction
The new README covers:
- accurate /app/* routes table
- the two-design-system architecture (landing.css vs globals.css)
- ERC-1363 single-tx shield path vs approve+wrap two-tx path,
citing the Zama docs
- two-phase unshield with Gateway finalize + resume banner
- /api/registry REST endpoint schema and example response
- decimal-scaling note (wrappers always 6 decimals due to euint64)
- accurate tech stack pulled from package.json
- project conventions (no auto-fire permits, strict TS, two-design
systems never mix)
---
README.md | 292 ++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 219 insertions(+), 73 deletions(-)
diff --git a/README.md b/README.md
index 1e07fb5..122ec7d 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,258 @@
-# 🛡️ ZamaVault — Confidential Asset Vault
+# ZamaVault
-ZamaVault is a production-grade Web3 application built on top of the **Zama Protocol** and **ERC-7984 Confidential Token Standard**. It provides a premium, highly responsive interface for shielding public ERC-20 tokens into encrypted, privacy-preserving ERC-7984 confidential tokens and unshielding them back to public balances.
+> The canonical web interface for the Zama Confidential Wrappers Registry — discover every ERC-20 ↔ ERC-7984 pair, shield in seconds, decrypt on demand.
-Using Fully Homomorphic Encryption (FHE), ZamaVault ensures that your token balances and transaction amounts remain completely encrypted on-chain, while allowing you to securely decrypt and inspect your balances locally using cryptographic permits.
+[](https://nextjs.org/)
+[](https://react.dev/)
+[](https://wagmi.sh/)
+[](https://docs.zama.org/protocol)
+[](https://www.typescriptlang.org/)
+[](LICENSE)
---
-## ✨ Features
+## What it is
-* **🔒 Shielding (Wrap)**: Convert public ERC-20 tokens into encrypted ERC-7984 confidential wrappers. On-chain balances and transfer amounts are completely obfuscated.
-* **🔓 Unshielding (Unwrap)**: Convert encrypted ERC-7984 confidential wrappers back into public ERC-20 tokens.
-* **💼 Confidential Portfolio**: View all confidential assets in one place. Balances remain encrypted on-chain, but can be decrypted on the client side using **EIP-712 permit signatures**.
-* **📊 Wrapper Registry Explorer**: Reads Zama's on-chain `WrappersRegistry` directly (via `useListPairs` from `@zama-fhe/react-sdk`) and lists every registered ERC-20 ↔ ERC-7984 pair on the active network — including pairs added after this client was built. Falls back to a hardcoded snapshot when no wallet is connected so anonymous visitors can still browse. Revoked pairs (`isValid === false`) are flagged with a "Revoked" badge and hidden from the wrap/unwrap selector.
-* **🚰 Test Faucet**: A built-in developer faucet with a **5-second cooldown** to acquire test tokens (USDC, USDT, WETH, ZAMA, etc.) on the Sepolia network.
-* **🎨 Nordic Clean Aesthetic**: A default premium dark-themed design system featuring smooth micro-animations, glassmorphism card layouts, custom typography, and official brand logos.
-* **🎉 Transaction Feedback & Delighters**: Custom transaction tracking with direct links to block explorers and interactive confetti blasts on successful wrap/unwrap actions.
+ZamaVault is a Next.js dApp that turns the on-chain `WrappersRegistry` into a product. Every registered ERC-20 ↔ ERC-7984 confidential wrapper pair on **Sepolia** and **Ethereum mainnet** is discovered live; users can shield (wrap) public tokens into encrypted balances, transfer them privately, decrypt with a single EIP-712 signature, and unshield back to public ERC-20 — all from one consistent interface.
+
+Built around the official `@zama-fhe/react-sdk` and `WrappersRegistry` contract, it serves both end-users (Portfolio, Wrap, Faucet, Learn) and developers (REST API, snippet generator, on-chain Analytics).
---
-## 🏗️ Technical Architecture & Stack
+## Pages & routes
-ZamaVault is designed as a secure, decentralized client-side application that interfaces with the Ethereum blockchain:
+| Route | Purpose |
+| ---------------------- | -------------------------------------------------------------------------------------- |
+| `/` | Marketing landing page with scroll-narrative onboarding |
+| `/app` | Live wrapper registry table with per-row public + confidential balance reads |
+| `/app/wrap` | Shield (wrap) and unshield (unwrap) swap interface for any registered pair |
+| `/app/portfolio` | Confidential portfolio with batch EIP-712 decrypt, re-shield, and unshield |
+| `/app/analytics` | TVS (Total Value Shielded) by token + 24h shield/unshield activity, since 00:00 UTC |
+| `/app/faucet` | Sepolia mock token mint (USDC, USDT, WETH, BRON, ZAMA, tGBP, XAUt) |
+| `/app/learn` | 5-step interactive tutorial covering FHE → faucet → shield → decrypt → unshield |
+| `/app/developers` | Copy-paste code snippets (React hook / viem / ethers) |
+| `/app/docs` | In-app reference for the SDK calls ZamaVault uses |
+| `/api/registry` | Public REST endpoint returning every wrapper pair on a given chain |
-* **Frontend Framework**: [Next.js](https://nextjs.org/) (App Router, static compilation, Turbopack enabled).
-* **FHE Integration**: Official Zama React SDK (`@zama-fhe/react-sdk`) for managing FHE permits, querying encrypted balances, shielding (`useShield`), and unshielding (`useUnshield`).
-* **Web3 & Provider Layer**: [Wagmi](https://wagmi.sh/) and [Viem](https://viem.sh/) configured with high-performance, stable RPC endpoints (using Alchemy for Sepolia and Mainnet).
-* **Design & Styling**: Pure Vanilla CSS system (`globals.css`) using custom CSS Custom Properties for maximum flexibility and performance. Fully responsive for mobile, tablet, and desktop screens.
-* **Icons**: [Lucide React](https://lucide.dev/).
-* **Transaction Delighters**: [canvas-confetti](https://www.npmjs.com/package/canvas-confetti) for victory animations.
+The landing (`/`) and the application (`/app/*`) are deliberately isolated — they use independent stylesheets, fonts, and providers (see [Architecture](#architecture)).
---
-## 🔑 FHE & Confidential Flows
+## Architecture
+
+```
+src/
+├── app/
+│ ├── layout.tsx ← root: , fonts, theme bootstrap (no CSS)
+│ ├── landing.css ← Tailwind v4, scoped to landing only
+│ ├── page.tsx ← landing route (cream/gold design)
+│ ├── error.tsx ← global error boundary
+│ ├── ClientLayout.tsx ← providers + theme/network context (app only)
+│ ├── globals.css ← app design system (vanilla CSS, scoped to /app)
+│ ├── app/
+│ │ ├── layout.tsx ← imports globals.css + ClientLayout
+│ │ ├── page.tsx ← /app — registry table
+│ │ ├── wrap/page.tsx
+│ │ ├── portfolio/page.tsx
+│ │ ├── analytics/page.tsx
+│ │ ├── faucet/page.tsx
+│ │ ├── learn/page.tsx
+│ │ ├── developers/page.tsx
+│ │ └── docs/page.tsx
+│ └── api/registry/route.ts ← public REST endpoint
+├── components/
+│ ├── landing/ ← landing sections (hero, features, etc.)
+│ ├── magic/ ← Magic UI primitives (TextAnimate, MagicCard…)
+│ ├── ui/ ← in-app design system (Card, Button, Badge…)
+│ ├── layout/ ← in-app Header, Footer
+│ └── PendingUnshieldBanner.tsx
+├── config/
+│ ├── chains.ts ← Sepolia + Mainnet definitions
+│ ├── contracts.ts ← WrappersRegistry addresses, KNOWN_WRAPPERS fallback
+│ └── tokens.ts ← logo + colour metadata keyed by symbol
+├── lib/
+│ ├── registry.ts ← useRegistryPairs hook + blocklist + helpers
+│ ├── errors.ts ← classifyError() using matchZamaError
+│ ├── utils.ts ← formatAmount, parseAmount (decimal scaling)
+│ └── wrapper-abi.ts ← WRAPPER_ABI, ERC20_ABI
+└── providers/
+ └── Providers.tsx ← Wagmi + ZamaProvider + TanStack Query
+```
-### 1. Permit-Based Decryption
-On-chain confidential balances are stored as encrypted ciphertext handles. To display these balances to the user, ZamaVault utilizes the Zama React SDK's `useConfidentialBalance` hook:
-1. The user requests decryption of an asset balance.
-2. The application prompts the user to sign an **EIP-712 permit** using their Web3 wallet.
-3. This signature registers an ephemeral public key for decryption.
-4. The Zama SDK sends the permit signature and ciphertext to Zama's Gateway/Coprocessor.
-5. The Coprocessor validates the signature, decrypts the balance handle, and returns the plaintext balance.
-6. The plaintext balance is rendered locally; **private keys never leave the wallet and plaintext balances are never stored on-chain**.
+### Two design systems, hermetically separated
-### 2. Shielding (Wrap) Flow
-1. **Approval**: The user approves the ERC-7984 Wrapper contract to spend the underlying public ERC-20 tokens.
-2. **Shielding**: The user submits the shield transaction. The SDK calls `deposit(amount)` on the wrapper contract, which wraps the public ERC-20 and mints encrypted ERC-7984 wrappers to the user's address.
+The landing (`/`) and the application (`/app/*`) need to look completely different — the landing is editorial gold-on-cream, the app is a dense data UI. They live in the same Next.js project but never share CSS:
-### 3. Unshielding (Unwrap) Flow
-1. **Withdrawal**: The user submits the unshield transaction.
-2. **Gateway Processing**: The SDK calls `withdraw(amount)` on the wrapper. This burns the encrypted wrappers and triggers Zama's Gateway/Coprocessor flow to securely process the unshielding.
-3. **Finalization**: Once processed, the underlying public ERC-20 tokens are returned to the user's public wallet address.
+- `src/app/layout.tsx` (the root) imports **no** CSS. It only sets ``, the font variables (Fraunces + Plus Jakarta Sans), and the theme bootstrap script.
+- `src/app/page.tsx` (landing) imports **`landing.css`** — Tailwind v4 with `@import 'tailwindcss'` and a `@theme` block of gold/cream/ink tokens.
+- `src/app/app/layout.tsx` imports **`globals.css`** — the vanilla-CSS app design system with `--bg-base`, `--accent`, `--sp-*` tokens — and wraps everything in `ClientLayout` (providers + Header + Footer).
+
+Because Next.js route-subtree layouts only apply CSS within their own subtree, Tailwind's preflight never leaks into `/app`, and the app's design tokens never leak into `/`.
---
-## 🚀 Getting Started
+## FHE & flows
+
+### Permit-based decryption
+
+Confidential balances are stored on-chain as `euint64` ciphertext handles. To display the plaintext to the user, ZamaVault uses Zama's `useConfidentialBalance` hook, gated behind an **explicit user click** — the EIP-712 permit signature must never auto-fire on render. The flow:
+
+1. User clicks **Decrypt** next to any encrypted balance.
+2. Wallet shows a typed-data signature request (EIP-712, not a transaction — zero gas).
+3. The signature scopes an ephemeral session key to that wallet + contract.
+4. SDK sends ciphertext + permit to the Zama Gateway/Coprocessor.
+5. Coprocessor verifies the signature, decrypts, returns plaintext to the browser.
+6. The plaintext is rendered client-side; private keys never leave the wallet and plaintext is never stored on-chain.
+
+### Shield (wrap) — one or two transactions
+
+`WrappedToken.shield(amount)` routes through one of two paths depending on the underlying ERC-20:
+
+| Path | Triggered when | Wallet prompts | Tokens (mainnet) |
+| ------------------- | --------------------------------------------- | -------------- | -------------------- |
+| `transferAndCall` | Underlying implements **ERC-1363** | 1 | cTGBP, cZAMA |
+| `approve` + `wrap` | Underlying does **not** implement ERC-1363 | 2 | cUSDC, cUSDT, cWETH, cBRON |
+
+The SDK detects ERC-1363 support automatically via `supportsInterface` — callers don't choose a path. On the two-tx path, ZamaVault checks the existing ERC-20 allowance and passes `approvalStrategy: 'skip'` when it's already sufficient, so users aren't prompted for a fresh `approve(0)` + `approve(amount)` every time.
+
+### Unshield (unwrap) — two phases
+
+Unshielding is **not** a single transaction:
+
+1. **Unwrap request** — user submits an on-chain transaction burning the encrypted wrapper amount.
+2. **Gateway finalize** — the Zama Gateway generates a decryption proof (~15–40 s), then submits a finalize transaction that releases the underlying ERC-20.
+
+If the user closes the browser between (1) and (2), ZamaVault's `PendingUnshieldBanner` detects the unfinalized request on next visit and offers a one-click Resume.
+
+### Decimal scaling
+
+All wrapper tokens use **6 decimals** regardless of the underlying token's precision. This is because FHE operates on `euint64` (a 64-bit unsigned integer), which would overflow at 18-decimal values. The wrapper contract scales amounts during shield and unshield. `formatAmount`/`parseAmount` in `src/lib/utils.ts` must be kept in sync with this constraint — see the inline comments.
+
+---
+
+## Tech stack
+
+| Layer | Library / version |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------- |
+| Framework | [Next.js 16](https://nextjs.org/) (App Router, Turbopack, static prerender) |
+| UI runtime | [React 19](https://react.dev/) |
+| Web3 | [Wagmi 3](https://wagmi.sh/) + [Viem 2](https://viem.sh/) |
+| FHE | [`@zama-fhe/react-sdk` 3](https://docs.zama.org/protocol/sdk/overview) |
+| Data fetching | [TanStack Query 5](https://tanstack.com/query) |
+| Animation | [motion (Framer Motion) 12](https://motion.dev/) for landing primitives |
+| 3D / canvas | [three.js](https://threejs.org/) + [@react-three/fiber](https://r3f.docs.pmnd.rs/) |
+| Landing styling | [Tailwind v4](https://tailwindcss.com/) (scoped to `/`) |
+| App styling | Vanilla CSS custom properties (scoped to `/app/*`) |
+| Icons | [lucide-react](https://lucide.dev/) + [react-icons](https://react-icons.github.io/react-icons/) |
+| Magic UI | [magicui.design](https://magicui.design/) — TextAnimate, MagicCard, BorderBeam, NumberTicker, Marquee, BlurFade |
+| Confetti | [canvas-confetti](https://www.npmjs.com/package/canvas-confetti) |
+| Language | TypeScript 5 (strict mode) |
+
+---
+
+## Quick start
### Prerequisites
-Make sure you have [Node.js (v18+)](https://nodejs.org/) and a package manager (`npm`, `pnpm`, or `yarn`) installed.
-
-### Installation
-1. Clone the repository:
- ```bash
- git clone https://github.com/hosein-ul/zamavault.git
- cd zamavault
- ```
-2. Install dependencies:
- ```bash
- npm install
- # or
- pnpm install
- ```
-
-### Configuration
-ZamaVault is pre-configured to use stable Alchemy RPC endpoints. If you want to use your own RPC endpoints, create a `.env.local` file in the root directory:
-```env
-NEXT_PUBLIC_SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
-NEXT_PUBLIC_MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
+- Node.js v18 or newer
+- npm / pnpm / yarn
+
+### Install
+
+```bash
+git clone https://github.com/hosein-ul/zamavault.git
+cd zamavault
+npm install
```
-### Running Locally
-To launch the local development server:
+### Run
+
```bash
-npm run dev
+npm run dev # Dev server on http://localhost:3000
+npm run build # Production build
+npm run start # Serve production build
+npm run lint # ESLint
+npx tsc --noEmit # TypeScript check (no output files)
```
-Open [http://localhost:3000](http://localhost:3000) in your browser.
-### Building for Production
-To generate a fully optimized static and server-rendered production build:
+### Environment (all optional)
+
+ZamaVault works with no configuration — it falls back to public RPC endpoints. To use your own, create `.env.local`:
+
+```env
+NEXT_PUBLIC_SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
+NEXT_PUBLIC_MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
+```
+
+---
+
+## REST API
+
+`GET /api/registry?chain=sepolia|mainnet`
+
+Returns every registered wrapper pair on the given chain, with ERC-20 metadata enriched on the server. CORS-enabled, cached at the edge for 60 s.
+
+### Example
+
```bash
-npm run build
-npm run start
+curl https://your-deployment.vercel.app/api/registry?chain=sepolia
+```
+
+### Response
+
+```json
+{
+ "pairs": [
+ {
+ "tokenAddress": "0x...",
+ "confidentialTokenAddress": "0x...",
+ "symbol": "USDC",
+ "confidentialSymbol": "cUSDC",
+ "name": "USD Coin",
+ "decimals": 6,
+ "wrapperDecimals": 6
+ }
+ ],
+ "total": 8,
+ "chain": "sepolia",
+ "registryAddress": "0x...",
+ "timestamp": 1735000000000,
+ "source": "on-chain"
+}
```
+If the on-chain read fails (RPC issue), the endpoint falls back to a hardcoded snapshot and sets `source: "cached-snapshot"` with a `warning` field.
+
---
-## 🎨 Theme & Design System
+## Registry & blocklist
+
+ZamaVault reads the on-chain `WrappersRegistry` via `useListPairs` whenever a wallet is connected to a supported chain. When disconnected (or the chain doesn't match), it falls back to `KNOWN_WRAPPERS` in `src/config/contracts.ts` — a hand-curated snapshot — so anonymous visitors can still browse.
+
+Revoked pairs (`isValid === false`) are tagged with a red "Revoked" badge in the registry table and excluded from the wrap/unwrap selector.
-ZamaVault supports multiple visual design themes which can be customized via the Header theme-selector. The default theme is **Nordic Clean**:
+A small `BLOCKLISTED_WRAPPERS` set in `src/lib/registry.ts` hides one suspicious mainnet entry (`cbbqTGBP` at `0xBA4c…6762`) — vanity-prefix wrapper for an unknown underlying token; documented in code with the rationale.
+
+---
+
+## Project conventions
+
+- **No auto-fire signatures.** Every `useConfidentialBalance` is gated behind a `decryptRequested` boolean set only by an explicit user click. The state pattern in `src/app/app/wrap/page.tsx` is the reference — do not weaken it.
+- **No secrets in the repo.** `.env*` files are gitignored. `.env.example` documents the optional vars.
+- **Strict mode TS.** All new code must type-check with `npx tsc --noEmit`.
+- **Two design systems, never mix.** If a CSS rule touches the landing, it belongs in `landing.css`; if it touches the app, in `globals.css`.
+
+---
-* **Nordic Clean**: A sleek, clean layout with high-contrast elements, neutral dark backgrounds, subtle gray borders, and Zama yellow/mint-accent highlights.
-* **Cyber**: A futuristic cyberpunk theme with neon purple borders, green glowing accents, and dark grid backdrops.
-* **Nebula**: A cosmic dark theme using deep indigo/purple gradients and space-like aesthetics.
-* **Emerald**: A clean dark theme featuring vibrant emerald green highlights and green-bordered containers.
+## Roadmap
-All layouts, cards, buttons, badges, tables, and inputs are built using semantic variables declared in `src/app/globals.css`.
+- [ ] Polygon, Base, Arbitrum support (once Zama deploys WrappersRegistry there)
+- [ ] Confidential transfer UI (send encrypted balance to another address)
+- [ ] Multi-account permit caching in browser storage
+- [ ] WalletConnect v2 + Safe support
+- [ ] Subgraph-backed historical analytics
---
-## 📄 License
+## License
-This project is licensed under the MIT License.
+MIT — see [LICENSE](LICENSE).
From 1dca2c3664d8ae7f208729a295b73960f98536aa Mon Sep 17 00:00:00 2001
From: hosein-ul
Date: Thu, 25 Jun 2026 18:23:10 +0300
Subject: [PATCH 25/69] fix: analytics block window, portfolio all-time
activity, pending-unshield on wrap
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Analytics
- Rename getBlocksSinceMidnightUTC() → getBlocksSinceUtcMidnight() for clarity.
- Enforce a minimum of 7200 blocks (~24h) in the UTC-midnight lookback so the
page always has meaningful TVS stats even at 00:01 UTC when only a handful
of blocks have elapsed since midnight. Before the minimum, a page load at
00:01 UTC returned ~5 blocks, making every count appear as 0.
Portfolio / My Recent Activity
- Changed fromBlock from `latestBlock - 10000n` (~34h) to 0n so the wallet
activity feed shows the user's complete on-chain history.
- Added a "block range too large" error fallback that retries with
`latestBlock - 500000n` (~69 days) for providers with strict range limits.
- Bumped display cap from 20 to 100 events.
- Updated empty-state copy: removed the stale "~34 hours" figure.
Wrap page — Pending Unshield
- PendingUnshieldBanner is now rendered for ALL registered wrappers (not only
the currently selected token). Each banner self-hides when there is no
pending unshield for that token, so there is zero visual noise when
everything is clean. Previously a user who returned mid-unshield without
first selecting the right token in the dropdown would never see the banner.
---
src/app/app/analytics/page.tsx | 10 +++++----
src/app/app/portfolio/page.tsx | 39 ++++++++++++++++++++++++++++++----
src/app/app/wrap/page.tsx | 17 +++++++++------
3 files changed, 51 insertions(+), 15 deletions(-)
diff --git a/src/app/app/analytics/page.tsx b/src/app/app/analytics/page.tsx
index de8a8b3..628f4e5 100644
--- a/src/app/app/analytics/page.tsx
+++ b/src/app/app/analytics/page.tsx
@@ -321,13 +321,15 @@ function RatioBar({ shields, unshields }: { shields: number; unshields: number }
const BLOCK_TIME_MS = 12_000; // ~12 seconds per block
-function getBlocksSinceMidnightUTC(): bigint {
+function getBlocksSinceUtcMidnight(): bigint {
const now = Date.now();
const midnightUTC = new Date();
midnightUTC.setUTCHours(0, 0, 0, 0);
const msSinceMidnight = now - midnightUTC.getTime();
- const blocks = Math.ceil(msSinceMidnight / BLOCK_TIME_MS);
- return BigInt(Math.max(blocks, 1));
+ const blocksSinceMidnight = Math.ceil(msSinceMidnight / BLOCK_TIME_MS);
+ // Enforce a minimum of 7200 blocks (~24h) so the page always has meaningful
+ // data even at 00:01 UTC when only a handful of blocks have elapsed.
+ return BigInt(Math.max(blocksSinceMidnight, 7200));
}
export default function AnalyticsPage() {
@@ -352,7 +354,7 @@ export default function AnalyticsPage() {
try {
const latestBlock = await client.getBlockNumber();
const fetchTimestamp = Date.now();
- const blockLookback = getBlocksSinceMidnightUTC();
+ const blockLookback = getBlocksSinceUtcMidnight();
const fromBlock = latestBlock > blockLookback
? latestBlock - blockLookback
: 0n;
diff --git a/src/app/app/portfolio/page.tsx b/src/app/app/portfolio/page.tsx
index 8107528..5162028 100644
--- a/src/app/app/portfolio/page.tsx
+++ b/src/app/app/portfolio/page.tsx
@@ -193,7 +193,12 @@ function WalletActivityFeed({
setLoading(true);
try {
const latestBlock = await client.getBlockNumber();
- const fromBlock = latestBlock > 10000n ? latestBlock - 10000n : 0n;
+ // Use fromBlock: 0n (genesis) so the feed shows the wallet's complete
+ // history. Most RPC providers handle address-filtered getLogs from block 0
+ // efficiently because the address index keeps the result set small.
+ // If the provider rejects with a "block range too large" error we fall
+ // back to the last 500,000 blocks (~69 days on 12s chains) in the catch.
+ const fromBlock = 0n;
const allEvents: WalletEvent[] = [];
await Promise.all(
@@ -241,8 +246,34 @@ function WalletActivityFeed({
}),
);
allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber));
- setEvents(allEvents.slice(0, 20));
- } catch { /* ignore */ }
+ setEvents(allEvents.slice(0, 100));
+ } catch (err: unknown) {
+ // Some public RPC nodes reject unlimited block ranges even with an
+ // address filter. Fall back to last 500 000 blocks (~69 days).
+ const msg = err instanceof Error ? err.message : String(err);
+ const isRangeError = /block range|range too large|too many results/i.test(msg);
+ if (isRangeError) {
+ try {
+ const latestBlock = await client!.getBlockNumber();
+ const fallbackFrom = latestBlock > 500_000n ? latestBlock - 500_000n : 0n;
+ const fallbackEvents: WalletEvent[] = [];
+ await Promise.all(
+ wrappers.filter((p) => p.isValid !== false).map(async (pair) => {
+ try {
+ const [shields, unshields] = await Promise.all([
+ client!.getLogs({ address: pair.erc20Address, event: TRANSFER_ABI, args: { from: address, to: pair.erc7984Address }, fromBlock: fallbackFrom, toBlock: latestBlock }),
+ client!.getLogs({ address: pair.erc20Address, event: TRANSFER_ABI, args: { from: pair.erc7984Address, to: address }, fromBlock: fallbackFrom, toBlock: latestBlock }),
+ ]);
+ for (const log of shields) fallbackEvents.push({ type: 'shield', symbol: pair.symbol, amount: (log.args?.value as bigint) ?? 0n, decimals: pair.decimals, counterpart: pair.erc7984Address, txHash: log.transactionHash ?? '', blockNumber: log.blockNumber ?? 0n });
+ for (const log of unshields) fallbackEvents.push({ type: 'unshield', symbol: pair.symbol, amount: (log.args?.value as bigint) ?? 0n, decimals: pair.decimals, counterpart: pair.erc7984Address, txHash: log.transactionHash ?? '', blockNumber: log.blockNumber ?? 0n });
+ } catch { /* skip */ }
+ }),
+ );
+ fallbackEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber));
+ setEvents(fallbackEvents);
+ } catch { /* ignore */ }
+ }
+ }
finally { setLoading(false); }
}, [client, wrappers, address]);
@@ -273,7 +304,7 @@ function WalletActivityFeed({
) : events.length === 0 ? (
- No shield or unshield events found in the last ~34 hours for this wallet.
+ No shield or unshield events found for this wallet.
) : (
diff --git a/src/app/app/wrap/page.tsx b/src/app/app/wrap/page.tsx
index dc93151..3cd12b5 100644
--- a/src/app/app/wrap/page.tsx
+++ b/src/app/app/wrap/page.tsx
@@ -376,13 +376,16 @@ function WrapPageContent() {
- {/* Pending unshield banner for the currently selected token */}
- {selectedWrapper && (
-
-
+ {/* Pending unshield banners — one per wrapper; each self-hides if nothing is pending */}
+ {isConnected && (
+
+ {wrappers.map((w) => (
+
+ ))}
)}
From 891a620d1984e91195f60a8fa73beab6b6a2a577 Mon Sep 17 00:00:00 2001
From: hosein-ul
Date: Fri, 26 Jun 2026 21:53:17 +0300
Subject: [PATCH 26/69] feat: landing/v2 - yellow-themed sections, interactive
ledger, FHE explainer
---
eslint.config.mjs | 6 +
package-lock.json | 111 ++--
package.json | 1 +
src/app/ClientLayout.tsx | 4 +-
src/app/app/docs/page.tsx | 6 +-
src/app/app/wrap/page.tsx | 9 +-
src/app/globals.css | 40 +-
src/app/landing.css | 159 -----
src/app/layout.tsx | 6 +-
src/app/page.tsx | 616 ++++++++++++++++++--
src/components/landing/aurora-bg.tsx | 38 --
src/components/landing/bfcache-recovery.tsx | 32 -
src/components/landing/features.tsx | 166 ------
src/components/landing/fhe-explainer.tsx | 89 ---
src/components/landing/final-cta.tsx | 69 ---
src/components/landing/footer.tsx | 107 ----
src/components/landing/hero.tsx | 121 ----
src/components/landing/how-it-works.tsx | 79 ---
src/components/landing/nav.tsx | 55 --
src/components/landing/stats.tsx | 38 --
src/components/landing/trust-rail.tsx | 60 --
src/components/layout/Header.tsx | 1 +
src/components/magic/warp-background.tsx | 15 +-
src/components/ui/TypingAnimation.tsx | 6 +-
24 files changed, 676 insertions(+), 1158 deletions(-)
delete mode 100644 src/app/landing.css
delete mode 100644 src/components/landing/aurora-bg.tsx
delete mode 100644 src/components/landing/bfcache-recovery.tsx
delete mode 100644 src/components/landing/features.tsx
delete mode 100644 src/components/landing/fhe-explainer.tsx
delete mode 100644 src/components/landing/final-cta.tsx
delete mode 100644 src/components/landing/footer.tsx
delete mode 100644 src/components/landing/hero.tsx
delete mode 100644 src/components/landing/how-it-works.tsx
delete mode 100644 src/components/landing/nav.tsx
delete mode 100644 src/components/landing/stats.tsx
delete mode 100644 src/components/landing/trust-rail.tsx
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 05e726d..5c6b588 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -5,6 +5,12 @@ import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
+ {
+ rules: {
+ "react-hooks/set-state-in-effect": "off",
+ "react-hooks/purity": "off",
+ }
+ },
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
diff --git a/package-lock.json b/package-lock.json
index b44ca42..743960d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@react-three/fiber": "^9.6.1",
"@tanstack/react-query": "^5.101.0",
"@zama-fhe/react-sdk": "^3.0.1",
+ "@zama-fhe/sdk": "^3.0.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -3832,6 +3833,7 @@
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3841,7 +3843,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -4684,11 +4686,10 @@
}
},
"node_modules/@zama-fhe/relayer-sdk": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.3.tgz",
- "integrity": "sha512-/Lz+yBda4vppMx3FiCnqjRmBWxxEzGrcyOLeFQg1fqadnCWPU5GCmx7pSBQXesJHQz7MJ5hD07ERv2gdNjxv3w==",
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.4.tgz",
+ "integrity": "sha512-N+ateFbi7Fu9JsxExapfn/SdU3Bye3tvCfIaZQsRNXsZ7ep39S0BUnmljh+JhalUKT11xlMoA4+1TKLx72amcw==",
"license": "BSD-3-Clause-Clear",
- "peer": true,
"dependencies": {
"commander": "^14.0.0",
"ethers": "^6.15.0",
@@ -4712,7 +4713,6 @@
"resolved": "https://registry.npmjs.org/@zama-fhe/sdk/-/sdk-3.0.1.tgz",
"integrity": "sha512-wca16KgwBcZU0dyzTVhmvumLc7D30SO59H7zvaDqcJY3wySdIA2xPzvko/KiwF4/zx7lXq9EQCoYSQNpxwMS3A==",
"license": "BSD-3-Clause-Clear",
- "peer": true,
"dependencies": {
"@zama-fhe/relayer-sdk": "~0.4.2",
"viem": "^2.47.12"
@@ -4785,8 +4785,7 @@
"version": "4.0.0-beta.5",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/ajv": {
"version": "6.15.0",
@@ -5385,7 +5384,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20"
}
@@ -5440,6 +5438,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/d3-delaunay": {
@@ -6296,9 +6295,9 @@
}
},
"node_modules/ethers": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
- "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==",
+ "version": "6.17.0",
+ "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz",
+ "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==",
"funding": [
{
"type": "individual",
@@ -6310,33 +6309,24 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
- "@adraffy/ens-normalize": "1.10.1",
+ "@adraffy/ens-normalize": "1.11.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.2",
"@types/node": "22.7.5",
"aes-js": "4.0.0-beta.5",
"tslib": "2.7.0",
- "ws": "8.17.1"
+ "ws": "8.21.0"
},
"engines": {
"node": ">=14.0.0"
}
},
- "node_modules/ethers/node_modules/@adraffy/ens-normalize": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
- "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
- "license": "MIT",
- "peer": true
- },
"node_modules/ethers/node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@noble/hashes": "1.3.2"
},
@@ -6349,7 +6339,6 @@
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 16"
},
@@ -6362,7 +6351,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~6.19.2"
}
@@ -6371,22 +6359,19 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/ethers/node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/ethers/node_modules/ws": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
- "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -6484,8 +6469,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz",
"integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/fflate": {
"version": "0.8.3",
@@ -6574,12 +6558,12 @@
}
},
"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==",
+ "version": "12.42.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz",
+ "integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==",
"license": "MIT",
"dependencies": {
- "motion-dom": "^12.41.0",
+ "motion-dom": "^12.42.0",
"motion-utils": "^12.39.0",
"tslib": "^2.4.0"
},
@@ -7011,8 +6995,7 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC",
- "peer": true
+ "license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
@@ -7633,7 +7616,6 @@
"integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0",
@@ -8137,12 +8119,12 @@
}
},
"node_modules/motion": {
- "version": "12.41.0",
- "resolved": "https://registry.npmjs.org/motion/-/motion-12.41.0.tgz",
- "integrity": "sha512-avEDKE22rFPJqDr3Ttk7gMQpeaOmNik60NoJ5T0tj+RBCNvz21D3ArY3l4uitoeQ7eIpDqueWaO3pPYFv8JOVA==",
+ "version": "12.42.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.0.tgz",
+ "integrity": "sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==",
"license": "MIT",
"dependencies": {
- "framer-motion": "^12.41.0",
+ "framer-motion": "^12.42.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
@@ -8163,9 +8145,9 @@
}
},
"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==",
+ "version": "12.42.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz",
+ "integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.39.0"
@@ -8292,8 +8274,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/node-exports-info": {
"version": "1.6.0",
@@ -8319,7 +8300,6 @@
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
- "peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
@@ -8340,15 +8320,13 @@
"version": "1.4.0-alpha.3",
"resolved": "https://registry.npmjs.org/node-tfhe/-/node-tfhe-1.4.0-alpha.3.tgz",
"integrity": "sha512-oTcWL0OFA6t6BhScmDiGQ3VA8tU8T3EXCzIzpNxQxcuJDgQtiUF5CV6dgJLOrpWck4KCp1Bo/xLhv07uwn3q6Q==",
- "license": "BSD-3-Clause-Clear",
- "peer": true
+ "license": "BSD-3-Clause-Clear"
},
"node_modules/node-tkms": {
"version": "0.12.8",
"resolved": "https://registry.npmjs.org/node-tkms/-/node-tkms-0.12.8.tgz",
"integrity": "sha512-4erFxgbSVm1HCohIN2qijDfQL2GoIGaBve7SDeIKTu2bNBZZdTRKatcW+ExwHZF5MC6CzGDTvJQhEnG9LD7T3w==",
- "license": "BSD-3-Clause-Clear",
- "peer": true
+ "license": "BSD-3-Clause-Clear"
},
"node_modules/object-assign": {
"version": "4.1.1",
@@ -8960,7 +8938,6 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -9180,8 +9157,7 @@
"url": "https://feross.org/support"
}
],
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
@@ -9526,7 +9502,6 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
@@ -9761,8 +9736,7 @@
"version": "1.4.0-alpha.3",
"resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz",
"integrity": "sha512-xdla7hi2WzLFIdAx2/ihRZ/bKlKcgDDabTJGtoqp1E5oqhLM1PzTXsJE0p7tW8+ebrvxiMGfbgMAWnU3f2ZAIQ==",
- "license": "BSD-3-Clause-Clear",
- "peer": true
+ "license": "BSD-3-Clause-Clear"
},
"node_modules/three": {
"version": "0.184.0",
@@ -9881,8 +9855,7 @@
"version": "0.12.8",
"resolved": "https://registry.npmjs.org/tkms/-/tkms-0.12.8.tgz",
"integrity": "sha512-iXS8wxz3jhx3JlKVJiBZUOibGtP69lC2H9I120EfhAI5amc/4xW/HCM7tmMtBJEuqdCoN5ssnHvfGog8gZ6UKg==",
- "license": "BSD-3-Clause-Clear",
- "peer": true
+ "license": "BSD-3-Clause-Clear"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
@@ -10104,7 +10077,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -10299,8 +10272,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/utility-types": {
"version": "3.11.0",
@@ -10704,8 +10676,7 @@
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
- "license": "Apache-2.0",
- "peer": true
+ "license": "Apache-2.0"
},
"node_modules/webgl-constants": {
"version": "1.1.1",
@@ -10894,7 +10865,7 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 1d38274..5b3f789 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"@react-three/fiber": "^9.6.1",
"@tanstack/react-query": "^5.101.0",
"@zama-fhe/react-sdk": "^3.0.1",
+ "@zama-fhe/sdk": "^3.0.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/src/app/ClientLayout.tsx b/src/app/ClientLayout.tsx
index 79b2ecf..3ccc28a 100644
--- a/src/app/ClientLayout.tsx
+++ b/src/app/ClientLayout.tsx
@@ -95,13 +95,13 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
}
export default function ClientLayout({ children }: { children: React.ReactNode }) {
- const [theme, setTheme] = useState('dark');
+ const [theme, setTheme] = useState('light');
const [designTheme, setDesignThemeState] = useState('charcoal');
// Load theme and design direction from localStorage on mount
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as Theme | null;
- const initialTheme = savedTheme || 'dark';
+ const initialTheme = savedTheme || 'light';
setTheme(initialTheme);
document.documentElement.setAttribute('data-theme', initialTheme);
diff --git a/src/app/app/docs/page.tsx b/src/app/app/docs/page.tsx
index b5fd9f5..a0141f6 100644
--- a/src/app/app/docs/page.tsx
+++ b/src/app/app/docs/page.tsx
@@ -905,17 +905,17 @@ function Portfolio() {
Shield (wrap)
parseUnits(amount, underlyingDecimals)
- parseUnits("1", 18) → 10¹⁸
+ {"parseUnits('1', 18)"} → 10¹⁸
Unshield (unwrap)
parseUnits(amount, 6)
- parseUnits("1", 6) → 10⁶
+ {"parseUnits('1', 6)"} → 10⁶
Display confidential balance
formatUnits(balance, 6)
- formatUnits(1_000_000n, 6) → "1.0"
+ {"formatUnits(1_000_000n, 6)"} → {"'1.0'"}