home: back-to-top button (0.66.0)

- the home page is long now (hero -> flow -> features -> session preview ->
  activity -> footer); add a floating back-to-top button that fades in once
  you scroll past the hero and smooth-scrolls the home container to top
- listens on the gm-home scroll container (not window); hidden by the
  persistent-home display:none when off-route; respects reduced-motion
- verified live: hidden at top, shows after scroll, returns to top on click
This commit is contained in:
artheru 2026-06-10 09:04:48 +08:00
parent e73dbbe186
commit 26030b0a13
4 changed files with 21 additions and 4 deletions

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" />
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.65.0-instanthome" />
<meta name="version" content="0.66.0-backtotop" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

View File

@ -1,6 +1,6 @@
{
"name": "amerc-site",
"version": "0.65.0-instanthome",
"version": "0.66.0-backtotop",
"private": true,
"type": "module",
"scripts": {

View File

@ -7,7 +7,7 @@ import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx';
import { api } from './api.js';
const RELEASE = '0.65.0-instanthome';
const RELEASE = '0.66.0-backtotop';
const NAV = [
{ id: 'home', label: 'Tavern' },
@ -100,9 +100,17 @@ function Reveal({ children, className = '', delay = 0, tag: Tag = 'div' }) {
function Home({ setRoute }) {
const [stats, setStats] = useState(null);
const [scrolled, setScrolled] = useState(false);
const homeRef = useRef(null);
useEffect(() => { api('/agents/stats').then(setStats).catch(() => {}); }, []);
useEffect(() => {
const el = homeRef.current; if (!el) return;
const onScroll = () => setScrolled(el.scrollTop > 500);
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
}, []);
return (
<div className="gm-home">
<div className="gm-home" ref={homeRef}>
<div className="gm-hero">
<div className="gm-stage"><TavernGame onHotspot={setRoute} stats={stats} /></div>
<div className="gm-caption">
@ -124,6 +132,7 @@ function Home({ setRoute }) {
</div>
<HomeFeatures setRoute={setRoute} />
<Footer setRoute={setRoute} />
<button className={`gm-totop${scrolled ? ' show' : ''}`} onClick={() => homeRef.current?.scrollTo({ top: 0, behavior: 'smooth' })} aria-label="Back to top" type="button"></button>
</div>
);
}

View File

@ -1449,3 +1449,11 @@ button {
/* persistent home host (0.65) — keeps the tavern mounted, hidden off-route */
.route-host { flex: 1; min-height: 0; flex-direction: column; }
/* back-to-top button (0.66) */
.gm-totop { position: fixed; right: 22px; bottom: 22px; width: 42px; height: 42px; border-radius: 50%; border: none; cursor: pointer;
background: linear-gradient(180deg, #2aa7d8, #1f86c4); color: #fff; font-size: 20px; line-height: 1;
box-shadow: 0 6px 18px rgba(0,0,0,0.45); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity .2s, transform .2s, background .15s; z-index: 30; }
.gm-totop.show { opacity: 0.92; transform: none; pointer-events: auto; }
.gm-totop.show:hover { opacity: 1; background: linear-gradient(180deg, #46c8e0, #2aa7d8); }
@media (prefers-reduced-motion: reduce) { .gm-totop { transition: opacity .2s; transform: none; } }