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
@@ -23,6 +23,8 @@
<script type="text/babel" src="tweaks-panel.jsx"></script>
<script type="text/babel" src="data.jsx"></script>
<script type="text/babel" src="components.jsx"></script>
<script type="text/babel" src="ctp-chat.jsx"></script>
<script type="text/babel" src="calltoplay.jsx"></script>
<script type="text/babel" src="launcher.jsx"></script>
</head>
<body>
@@ -32,7 +34,7 @@
const { useState } = React;
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "#3b82f6",
"accent": "#a855f7",
"density": "normal",
"aspect": "square",
"bg": "gradient",
@@ -47,9 +49,77 @@ function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const heroGame = GAMES.find(g => g.id === 'cs'); // installed + canHostServer → shows Play + Start Server + Uninstall
// Seed data for the "Call to Play" showcase — a live call with chat, a
// fresh call started by you, a SCHEDULED call whose 15-minute check-in
// window just opened (highlighted, you still have to ready up), and one
// scheduled for later tonight that's still collecting "I'm in" RSVPs.
const ctpSeeds = React.useMemo(() => {
const now = Date.now();
const me = (t.username || 'you').trim() || 'you';
return [
{
gameId: 'cs', creator: 'shadowfox', maxPlayers: 10, ageSec: 95, durationMin: 25,
participants: {
pixelqueen: { status: 'ready', joinedAt: now - 60000 },
kbmaster: { status: 'ready', joinedAt: now - 40000 },
nullptr: { status: 'pending', joinedAt: now - 20000, readyAt: now + 50000 },
},
messages: [
{ from: 'shadowfox', text: 'dust2, default rules — who is in?', agoSec: 90 },
{ from: 'kbmaster', text: 'in. someone seed the map pack for nullptr', agoSec: 60 },
{ from: 'nullptr', text: 'downloading, 2 min', agoSec: 35 },
],
},
{
gameId: 'l4d2', creator: me, maxPlayers: 4, ageSec: 15, durationMin: 6,
participants: {
afk_andy: { status: 'pending', joinedAt: now - 5000, readyAt: now + 35000 },
},
},
{
gameId: 'tf2', creator: 'wavedash', maxPlayers: 12, ageSec: 3600, scheduledInMin: 12,
participants: {
[me]: { status: 'in', joinedAt: now - 3000000 },
gg_val: { status: 'ready', joinedAt: now - 2000000 },
crit_happens: { status: 'in', joinedAt: now - 1500000 },
lowping_larry: { status: 'pending', joinedAt: now - 1000000, readyAt: now + 240000 },
},
messages: [
{ from: 'wavedash', text: 'payload maps tonight — check in when you are at your seat', agoSec: 300 },
{ from: 'gg_val', text: 'ready, lobby is open', agoSec: 120 },
],
},
{
gameId: 'amongus', creator: 'pixelqueen', maxPlayers: 10, ageSec: 5400, scheduledInMin: 128,
participants: {
ctrlaltdave: { status: 'in', joinedAt: now - 4000000 },
afk_andy: { status: 'in', joinedAt: now - 3000000 },
},
messages: [
{ from: 'pixelqueen', text: 'right after the pizza break — bring headphones', agoSec: 4800 },
{ from: 'ctrlaltdave', text: 'in. can we start 10 min later?', agoSec: 3600 },
],
},
];
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<React.Fragment>
<DesignCanvas>
<DCSection id="call-to-play" title="Call to Play — now, scheduled &amp; chat"
subtitle="Anyone can call a match — right now (give people N minutes to ready up) or scheduled for a clock time ('Among Us at 20:00'). Scheduled calls collect 'I'm in' RSVPs; 15 minutes before start the call enters its check-in window and lights up everywhere, nudging everyone who's in to answer Ready now or +N minutes. Each call carries a small group chat. The quick bars lead with the status — SCHEDULED / CALL TO PLAY / STARTING SOON / READY, each with its own LED color — sorted ready → starting soon → the rest (soonest first), with per-player ready-bubbles and the latest chat line inline. Click anything to interact live.">
<DCArtboard id="ctp-quickbars" label="Main view — quick bars with ready-bubbles + check-in highlight" width={1340} height={840}>
<Launcher variant="single" tweaks={t} setTweak={setTweak}
initialFilter="all" initialSort="recent"
seedCallToPlay={ctpSeeds}/>
</DCArtboard>
<DCArtboard id="ctp-live" label="Overlay — live call, your call, check-in &amp; scheduled RSVP + chat" width={1340} height={840}>
<Launcher variant="single" tweaks={t} setTweak={setTweak}
initialFilter="all" initialSort="recent"
seedCallToPlay={ctpSeeds} initialCtpOpen={true}/>
</DCArtboard>
</DCSection>
<DCSection id="chrome" title="Chrome variations"
subtitle="Two ways to organize the top bar — pick whichever density of controls you prefer. Click any card to open the detail overlay, or the kebab menu to open Settings.">
<DCArtboard id="single-row" label="A · Single-row + segmented filters" width={1340} height={840}>