liveliness + consistency: activity reveal, gatehouse count-up (0.62.0)

- Reveal now accepts a tag prop so it can wrap semantic elements
- 'Live in the tavern' activity items stagger-fade-up as they scroll into
  view (as <li>, valid list semantics), matching the feature cards
- gatehouse stats now count up from 0, consistent with the home hero
- both respect prefers-reduced-motion
- verified live: 8 items reveal (opacity 1, not stuck), gatehouse Count
  renders, 0 exceptions
This commit is contained in:
artheru 2026-06-10 08:37:52 +08:00
parent 2d00cb4e63
commit e3e5cefd03
3 changed files with 9 additions and 9 deletions

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" /> <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="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.61.0-syntax" /> <meta name="version" content="0.62.0-liveness" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" /> <link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />

View File

@ -1,6 +1,6 @@
{ {
"name": "amerc-site", "name": "amerc-site",
"version": "0.61.0-syntax", "version": "0.62.0-liveness",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -7,7 +7,7 @@ import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx'; import TavernGame from './TavernGame.jsx';
import { api } from './api.js'; import { api } from './api.js';
const RELEASE = '0.61.0-syntax'; const RELEASE = '0.62.0-liveness';
const NAV = [ const NAV = [
{ id: 'home', label: 'Tavern' }, { id: 'home', label: 'Tavern' },
@ -86,7 +86,7 @@ function Count({ n }) {
} }
// Fade-up as the element scrolls into view; always-visible fallback if no IO / reduced motion. // Fade-up as the element scrolls into view; always-visible fallback if no IO / reduced motion.
function Reveal({ children, className = '', delay = 0 }) { function Reveal({ children, className = '', delay = 0, tag: Tag = 'div' }) {
const ref = useRef(null); const ref = useRef(null);
const [armed] = useState(() => typeof window !== 'undefined' && 'IntersectionObserver' in window && !prefersReducedMotion()); const [armed] = useState(() => typeof window !== 'undefined' && 'IntersectionObserver' in window && !prefersReducedMotion());
const [shown, setShown] = useState(false); const [shown, setShown] = useState(false);
@ -95,7 +95,7 @@ function Reveal({ children, className = '', delay = 0 }) {
const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }); }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); 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(); io.observe(ref.current); return () => io.disconnect();
}, [armed]); }, [armed]);
return <div ref={ref} className={`${className} ${armed ? 'reveal' : ''} ${shown ? 'in' : ''}`.replace(/\s+/g, ' ').trim()} style={delay && armed ? { transitionDelay: `${delay}ms` } : undefined}>{children}</div>; return <Tag ref={ref} className={`${className} ${armed ? 'reveal' : ''} ${shown ? 'in' : ''}`.replace(/\s+/g, ' ').trim()} style={delay && armed ? { transitionDelay: `${delay}ms` } : undefined}>{children}</Tag>;
} }
function Home({ setRoute }) { function Home({ setRoute }) {
@ -149,7 +149,7 @@ function ActivityFeed() {
<h3 className="hf-sub">Live in the tavern</h3> <h3 className="hf-sub">Live in the tavern</h3>
<ul className="act-list"> <ul className="act-list">
{items.map((a, i) => ( {items.map((a, i) => (
<li key={i}><span className="act-ico">{ACT_ICON[a.kind] || '•'}</span><span className="act-text">{a.text}</span><span className="act-time">{relTime(a.at)}</span></li> <Reveal tag="li" key={i} delay={Math.min(i, 6) * 50}><span className="act-ico">{ACT_ICON[a.kind] || '•'}</span><span className="act-text">{a.text}</span><span className="act-time">{relTime(a.at)}</span></Reveal>
))} ))}
</ul> </ul>
</div> </div>
@ -309,9 +309,9 @@ function Gatehouse({ auth, setRoute }) {
<div className="gate-sso"><span>🔓</span> One sign-in works across <b>tavern</b>, <b>docs</b>, <b>PM</b> and <b>git</b>.</div> <div className="gate-sso"><span>🔓</span> One sign-in works across <b>tavern</b>, <b>docs</b>, <b>PM</b> and <b>git</b>.</div>
{stats && ( {stats && (
<div className="gate-live"> <div className="gate-live">
<span><b>{stats.classes}</b> classes</span><i /> <span><b><Count n={stats.classes} /></b> classes</span><i />
<span><b>{stats.online}</b> online</span><i /> <span><b><Count n={stats.online} /></b> online</span><i />
<span><b>{stats.sessions}</b> sessions</span> <span><b><Count n={stats.sessions} /></b> sessions</span>
</div> </div>
)} )}
</div> </div>