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
200 lines
10 KiB
HTML
200 lines
10 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>SoftLAN Launcher — Redesign</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap">
|
|
<link rel="stylesheet" href="styles.css">
|
|
<style>
|
|
html, body { margin: 0; padding: 0; background: #f0eee9; height: 100%; }
|
|
/* Tighten canvas chrome a hair */
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
#root { width: 100%; height: 100%; }
|
|
</style>
|
|
|
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
|
|
<script type="text/babel" src="design-canvas.jsx"></script>
|
|
<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>
|
|
<div id="root"></div>
|
|
|
|
<script type="text/babel">
|
|
const { useState } = React;
|
|
|
|
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
|
"accent": "#a855f7",
|
|
"density": "normal",
|
|
"aspect": "square",
|
|
"bg": "gradient",
|
|
"username": "ddidderr",
|
|
"language": "en",
|
|
"gameFolder": "\/some\/folder\/to\/games"
|
|
}/*EDITMODE-END*/;
|
|
|
|
const ACCENTS = ['#3b82f6', '#22d3ee', '#a855f7', '#22c55e', '#f59e0b', '#ef4444'];
|
|
|
|
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 & 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 & 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}>
|
|
<Launcher variant="single" tweaks={t} setTweak={setTweak}
|
|
initialFilter="all" initialSort="recent"/>
|
|
</DCArtboard>
|
|
|
|
<DCArtboard id="two-row" label="B · Two-row + underlined tabs" width={1340} height={840}>
|
|
<Launcher variant="two" tweaks={t} setTweak={setTweak}
|
|
initialFilter="installed" initialSort="az"/>
|
|
</DCArtboard>
|
|
</DCSection>
|
|
|
|
<DCSection id="detail" title="Game detail overlay"
|
|
subtitle="Opens when you click a card. Full description, metadata, primary action + secondary actions (incl. uninstall).">
|
|
<DCArtboard id="detail-modal" label="C · Detail overlay (installed, can host server)" width={1340} height={840}>
|
|
<Launcher variant="single" tweaks={t} setTweak={setTweak}
|
|
initialFilter="installed" initialSort="az"
|
|
initialOpenGame={heroGame}/>
|
|
</DCArtboard>
|
|
<DCArtboard id="detail-modal-local" label="D · Detail overlay (downloaded, not installed)" width={1340} height={840}>
|
|
<Launcher variant="single" tweaks={t} setTweak={setTweak}
|
|
initialFilter="local" initialSort="az"
|
|
initialOpenGame={GAMES.find(g => g.id === 'cod4mw')}/>
|
|
</DCArtboard>
|
|
<DCArtboard id="detail-modal-downloading" label="E · Detail overlay (downloading)" width={1340} height={840}>
|
|
<Launcher variant="two" tweaks={t} setTweak={setTweak}
|
|
initialFilter="local" initialSort="state"
|
|
initialOpenGame={GAMES.find(g => g.id === 'avp')}/>
|
|
</DCArtboard>
|
|
</DCSection>
|
|
|
|
<DCSection id="settings" title="Settings dialog"
|
|
subtitle="Same controls as the dev Tweaks panel, surfaced as an in-app preferences dialog. Open via top-bar menu → Settings.">
|
|
<DCArtboard id="settings-open" label="F · Settings dialog (open)" width={1340} height={840}>
|
|
<Launcher variant="single" tweaks={t} setTweak={setTweak}
|
|
initialFilter="all" initialSort="recent"
|
|
initialSettingsOpen={true}/>
|
|
</DCArtboard>
|
|
</DCSection>
|
|
</DesignCanvas>
|
|
|
|
<TweaksPanel>
|
|
<TweakSection label="Profile"/>
|
|
<TweakText label="Username" value={t.username}
|
|
onChange={(v) => setTweak('username', v)}/>
|
|
<TweakRadio label="Language" value={t.language}
|
|
options={[{value: 'en', label: 'English'}, {value: 'de', label: 'Deutsch'}]}
|
|
onChange={(v) => setTweak('language', v)}/>
|
|
|
|
<TweakSection label="Theme"/>
|
|
<TweakColor label="Accent" value={t.accent} options={ACCENTS}
|
|
onChange={(v) => setTweak('accent', v)}/>
|
|
<TweakRadio label="Background" value={t.bg}
|
|
options={['flat', 'gradient', 'animated']}
|
|
onChange={(v) => setTweak('bg', v)}/>
|
|
|
|
<TweakSection label="Grid"/>
|
|
<TweakRadio label="Density" value={t.density}
|
|
options={['compact', 'normal', 'large']}
|
|
onChange={(v) => setTweak('density', v)}/>
|
|
<TweakRadio label="Cover aspect" value={t.aspect}
|
|
options={['box', 'square', 'banner']}
|
|
onChange={(v) => setTweak('aspect', v)}/>
|
|
|
|
<TweakSection label="Library"/>
|
|
<TweakText label="Game folder" value={t.gameFolder}
|
|
onChange={(v) => setTweak('gameFolder', v)}/>
|
|
</TweaksPanel>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
|
|
</script>
|
|
</body>
|
|
</html>
|