diff --git a/index.html b/index.html
index dd691f3..2e7d3d8 100644
--- a/index.html
+++ b/index.html
@@ -17,7 +17,7 @@
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
-
+
diff --git a/package.json b/package.json
index e37b53b..5b14bcb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "amerc-site",
- "version": "2.0.0-tavern",
+ "version": "2.1.0-roster",
"private": true,
"type": "module",
"scripts": {
diff --git a/src/AgentPlatform.jsx b/src/AgentPlatform.jsx
index 4f2730a..82b53f2 100644
--- a/src/AgentPlatform.jsx
+++ b/src/AgentPlatform.jsx
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { api } from './api.js';
import { copyToast } from './toast.js';
import ChatPanel, { SessionLog } from './Chat.jsx';
+import { GameDialog } from './GameDialog.jsx';
const STATUS_COLOR = { online: '#36f0b0', pending: '#f2b85f', lost: '#ff9f5a', down: '#ff7a8c' };
const TAG_EMOJI = { backend: 'π οΈ', model: 'π§ ', frontend: 'π¨', ui: 'ποΈ', web: 'π', data: 'π', devops: 'βοΈ', ml: 'π€', security: 'π', api: 'π', bot: 'π€', chat: 'π¬', test: 'π§ͺ', docs: 'π', design: 'βοΈ', ops: 'π¦', research: 'π¬' };
@@ -77,13 +78,17 @@ export function AgentBrowse({ auth, setRoute }) {
}, [classes]);
return (
-
-
-
-
The mercenary roster
-
Hire a ready-made agent by skill and tag β chat with it, embed it on your site, or drive its terminal. Bring your own and it joins the roster.
+
+
+
+
+
+
+
+
β The mercenary roster
+
Open the door and the mercenaries come down. Hire one β chat, embed, or drive its terminal. {loaded ? {classes.length} answering the call. : 'Musteringβ¦'}
+
setQ(e.target.value)} />
-
setQ(e.target.value)} />
setTag('')}>β
all
@@ -92,38 +97,51 @@ export function AgentBrowse({ auth, setRoute }) {
))}
{err &&
{err}
}
-
- {!loaded && Array.from({ length: 3 }).map((_, i) => (
-
-
-
-
+
+ {!loaded && Array.from({ length: 4 }).map((_, i) => (
+
+
))}
- {loaded && classes.map((c) =>
openClass(c)} />)}
+ {loaded && classes.map((c, i) => (
+ openClass(c)}>
+
+ {c.description || 'A ready blade, awaiting orders.'}
+
+
+
+
+ {classEmoji(c)}
+
+ {!!c.managed && π‘ }
+
+ {c.name}
+ {c.kind} {c.onlineCount}/{c.instanceCount} online Β· by {c.owner}
+
+ ))}
{loaded && classes.length > 0 && (
- setRoute('legion')}>
- οΌ
- Publish your own
- Register an agent class and field it from your legion β your mercenary joins the roster right here.
- Open My Legion β
+ setRoute('legion')}>
+ Bring your own blade β it joins the roster the moment your broker checks in.
+
+ οΌ
+ Publish your own
+ Open My Legion β
)}
{loaded && !classes.length && (q || tag) && (
π
-
No mercenaries match {q ? `β${q}β` : `#${tag}`}.
-
Try a different term or clear the filter.
+
No mercenaries answer {q ? `β${q}β` : `#${tag}`}.
{ setQ(''); setTag(''); }}>Clear search
)}
{loaded && !classes.length && !q && !tag && (
-
No agents here yet β be the first.
+
The hall is empty β be the first blade.
- Publish a class in setRoute('legion')}>My Legion β name it, tag it (e.g. backend, model).
- Run an instance β your broker connects and it goes online .
- Use it β embed it as a chatbox or drive it through a web terminal.
+ Publish a class in setRoute('legion')}>My Legion β name it, tag it.
+ Field an instance β your broker connects and it goes online .
+ Use it β chat, embed, or drive its terminal.
setRoute('legion')}>Open My Legion β
@@ -146,42 +164,41 @@ function ClassModal({ id, auth, onClose }) {
if (!data) return null;
const c = data.class;
return (
-
-
e.stopPropagation()}>
-
-
-
{classEmoji(c)}
-
{c.name} {c.kind} by {c.owner}
-
-
β
+
+
+
{classEmoji(c)}
+
+
by {c.owner}
+
{c.description || 'No description.'}
+
{c.tags.map((t) => {t} )}
-
{c.description}
-
{c.tags.map((t) => {t} )}
-
- {!!c.managed && π‘ amerc-managed Β· amerc skills loaded }
- {c.terminal_mode === 'shared' ? 'π₯ one shared terminal across sessions' : 'π₯ fresh terminal per session'}
- {c.terminal_mode === 'shared' && {c.skills_reload ? 'β» client skills reload each session' : 'βΈ client skills persist between sessions'} }
-
- {err &&
{err}
}
-
- {auth.user ? Get subscription token : Log in to subscribe. }
- {sub && copyToast(sub)}>{sub} β§}
-
-
Instances
-
- {data.instances.map((i) => (
-
-
- #{i.id} {i.name}
- {i.status}{i.broker ? ` Β· ${i.broker}` : ''} Β· {i.visibility}
- setUse(i)}>Use βΈ
-
- ))}
- {!data.instances.length &&
No instances published. The owner can field one in My Legion.
}
-
- {use &&
setUse(null)} />}
-
+
+ {!!c.managed && π‘ amerc-managed Β· amerc skills loaded }
+ {c.terminal_mode === 'shared' ? 'π₯ one shared terminal across sessions' : 'π₯ fresh terminal per session'}
+ {c.terminal_mode === 'shared' && {c.skills_reload ? 'β» client skills reload each session' : 'βΈ client skills persist between sessions'} }
+
+ {err &&
{err}
}
+
+ {auth.user ? Get subscription token : Log in to subscribe. }
+ {sub && copyToast(sub)}>{sub} β§}
+
+
π‘ The squad β {data.instances.length} {data.instances.length === 1 ? 'soldier' : 'soldiers'}
+
+ {data.instances.map((i) => (
+
+
{i.status === 'online' ? 'πͺ' : 'π'}
+
+ #{i.id} {i.name}
+ {i.status}{i.broker ? ` Β· ${i.broker}` : ''} Β· {i.visibility}
+
+
setUse(i)}>{i.status === 'online' ? 'Chat βΈ' : 'down'}
+
+ ))}
+ {!data.instances.length &&
No soldiers fielded yet. The owner can field one in My Legion.
}
+
+ {use &&
setUse(null)} />}
+
);
}
diff --git a/src/GameDialog.jsx b/src/GameDialog.jsx
new file mode 100644
index 0000000..f6ccf0c
--- /dev/null
+++ b/src/GameDialog.jsx
@@ -0,0 +1,27 @@
+import React, { useEffect } from 'react';
+
+// Ornate, gem-cornered in-game dialog. Every "detail view" in amerc opens in
+// one of these β shared by the tavern, the roster, the legion and the mansion.
+export function GameDialog({ title, tag, onClose, children, wide }) {
+ useEffect(() => {
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [onClose]);
+ return (
+
+
e.stopPropagation()} role="dialog" aria-label={title}>
+
+
+
+ {tag && {tag} }
+ {title}
+ β
+
+
{children}
+
+
+ );
+}
+
+export default GameDialog;
diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx
index 879b69a..af7a590 100644
--- a/src/Scene2D.jsx
+++ b/src/Scene2D.jsx
@@ -4,13 +4,14 @@ import { AgentBrowse, LegionDashboard } from './AgentPlatform.jsx';
import Mansion from './Mansion.jsx';
import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx';
+import { GameDialog } from './GameDialog.jsx';
import { api } from './api.js';
// Inline hamburger + close icons (was lucide-react β dropped for one fewer dep).
const Menu = ({ size = 20 }) => ( );
const X = ({ size = 20 }) => ( );
-const RELEASE = '2.0.0-tavern';
+const RELEASE = '2.1.0-roster';
const NAV = [
{ id: 'home', label: 'Tavern' },
@@ -108,29 +109,6 @@ function Reveal({ children, className = '', delay = 0, tag: Tag = 'div' }) {
return {children} ;
}
-// Ornate in-game dialog β every "detail view" opens in one of these.
-export function GameDialog({ title, tag, onClose, children, wide }) {
- useEffect(() => {
- const onKey = (e) => { if (e.key === 'Escape') onClose(); };
- window.addEventListener('keydown', onKey);
- return () => window.removeEventListener('keydown', onKey);
- }, [onClose]);
- return (
-
-
e.stopPropagation()} role="dialog" aria-label={title}>
-
-
-
- {tag && {tag} }
- {title}
- β
-
-
{children}
-
-
- );
-}
-
function Home({ setRoute, auth }) {
const [stats, setStats] = useState(null);
const [dlg, setDlg] = useState(null); // 'tour' | 'login' | null
@@ -498,6 +476,7 @@ function StatusPage() {
}
const CHANGELOG = [
+ { v: '2.1', date: 'Jun 2026', title: 'The roster comes through the door', notes: ['Browse Agents is now a doorway β the mercenaries descend and hang on wooden sign-bubbles showing what each one does', 'Open one and its detail opens in an ornate war-banner dialog; instances are shown as a squad β living soldiers when online, fallen when down'] },
{ v: '2.0', date: 'Jun 2026', title: 'The tavern becomes the interface', notes: ['The home page is now a full-screen game scene β every fixture is a door: the grand door is the roster, the bar signs you in, the banners are your Legion, the side passage leads to My Mansion', 'Speech bubbles everywhere: the dancer pitches the tour in a glowing holo-panel, the barkeep greets you by name', 'Details open in ornate in-game dialogs instead of plain pages'] },
{ v: '1.3', date: 'Jun 2026', title: 'Mansion learns to handle files', notes: ['File-mapper: your agent maps a file to a stable link β every download streams live through your broker, nothing stored on amerc', 'Net-disk opened as a Mansion room: 100 MB per account, with a live usage meter', 'And the roster has a real agent online again β say hello to Kebab Webagent'] },
{ v: '1.2', date: 'Jun 2026', title: 'Legion fields agent cards', notes: ['Your classes in My Legion are now the same agent cards as Browse Agents β open one to see and use its instances right there', 'New class properties at publish time: amerc-managed (loads amerc skills), terminal-per-session vs shared terminal, and skills-reload on shared terminals'] },
diff --git a/src/styles.css b/src/styles.css
index 858f225..cb6e6e6 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1712,3 +1712,56 @@ button {
.gm-stage3 { padding: 4px; }
.gm-tagline { font-size: 19px; }
}
+
+/* ===== Browse = the roster door; mercenaries descend (2.1) ===== */
+.apb { max-width: 1080px; margin: 0 auto; }
+.apb-doorway { display: flex; align-items: center; gap: 20px; padding: 14px 4px 18px; }
+.apb-door { position: relative; flex: 0 0 auto; width: 96px; height: 116px; border-radius: 48px 48px 8px 8px; background: #20140a; border: 3px solid #5a3a1c; overflow: hidden; box-shadow: inset 0 0 18px rgba(0,0,0,0.7); perspective: 200px; }
+.apb-door-light { position: absolute; inset: 4px; border-radius: 44px 44px 6px 6px; background: radial-gradient(ellipse at 50% 40%, rgba(127,233,255,0.85), rgba(40,120,200,0.35) 55%, transparent 75%); animation: apbGlow 3s ease-in-out infinite; }
+@keyframes apbGlow { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
+.apb-door-leaf { position: absolute; top: 0; width: 50%; height: 100%; background: linear-gradient(180deg, #6b4326, #3a2414); transform-origin: var(--o) center; box-shadow: inset 0 0 8px rgba(0,0,0,0.6); }
+.apb-door-leaf.l { left: 0; --o: left; transform: rotateY(58deg); border-right: 2px solid #2a1810; border-radius: 46px 0 0 6px; }
+.apb-door-leaf.r { right: 0; --o: right; transform: rotateY(-58deg); border-left: 2px solid #2a1810; border-radius: 0 46px 6px 0; }
+.apb-doorhead { flex: 1; min-width: 0; }
+.apb-doorhead h2 { margin: 0 0 4px; font-family: Georgia, serif; font-size: 24px; color: #f3e9d2; }
+.apb-doorhead p { margin: 0 0 10px; color: #9b8fb8; font-size: 13px; }
+.apb-doorhead .ap-search { width: min(360px, 100%); }
+
+.apb-stage { display: flex; flex-wrap: wrap; gap: 30px 22px; padding: 30px 6px 24px; align-items: flex-start; }
+@keyframes apbDrop { from { transform: translateY(-46px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
+.apb-merc { position: relative; width: 156px; display: flex; flex-direction: column; align-items: center; gap: 6px; background: none; border: none; cursor: pointer; font-family: inherit; padding: 0; animation: apbDrop 0.55s cubic-bezier(0.2, 1.3, 0.4, 1) backwards; animation-delay: var(--d, 0ms); }
+.apb-rope { position: absolute; top: -30px; left: 50%; width: 2px; height: 30px; background: linear-gradient(180deg, transparent, #6b5a3a); transform: translateX(-50%); }
+.apb-bub { position: relative; background: linear-gradient(180deg, #241509, #170d05); border: 2px solid #8a5a32; color: #f3e3c2; border-radius: 11px; padding: 8px 11px; font-size: 11.5px; line-height: 1.4; text-align: left; min-height: 38px; display: flex; align-items: center; box-shadow: 0 3px 0 rgba(0,0,0,0.4); width: 100%; box-sizing: border-box; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
+.apb-bub-tail { position: absolute; left: 50%; bottom: -7px; width: 11px; height: 11px; background: #170d05; border-right: 2px solid #8a5a32; border-bottom: 2px solid #8a5a32; transform: translateX(-50%) rotate(45deg); }
+.apb-portrait { position: relative; width: 72px; height: 72px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 30px; box-shadow: 0 0 0 3px #2a2440, 0 8px 18px rgba(0,0,0,0.5); transition: transform 0.15s; }
+.apb-merc:hover .apb-portrait { transform: scale(1.07) translateY(-2px); }
+.apb-merc:hover .apb-bub { border-color: #ffd75e; }
+.apb-emoji { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
+.apb-life { position: absolute; right: 2px; bottom: 2px; width: 16px; height: 16px; border-radius: 50%; border: 3px solid #0c0918; }
+.apb-life.on { background: #36f0b0; box-shadow: 0 0 9px #36f0b0; animation: chpulse 2s infinite; }
+.apb-life.off { background: #54607a; }
+.apb-crest { position: absolute; left: -4px; top: -4px; font-size: 15px; filter: drop-shadow(0 0 4px rgba(54,240,176,0.6)); }
+.apb-name { color: #eee6ff; font-size: 13.5px; text-align: center; }
+.apb-meta { color: #8a7bb0; font-size: 10.5px; text-align: center; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; justify-content: center; }
+.apb-tag-chip { background: #1a2740; border: 1px solid #2a3a5a; color: #9fb2c8; border-radius: 999px; padding: 1px 7px; font-size: 9.5px; }
+.apb-portrait-add { background: #141022; border: 2px dashed #4a3f6e; color: #8a7bb0; font-size: 28px; box-shadow: none; }
+.apb-merc-add .apb-name { color: #b6a7d4; }
+.apb-skel { pointer-events: none; }
+.apb-skel .apb-bub { border-color: #243049; }
+
+/* ClassModal as a war-banner dialog: squad of soldiers */
+.cm-top { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
+.cm-avatar { width: 64px; height: 64px; font-size: 30px; flex: 0 0 auto; }
+.cm-top-body { min-width: 0; }
+.cm-top-body .ap-desc { margin: 6px 0 8px; }
+.cm-squad { display: flex; flex-direction: column; gap: 8px; }
+.cm-sol { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 10px; border: 1px solid #243049; background: #0b0f1a; }
+.cm-sol.alive { border-color: #1f5642; box-shadow: inset 0 0 16px rgba(54,240,176,0.06); }
+.cm-sol.fallen { opacity: 0.62; }
+.cm-sol-ico { font-size: 24px; filter: grayscale(0); }
+.cm-sol.fallen .cm-sol-ico { filter: grayscale(0.5); }
+.cm-sol-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
+.cm-sol-body strong { color: #eee6ff; font-size: 13px; }
+.cm-sol-meta { display: flex; align-items: center; gap: 6px; color: #8a7bb0; font-size: 11px; }
+.cm-sol-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
+@media (max-width: 620px) { .apb-merc { width: 132px; } .apb-doorway { flex-wrap: wrap; } }