Files
industries/components/homepage2/SlideIndicator.tsx
zooqueenandHanzo Dev 19905ecd56 Homepage redesign: investor-narrative slide deck + editorial variant
Adds three home compositions sharing the same investor narrative:

- / (canonical) — restored old composition with Hero + zoo composite,
  Ecosystem, ZooLabsNarrative, ServicesOverview, ResearchHighlights,
  Leadership, TrustedBy, Contact. Adds polished motion (Reveal,
  Parallax wrappers, scroll-linked Hero drift), softer brutalist reset
  (rounded utilities now opt-in), and a Zen5 rainbow-gradient top
  banner.

- /homepage1 — backup of the previous / composition (noindex).

- /homepage2 — 10-slide deck with snap-mandatory CSS scroll snap,
  side-rail slide indicator, alternating brand-color slide washes,
  CSS-3D AI-coin scene behind the Hero (Zen5 family + Cloud / Network
  spokes, perspective + per-coin float keyframes, light blur),
  network-mesh edges, Foundation + Leadership + Traction + Access
  slides, plus the AI-native market-cap composite with NVIDIA as
  public-market comparable.

- /homepage3 — editorial 'Quarterly' variant with serif display,
  masthead, drop cap, ornamental dividers, chapter framing, and a
  colophon footer. Wax-seal red accent on a cream paper surface.

Other changes:
- Navbar: drop rounded floating pill in favour of sharp transparent
  bar with a sharp backdrop-blur strip + border-b-2 black on scroll.
- TopBanner: animated rainbow gradient background (300% width,
  background-position keyframe, prefers-reduced-motion aware).
- Leadership / TeamMemberCard: rounded-full avatars (override the
  brutalist global border-radius reset via data-circle).
- Ecosystem strip: render JPGs at /public/ecosystem/*.jpg with
  emoji fallback only when an image is missing (bear, drought, lake,
  eagle, forest, whale).
- TheStack: each layer card is now a click target to its live
  domain (zenlm.org, zoo.cloud, zoo.network, zoo.fund, zoo.vote).
- public/: new /hero/zooimg.png, /ecosystem/*.jpg, /pe/*.png.
- Reveal / Parallax / ParallaxDivider / ParallaxBackground /
  CoinScene / SlideIndicator / SlideParallax / NetworkAnimation
  primitives under components/.

Co-authored-by: Hanzo Dev <[email protected]>
2026-05-18 22:22:51 -07:00

83 lines
2.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
/**
* Right-rail slide indicator. Watches every ``[data-slide]`` section
* with an IntersectionObserver and highlights whichever is closest to
* centred in the viewport. Clicking a dot scrolls to that slide.
*
* Renders on lg+ only — there's no room for the rail on mobile, and
* the snap behaviour is enough of a hint on small screens. Hidden
* during ``prefers-reduced-motion`` is overkill (no motion happens
* here); we leave it visible.
*/
export function SlideIndicator() {
const [slides, setSlides] = useState<HTMLElement[]>([]);
const [activeIdx, setActiveIdx] = useState(0);
useEffect(() => {
const found = Array.from(
document.querySelectorAll<HTMLElement>("[data-slide]"),
);
setSlides(found);
if (found.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
// Most-visible entry wins. Single-pass; no debounce needed.
let best: { idx: number; ratio: number } | null = null;
entries.forEach((e) => {
const idx = found.indexOf(e.target as HTMLElement);
if (idx === -1) return;
if (!best || e.intersectionRatio > best.ratio) {
best = { idx, ratio: e.intersectionRatio };
}
});
if (best && (best as { idx: number; ratio: number }).ratio > 0.4) {
setActiveIdx((best as { idx: number; ratio: number }).idx);
}
},
{
// Trigger updates roughly every 25% so the dot tracks scroll
// smoothly without firing on every pixel.
threshold: [0.25, 0.5, 0.75, 1.0],
},
);
found.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
if (slides.length === 0) return null;
return (
<nav
aria-label="Slides"
className="hidden lg:flex fixed right-6 top-1/2 -translate-y-1/2 z-50 flex-col gap-3"
>
{slides.map((_, i) => (
<button
key={i}
aria-label={`Go to slide ${i + 1}`}
aria-current={i === activeIdx ? "true" : undefined}
onClick={() =>
slides[i]?.scrollIntoView({ behavior: "smooth", block: "start" })
}
className="group h-3 w-3 grid place-items-center"
>
<span
className={`block transition-all duration-300 ease-out border-2 border-black transform-gpu ${
i === activeIdx
? "w-3 h-3 bg-foreground"
: "w-2 h-2 bg-transparent group-hover:bg-foreground/40"
}`}
style={{ borderRadius: "9999px" }}
/>
</button>
))}
</nav>
);
}