diff --git a/index.html b/index.html index abe16b3..75a44aa 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + diff --git a/package.json b/package.json index a9a1a5a..14614e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.44.0-roster", + "version": "0.45.0-motion", "private": true, "type": "module", "scripts": { diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index f3e6305..a0d2089 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Menu, X } from 'lucide-react'; import { useAuth, AuthPanel, AdminConsole } from './Auth.jsx'; import { AgentBrowse, BoothDashboard } from './AgentPlatform.jsx'; @@ -7,7 +7,7 @@ import Account from './Account.jsx'; import TavernGame from './TavernGame.jsx'; import { api } from './api.js'; -const RELEASE = '0.44.0-roster'; +const RELEASE = '0.45.0-motion'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -70,6 +70,33 @@ function PixelTopbar({ route, setRoute, auth }) { ); } +const prefersReducedMotion = () => typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +// Count up from 0 to n with an ease-out curve (delightful stat reveals). +function Count({ n }) { + const [v, setV] = useState(prefersReducedMotion() ? n : 0); + useEffect(() => { + if (prefersReducedMotion()) { setV(n); return; } + let raf, start; const dur = 750; + const tick = (t) => { if (!start) start = t; const p = Math.min(1, (t - start) / dur); setV(Math.round(n * (1 - Math.pow(1 - p, 3)))); if (p < 1) raf = requestAnimationFrame(tick); }; + raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); + }, [n]); + return <>{v}>; +} + +// Fade-up as the element scrolls into view; always-visible fallback if no IO / reduced motion. +function Reveal({ children, className = '', delay = 0 }) { + const ref = useRef(null); + const [armed] = useState(() => typeof window !== 'undefined' && 'IntersectionObserver' in window && !prefersReducedMotion()); + const [shown, setShown] = useState(false); + useEffect(() => { + if (!armed || !ref.current) return; + const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }); }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); + io.observe(ref.current); return () => io.disconnect(); + }, [armed]); + return
Walk into the tavern — talk to the screen to browse staff, or step to the floor to open your booth.
{stats && (An agent mercenary layer for embedding, bringing, and hosting AI agents across vertical software boundaries.
{s.body}
-{u.body}
{u.body}