docs(launcher): add Call to Play specification and design reference

Extend the launcher design specification and prototype reference with the
"Call to Play" multiplayer coordination feature and clean up top-bar chrome.

Key additions and changes:
- Spec & Roadmap: Document Call to Play mechanics in SPEC.md, including Play
  Now / Scheduled call flavors, 15-minute check-in windows, top-bar button
  with active count badge, quick-bar ticker stack above grid, overlay modal,
  and per-call group chat. Update design/README.md.
- Components & Logic: Add calltoplay.jsx and ctp-chat.jsx components for call
  creation, status progression, and chat. Extend data.jsx with a mock peer
  roster (PEERS) and helper functions for peer install count tracking.
- Top-bar Cleanup: Move game-folder configuration from top bar into Settings ->
  Library, freeing top-bar space for the Call to Play action button.
- Styling & Layout: Add CTP quick-bar, ticker, badge, card, and chat styles to
  styles.css, and integrate CTP components into launcher.jsx, components.jsx,
  and SoftLAN Launcher.html.

Test Plan:
- `git diff --cached --check` -- passed (no trailing whitespace or conflict markers)
- `cargo check --workspace` -- passed cleanly
- Manual review of staged diff across 9 design/launcher files -- confirmed clean staging
This commit is contained in:
2026-07-21 20:00:07 +02:00
parent 8d54e6e954
commit 8f151e38b4
9 changed files with 1831 additions and 10 deletions
@@ -0,0 +1,86 @@
// ctp-chat.jsx — shared Call-to-Play helpers (avatar colors, clock formatting)
// plus the per-call chat panel. Loaded after components.jsx, before calltoplay.jsx.
const { useState: useChatState, useEffect: useChatEffect, useRef: useChatRef } = React;
const CTP_AVATAR_COLORS = ['#60a5fa', '#34d399', '#c084fc', '#fbbf24', '#f472b6', '#38bdf8', '#a3e635', '#fb7185'];
const ctpHashName = (s) => [...String(s)].reduce((a, c) => a + c.charCodeAt(0), 0);
const ctpAvatarColor = (name) => CTP_AVATAR_COLORS[ctpHashName(name) % CTP_AVATAR_COLORS.length];
// 24h clock, LAN-party style: "20:00"
const ctpFmtClock = (ts) => {
const d = new Date(ts);
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
};
// ────────────────────────────────────────────────────────────────────
// Per-call chat — collapsed to a one-line toggle with an unread badge
// and a preview of the last message; expands to a compact IRC-style
// message list + input. All state is local to the card.
// ────────────────────────────────────────────────────────────────────
function CtpChat({ nomination: n, username, onSend, disabled }) {
const IconX = window.Icon;
const msgs = n.messages || [];
const [open, setOpen] = useChatState(false);
const [seen, setSeen] = useChatState(msgs.length);
const [draft, setDraft] = useChatState('');
const listRef = useChatRef(null);
useChatEffect(() => { if (open) setSeen(msgs.length); }, [open, msgs.length]);
useChatEffect(() => {
if (open && listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
}, [open, msgs.length]);
const unread = Math.max(0, msgs.length - seen);
const last = msgs[msgs.length - 1];
const submit = () => {
const t = draft.trim();
if (!t) return;
onSend(t);
setDraft('');
};
return (
<div className={`ctp-chat ${open ? 'is-open' : ''}`}>
<button className="ctp-chat-toggle" onClick={() => setOpen(o => !o)}>
<IconX.chat/>
<span>Chat</span>
{msgs.length > 0 && <span className="ctp-chat-count">{msgs.length}</span>}
{!open && unread > 0 && <span className="ctp-chat-unread">{unread}</span>}
{!open && last && (
<span className="ctp-chat-preview">
<b style={{ color: ctpAvatarColor(last.from) }}>{last.from}:</b> {last.text}
</span>
)}
<span className="ctp-chat-chevron"><IconX.chevron/></span>
</button>
{open && (
<React.Fragment>
<div className="ctp-chat-list" ref={listRef}>
{msgs.length === 0 && <div className="ctp-chat-empty">No messages yet say hi.</div>}
{msgs.map(m => (
<div key={m.id} className={`ctp-chat-msg ${m.from === username ? 'is-me' : ''}`}>
<b style={{ color: ctpAvatarColor(m.from) }}>{m.from}</b>
<span className="ctp-chat-time">{ctpFmtClock(m.at)}</span>
<span className="ctp-chat-text"> {m.text}</span>
</div>
))}
</div>
{!disabled && (
<div className="ctp-chat-form">
<input type="text" className="ctp-chat-input" placeholder="Message the group…"
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}/>
<button className="ctp-chat-send" onClick={submit} aria-label="Send"><IconX.send/></button>
</div>
)}
</React.Fragment>
)}
</div>
);
}
Object.assign(window, { CtpChat, ctpAvatarColor, ctpFmtClock });