Skip to content

feat(landing-v2): editorial scroll-narrative landing with GSAP + Lenis#3

Draft
hosein-ul wants to merge 18 commits into
mainfrom
feat/landing-redesign
Draft

feat(landing-v2): editorial scroll-narrative landing with GSAP + Lenis#3
hosein-ul wants to merge 18 commits into
mainfrom
feat/landing-redesign

Conversation

@hosein-ul

@hosein-ul hosein-ul commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

Brand-new landing at / on a dedicated branch. The previous landing design is left fully intact on disk (src/components/landing/, src/app/landing.css) so it's reachable with a single git checkout.

Marked draft — depends on PR #2 (feat/dynamic-registry-finding-1) being merged first.

Identity

  • Cream base (#FAF7F1), gold accent (#FFD208), aubergine (#3B1F2B) for the single dark beat. No all-dark theme.
  • Fraunces (display, serif) + Plus Jakarta Sans (body) — loaded via next/font from the root layout.
  • All tokens in a new src/app/landing-v2.css, isolated from landing.css and globals.css via Next.js route-subtree CSS scoping.

Scroll plumbing

  • Lenis smooth-scroll provider bridges into GSAP ScrollTrigger so pinned timelines stay aligned with the lerped scroll.
  • Scroll-progress rail pinned to the right viewport edge — 2 px gold line scrubbed by document scroll progress + live section label driven by IntersectionObserver.
  • Noise background — Canvas 2D + simplex-noise, fixed behind everything. Hue and dot density shift based on scroll progress (cream → gold-warm → aubergine drift).

Sections (top to bottom)

# Section What it does
1 Aperture Gold dot blooms into halo; two-line serif headline ("Your balance was always public. Now it isn't.")
2 Manifesto Pinned 130 % vh; paragraph word-reveals on scrub; gold underline draws beneath the closing phrase
3 Problem ledger Mock public USDC transfers + what an observer can derive (clusters, totals, behaviour)
4 Solution encrypt Pinned 150 % vh — same ledger SCRAMBLES character-by-character into ciphertext via GSAP onUpdate; gold "euint64 · on-chain" pill and explanation resolve at the end
5 Token constellation Real CMC logos for the 7 confidential pairs
6 Capabilities Editorial 3 × 2 grid — six tools, one consistent design
7 Numbers Four count-up figures with custom easeOutQuart
8 Flow Vertical timeline whose gold rail fills by scroll position
9 For Devs Three syntax-stripped code blocks (React / curl / viem)
10 Final beat Aubergine, big serif "Your tokens. Your privacy. On-chain."
11 Footer Editorial wide-gutter

Dependencies

  • New: gsap@^3.15 (ScrollTrigger is now MIT-licensed), lenis@^1.3
  • Reuses existing: motion (framer-motion), simplex-noise, lucide-react, react-icons, next/font

Accessibility

  • Respects prefers-reduced-motion globally — Lenis no-ops, GSAP timelines early-return, noise canvas freezes a single static frame.
  • All interactive elements are <Link> or <a> — keyboard navigable.
  • Headlines stay readable on first paint via motion.div opacity safety fades.

Test plan

  • npx tsc --noEmit — clean
  • npx next build — all 11 routes prerendered cleanly
  • Live preview verified: 10 sections registered (aperture, manifesto, problem, solution, tokens, capabilities, numbers, flow, devs, final)
  • Lenis active on <html> (class observed)
  • Noise canvas fixed-positioned behind content
  • Scroll-progress rail rendered
  • Capabilities grid is exactly 3 × 2 (rows at y=337 and y=594, 3 cards each)
  • Final section background is aubergine rgb(59, 31, 43) — not black
  • Manual: walk through the scroll, watch the Solution scramble effect
  • Manual: test on prefers-reduced-motion: reduce — content should be readable without animations
  • Lighthouse: target Performance ≥ 85, Accessibility ≥ 95

hosein-ul added 18 commits June 23, 2026 12:00
- Replace all Etherscan links with Blockscout (eth-sepolia.blockscout.com /
  eth.blockscout.com) — Blockscout supports Zama FHE protocol decoding
- Replace hardcoded zamavault.xyz in docs/developers/API with
  NEXT_PUBLIC_APP_URL env var (falls back to placeholder until deployed)
- Add NEXT_PUBLIC_APP_URL to .env.example with documentation
- chains.ts: update explorerUrl for both Sepolia and Mainnet
…permit bug

TanStack Query's refetch() bypasses the enabled flag unconditionally. After
a successful shield or unshield, calling refetchWrapperBalance() was silently
triggering useConfidentialBalance even though decryptRequested=false, causing
the EIP-712 permit wallet prompt to auto-fire without user consent (and retry
up to 3x before surfacing the rejection error).

Fix: remove refetchWrapperBalance() from both success paths in handleAction.
The confidential balance is only refreshed when the user explicitly clicks
the "Decrypt" button (which sets decryptRequested=true before calling refetch).
After a successful wrap/unwrap, setDecryptRequested(false) prevents
TanStack Query's refetchOnWindowFocus from auto-firing the EIP-712 permit
(which caused the wallet to prompt up to 3 times for a permit the user
had not explicitly requested). The user must click Decrypt again to see
the refreshed confidential balance after a transaction.
Phase 3.1 — /analytics page:
- TVL per token via balanceOf on underlying ERC-20 held by wrapper
- Shield/Unshield event counts from Transfer logs (last 5000 blocks)
- Recent activity feed sorted by block number
- 4 stat cards: active pairs, shields, unshields, unique shielders
- Refresh button, Blockscout links, loading skeletons

Phase 3.2 — Portfolio activity feed:
- WalletActivityFeed component at bottom of portfolio
- Shows recent shield/unshield events for connected wallet (last 10000 blocks)
- Links to Analytics page

Phase 4.4 — Mobile responsive CSS:
- Header nav hidden on mobile, container padding reduced
- Grid-2 collapses to 1 column at 768px
- Swap panel, modal, typography scale for 360px–480px
- Logo text hidden at 480px to save space
- Network switcher compact on small screens
- formatTVLCompact(): 22976602.1114 → 23.00M (fixes display overflow)
- estimateTimeAgo(): block-based time estimate without extra RPC calls
  (latestBlock - eventBlock) × 12s → '2m ago', '3h ago', etc.
- Activity: show up to 25 events (max 8 per token), add timestamp to each row
- Add amount localeString formatting (commas for thousands)
- New: Shield vs Unshield ratio bar card
- New: Most Active Token card (by tx count)
- New: shieldVolume / unshieldVolume tracking (period volume in TVL bar)
- insights-grid 2-col responsive layout
- Shield: skip Approve step in UI when allowance is already sufficient
  (SDK auto-skips it; now UI matches by starting at step 3)
- Unshield: add onFinalizing callback — shows toast when Zama Gateway
  is generating the decryption proof (15-40s wait phase)
- Unshield: add onFinalizeSubmitted callback — shows finalization tx
- Step indicator: Approve step only shown when needsApproval is true
- Step indicator: Unshield now shows 3 steps (Unwrap → Finalize → Done)
  instead of 2, matching the actual 2-phase protocol flow
Root cause: .badge CSS class had text-transform:uppercase which turned
"cZAMA" into "CZAMA", "cUSDC" into "CUSDC" everywhere badges were used.

- Remove text-transform:uppercase from .badge class
- Token selector in wrap page: shows "cBRON", "cZAMA" etc when in
  unwrap mode (From = Confidential), plain "BRON", "ZAMA" in wrap mode
- Verified all symbol conventions:
  - Public ERC-20: ZAMA, USDC, WETH, BRON, etc.
  - Confidential ERC-7984: cZAMA, cUSDC, cWETH, cBRON, etc.
  - c is always lowercase per Zama convention
analytics:
- Fix TVL sort: was comparing raw bigints (wrong — ZAMA 18-dec raw >>
  USDC 6-dec raw for equal human values). Now sorts by tvlHuman (float
  normalized by decimals). USDC 22.89M now correctly ranks above tGBP 3.86M.
- Fix bar width: same normalization applied to bar percentage calculation.
- Change period: 5000 blocks (~17h) → 7200 blocks (24h exactly at 12s/block).
- Update all "~17h" text to "~24h".

registry page:
- Shorten all tooltip text significantly (1-2 lines max).
- Fix tooltip triggers: Mock badge and Confidential badge now have a
  separate ? icon (Tooltip standalone), not the badge itself as trigger.
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
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
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 <BFCacheRecovery /> 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.
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)
A brand-new landing at / on a dedicated branch. v1 design is left fully
intact on disk (src/components/landing/, src/app/landing.css) so the
previous design is one-branch-switch away.

