mirror of
https://github.com/zoo-apps/industries.git
synced 2026-10-30 14:57:03 +00:00
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]>
83 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|