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
{children}
; +} + function Home({ setRoute }) { const [stats, setStats] = useState(null); useEffect(() => { api('/agents/stats').then(setStats).catch(() => {}); }, []); @@ -82,9 +109,9 @@ function Home({ setRoute }) {

Walk into the tavern — talk to the screen to browse staff, or step to the floor to open your booth.

{stats && (
- {stats.classes} classes - {stats.online} online - {stats.sessions} sessions + classes + online + sessions
)}
document.querySelector('.hf')?.scrollIntoView({ behavior: 'smooth' })}>how it works ↓
@@ -130,18 +157,18 @@ function HomeFeatures({ setRoute }) {

An agent mercenary layer for embedding, bringing, and hosting AI agents across vertical software boundaries.

{HF_STEPS.map((s, i) => ( -
+ {s.icon}

{s.title}

{s.body}

-
+ ))}

Three ways to use an agent

{HF_USES.map((u, i) => ( -
{u.icon}
{u.title}

{u.body}

+ {u.icon}
{u.title}

{u.body}

))}
diff --git a/src/styles.css b/src/styles.css index 1bfcf83..167257c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1267,3 +1267,10 @@ button { .ap-card-add strong { color: #eaf6ff; font-size: 15px; } .ap-card-add p { margin: 0; color: #9d8fc0; font-size: 12.5px; line-height: 1.55; } .ap-add-cta { color: #46c8e0; font-size: 12.5px; font-weight: 600; margin-top: 2px; } + +/* scroll-reveal + count-up motion (0.45) */ +.reveal { transition: opacity .6s cubic-bezier(.2,.7,.2,1), transform .6s cubic-bezier(.2,.7,.2,1); } +.hf-card.reveal { transition: opacity .6s cubic-bezier(.2,.7,.2,1), transform .6s cubic-bezier(.2,.7,.2,1), border-color .14s; } +.reveal:not(.in) { opacity: 0; transform: translateY(18px); } +.reveal.in { opacity: 1; } +@media (prefers-reduced-motion: reduce) { .reveal, .reveal:not(.in) { opacity: 1 !important; transform: none !important; transition: none !important; } }