Identity
- Cream base (#FAF7F1), gold accent (#FFD208), aubergine (#3B1F2B) for
  the single dark beat. Editorial — no all-dark theme.
- Fraunces (display, serif) + Plus Jakarta Sans (body) — already loaded
  via next/font from the root layout.
- All tokens in a new src/app/landing-v2.css, isolated from landing.css
  and globals.css via Next.js route-subtree CSS scoping.

Scroll plumbing
- Lenis smooth-scroll provider bridges into GSAP ScrollTrigger so pinned
  timelines stay aligned with the lerp'd scroll.
- Scroll-progress rail pinned to the right viewport edge: a 2 px gold
  line scrubbed by document scroll progress, plus a label showing the
  current section name (driven by IntersectionObserver).
- Noise background — Canvas 2D + simplex-noise — fixed behind everything,
  hue and dot density shift based on scroll progress through the page
  narrative (cream → gold-warm → aubergine drift).

Sections (top to bottom)
- Aperture: gold dot blooms into halo, two-line serif headline.
- Manifesto: pinned 130 % vh, paragraph word-reveals on scrub, then a
  gold underline draws beneath the closing phrase.
- Problem ledger: mock public transfers + what an observer can derive.
- Solution encrypt: pinned 150 % vh, same ledger SCRAMBLES character-by-
  character into ciphertext via GSAP onUpdate; a gold "euint64 · on-chain"
  pill and explanation fade in at the end.
- Token constellation: real CMC logos for the 7 confidential pairs.
- Capabilities: 3 × 2 editorial grid, six tools, one consistent design.
- Numbers: four count-up figures with custom easeOutQuart.
- Flow: vertical timeline whose gold rail fills by scroll position
  (native IntersectionObserver, no GSAP needed for this one).
- For Devs: three syntax-stripped code blocks (React / curl / viem).
- Final beat: aubergine, big serif "Your tokens. Your privacy. On-chain."
- Footer: editorial wide-gutter, fine type.

Tech
- New deps: gsap ^3.15, lenis ^1.3
- Reuses existing: motion (framer-motion), simplex-noise, lucide-react,
  react-icons, next/font (Fraunces + Plus Jakarta Sans).
- Respects prefers-reduced-motion globally — Lenis no-ops, GSAP timelines
  early-return, noise canvas freezes one static frame.
- BFCacheRecovery reused from v1.
…hield on wrap

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant