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
@@ -15,6 +15,8 @@ function applyFilterAndSort(games, filter, sort, query) {
} else if (sort === 'recent') {
const order = { installed: 0, local: 1, none: 2 };
g = [...g].sort((a, b) => order[a.state] - order[b.state] || b.version.localeCompare(a.version));
} else if (sort === 'peers') {
g = [...g].sort((a, b) => installedPeersFor(b).length - installedPeersFor(a).length || a.title.localeCompare(b.title));
}
return g;
}
@@ -25,6 +27,8 @@ function Launcher({
initialFilter = 'all', initialSort = 'recent', initialQuery = '',
initialOpenGame = null,
initialSettingsOpen = false,
seedCallToPlay = [],
initialCtpOpen = false,
}) {
const { density, aspect, accent, bg } = tweaks;
const [filter, setFilter] = useState(initialFilter);
@@ -32,6 +36,11 @@ function Launcher({
const [query, setQuery] = useState(initialQuery);
const [openGame, setOpenGame] = useState(initialOpenGame);
const [settingsOpen, setSettingsOpen] = useState(initialSettingsOpen);
const [ctpOpen, setCtpOpen] = useState(initialCtpOpen);
const [ctpFocusId, setCtpFocusId] = useState(null);
const nomUsername = tweaks.username && tweaks.username.trim() ? tweaks.username.trim() : 'you';
const ctp = useNominations({ username: nomUsername, seed: seedCallToPlay });
const counts = useMemo(() => countByFilter(GAMES), []);
const list = useMemo(() => applyFilterAndSort(GAMES, filter, sort, query), [filter, sort, query]);
@@ -66,6 +75,7 @@ function Launcher({
<SortMenu value={sort} onChange={setSort} accent={accent}/>
</div>
<div className="topbar-right-trail">
<CallToPlayButton nominations={ctp.nominations} onClick={() => setCtpOpen(true)}/>
<KebabMenu items={menuItems}/>
</div>
</div>
@@ -79,6 +89,7 @@ function Launcher({
</div>
<div className="topbar-row1-right">
<StorageMeter accent={accent}/>
<CallToPlayButton nominations={ctp.nominations} onClick={() => setCtpOpen(true)}/>
<KebabMenu items={menuItems}/>
</div>
</div>
@@ -93,6 +104,8 @@ function Launcher({
)}
<main className="grid-wrap">
<CallToPlayTicker nominations={ctp.nominations} accent={accent}
onOpen={(id) => { setCtpFocusId(id); setCtpOpen(true); }}/>
{variant === 'single' && (
<div className="results-bar">
<div className="results-count">
@@ -113,6 +126,12 @@ function Launcher({
{settingsOpen && setTweak && (
<SettingsDialog settings={tweaks} onChange={setTweak} onClose={() => setSettingsOpen(false)}/>
)}
{ctpOpen && (
<CallToPlayOverlay nominations={ctp.nominations} username={nomUsername} accent={accent}
focusId={ctpFocusId}
onClose={() => { setCtpOpen(false); setCtpFocusId(null); }}
actions={{ createNomination: ctp.createNomination, respond: ctp.respond, rsvp: ctp.rsvp, sendMessage: ctp.sendMessage, leave: ctp.leave, cancel: ctp.cancel, startNow: ctp.startNow, addTime: ctp.addTime }}/>
)}
</div>
);
}