docs: update launcher design for profile and server actions

Document the profile settings added to the launcher design and the new
Start Server detail action. The settings contract now includes a persisted
username and language choice, and the game detail overlay shows Start Server
only for installed games that can host a dedicated server.

The reference mock now includes the matching Profile controls, a server icon,
server-capable sample catalog entries, and the updated detail/settings
artboards so implementation can follow the selected design direction.

Test Plan:
- git diff --cached --check

Refs: design/README.md
This commit is contained in:
2026-05-21 09:23:05 +02:00
parent 12a0d7abe9
commit 91c709960a
5 changed files with 186 additions and 30 deletions
+12 -3
View File
@@ -35,14 +35,16 @@ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "#3b82f6",
"density": "normal",
"aspect": "square",
"bg": "gradient"
"bg": "gradient",
"username": "d",
"language": "en"
}/*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 === 'ra3'); // installed → modal shows Play + Uninstall
const heroGame = GAMES.find(g => g.id === 'cs'); // installed + canHostServer → shows Play + Start Server + Uninstall
return (
<React.Fragment>
@@ -62,7 +64,7 @@ function App() {
<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 game)" width={1340} height={840}>
<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}/>
@@ -90,6 +92,13 @@ function App() {
</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)}/>
+37
View File
@@ -9,6 +9,7 @@ const { useState, useMemo, useRef, useEffect } = React;
const Icon = {
search: (p) => <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="7" cy="7" r="5"/><path d="m13.5 13.5-3-3"/></svg>,
play: (p) => <svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" {...p}><path d="M4 2.5v11l10-5.5z"/></svg>,
server: (p) => <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="2" y="3" width="12" height="4.5" rx="1"/><rect x="2" y="8.5" width="12" height="4.5" rx="1"/><circle cx="4.6" cy="5.25" r=".55" fill="currentColor" stroke="none"/><circle cx="4.6" cy="10.75" r=".55" fill="currentColor" stroke="none"/><path d="M7 5.25h4.5M7 10.75h4.5"/></svg>,
install:(p) => <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M8 2v8"/><path d="m4.5 7 3.5 3.5L11.5 7"/><path d="M2.5 12.5h11"/></svg>,
download:(p)=> <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M8 2v8"/><path d="m4.5 7 3.5 3.5L11.5 7"/><path d="M2.5 13.5h11"/></svg>,
folder: (p) => <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" {...p}><path d="M1.75 3.75v8.5a1 1 0 0 0 1 1h10.5a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7.5L6 2.75H2.75a1 1 0 0 0-1 1z"/></svg>,
@@ -435,6 +436,13 @@ function GameDetailModal({ game, accent, onClose }) {
<p className="modal-desc">{game.desc}</p>
<div className="modal-actions">
<ActionButton state={game.state} accent={accent} size="lg" game={game}/>
{game.canHostServer && game.state === 'installed' && (
<button className="act-btn act-lg act-server"
style={{ '--accent': accent }}
onClick={(e) => e.stopPropagation()}>
<Icon.server/><span>Start Server</span>
</button>
)}
{game.state === 'installed' && (
<button className="ghost-btn ghost-danger"><Icon.trash/><span>Uninstall</span></button>
)}
@@ -468,8 +476,22 @@ const SETTING_OPTIONS = {
bg: [{ value: 'flat', label: 'Flat' }, { value: 'gradient', label: 'Gradient' }, { value: 'animated', label: 'Animated' }],
density: [{ value: 'compact', label: 'Compact' }, { value: 'normal', label: 'Normal' }, { value: 'large', label: 'Large' }],
aspect: [{ value: 'box', label: 'Box-art' }, { value: 'square', label: 'Square' }, { value: 'banner', label: 'Banner' }],
language: [{ value: 'en', label: 'English' }, { value: 'de', label: 'Deutsch' }],
};
function SettingsTextInput({ value, placeholder, maxLength = 24, onChange, accent }) {
return (
<div className="settings-text" style={{ '--accent': accent }}>
<input type="text"
value={value || ''}
placeholder={placeholder}
maxLength={maxLength}
spellCheck={false}
onChange={(e) => onChange(e.target.value)}/>
</div>
);
}
function SettingsRow({ label, hint, children }) {
return (
<div className="settings-row">
@@ -524,6 +546,21 @@ function SettingsDialog({ settings, onChange, onClose }) {
<button className="modal-close settings-close" onClick={onClose} aria-label="Close"><Icon.close/></button>
</div>
<div className="settings-body">
<div className="settings-section">
<div className="settings-section-title">Profile</div>
<SettingsRow label="Username" hint="Shown to other players on the LAN">
<SettingsTextInput value={settings.username}
placeholder="Enter a username"
onChange={(v) => onChange('username', v)}
accent={settings.accent}/>
</SettingsRow>
<SettingsRow label="Language" hint="Interface language">
<SegmentedRadio value={settings.language || 'en'}
options={SETTING_OPTIONS.language}
onChange={(v) => onChange('language', v)}
accent={settings.accent}/>
</SettingsRow>
</div>
<div className="settings-section">
<div className="settings-section-title">Appearance</div>
<SettingsRow label="Accent color" hint="Used for primary actions and highlights">
+14 -14
View File
@@ -28,13 +28,13 @@ const GAMES = [
cover: { c1: '#ef4444', c2: '#1e3a8a', accent: '#fef08a', mood: 'playful' },
},
{
id: 'bf1942', title: 'Battlefield 1942', size: 7.8, version: '2016.01.30',
id: 'bf1942', title: 'Battlefield 1942', size: 7.8, version: '2016.01.30', canHostServer: true,
desc: "The original Battlefield. WWII on land, sea, and air across 16 maps. The mod scene basically reinvented PC gaming on top of this engine.",
state: 'installed', players: '264', tags: ['FPS', 'Vehicles', 'LAN'],
cover: { c1: '#92400e', c2: '#1c1917', accent: '#facc15', mood: 'war' },
},
{
id: 'bf2', title: 'Battlefield 2 Complete', size: 8.0, version: '2021.12.27',
id: 'bf2', title: 'Battlefield 2 Complete', size: 8.0, version: '2021.12.27', canHostServer: true,
desc: "Modern combat with commander mode, squads, and the kind of jet-vs-jet duels you tell stories about for a decade.",
state: 'local', players: '264', tags: ['FPS', 'Vehicles', 'Tactical'],
cover: { c1: '#3f3f46', c2: '#0a0a0a', accent: '#22d3ee', mood: 'tactical' },
@@ -46,19 +46,19 @@ const GAMES = [
cover: { c1: '#f97316', c2: '#7c2d12', accent: '#fde047', mood: 'arcade' },
},
{
id: 'cod2', title: 'Call of Duty 2', size: 7.0, version: '2016.09.22',
id: 'cod2', title: 'Call of Duty 2', size: 7.0, version: '2016.09.22', canHostServer: true,
desc: "WWII shooter — Russian, British and American campaigns, plus the multiplayer that defined LAN parties for years.",
state: 'installed', players: '232', tags: ['FPS', 'War'],
cover: { c1: '#57534e', c2: '#1c1917', accent: '#fbbf24', mood: 'war' },
},
{
id: 'cod4mw', title: 'Call of Duty 4: Modern Warfare', size: 13.0, version: '2016.09.21',
id: 'cod4mw', title: 'Call of Duty 4: Modern Warfare', size: 13.0, version: '2016.09.21', canHostServer: true,
desc: "The shooter that flipped the genre to modern combat and minted a generation of esports careers. All Ghillied Up still holds up.",
state: 'local', players: '232', tags: ['FPS', 'Modern'],
cover: { c1: '#525252', c2: '#0a0a0a', accent: '#84cc16', mood: 'tactical' },
},
{
id: 'coduo', title: 'Call of Duty: United Offensive', size: 3.8, version: '2018.09.08',
id: 'coduo', title: 'Call of Duty: United Offensive', size: 3.8, version: '2018.09.08', canHostServer: true,
desc: "Expansion to the original CoD. Battle of the Bulge, Sicily, Kursk. Adds tanks, B-17 sequences, and the flamethrower nobody asked for but everybody loved.",
state: 'none', players: '232', tags: ['FPS', 'Expansion'],
cover: { c1: '#78716c', c2: '#292524', accent: '#fb923c', mood: 'war' },
@@ -76,37 +76,37 @@ const GAMES = [
cover: { c1: '#a16207', c2: '#422006', accent: '#facc15', mood: 'war' },
},
{
id: 'cs', title: 'Counter-Strike 1.6', size: 0.7, version: '2014.01.21',
id: 'cs', title: 'Counter-Strike 1.6', size: 0.7, version: '2014.01.21', canHostServer: true,
desc: "The 1.6 build still everyone insists was the peak. Terrorists vs Counter-Terrorists, AWP camping, de_dust2.",
state: 'installed', players: '232', tags: ['FPS', 'Competitive', 'LAN'],
cover: { c1: '#1e40af', c2: '#0c1f3a', accent: '#fbbf24', mood: 'tactical' },
},
{
id: 'css', title: 'Counter-Strike: Source', size: 4.3, version: '2014.10.23',
id: 'css', title: 'Counter-Strike: Source', size: 4.3, version: '2014.10.23', canHostServer: true,
desc: "CS reborn on the Source engine. Same maps, same rules, with physics that lets the molotovs work properly.",
state: 'installed', players: '232', tags: ['FPS', 'Competitive'],
cover: { c1: '#1d4ed8', c2: '#0c1f3a', accent: '#f59e0b', mood: 'tactical' },
},
{
id: 'cube2', title: 'Cube 2: Sauerbraten', size: 0.4, version: '2013.09.20',
id: 'cube2', title: 'Cube 2: Sauerbraten', size: 0.4, version: '2013.09.20', canHostServer: true,
desc: "Open-source arena FPS with in-game level editing. Fast, free, and one of those things every LAN party always had a copy of.",
state: 'none', players: '216', tags: ['FPS', 'Open Source'],
cover: { c1: '#dc2626', c2: '#7f1d1d', accent: '#f1f5f9', mood: 'arcade' },
},
{
id: 'doom3', title: 'Doom 3', size: 2.2, version: '2012.01.31',
id: 'doom3', title: 'Doom 3', size: 2.2, version: '2012.01.31', canHostServer: true,
desc: "Sci-fi horror reboot of the franchise. Mars Research Facility, demonic incursion, the shotgun that started a flashlight debate.",
state: 'local', players: '216', tags: ['FPS', 'Horror'],
cover: { c1: '#7f1d1d', c2: '#000000', accent: '#f97316', mood: 'dark' },
},
{
id: 'l4d2', title: 'Left 4 Dead 2', size: 6.5, version: '2020.08.15',
id: 'l4d2', title: 'Left 4 Dead 2', size: 6.5, version: '2020.08.15', canHostServer: true,
desc: "Co-op zombie survival with the AI Director rewriting every campaign run. Bring four friends or four strangers; the chainsaw works the same.",
state: 'installed', players: '18', tags: ['Co-op', 'FPS', 'Horror'],
cover: { c1: '#15803d', c2: '#052e16', accent: '#fef08a', mood: 'horror' },
},
{
id: 'minecraft', title: 'Minecraft', size: 1.0, version: '2024.03.01',
id: 'minecraft', title: 'Minecraft', size: 1.0, version: '2024.03.01', canHostServer: true,
desc: "Infinite voxel sandbox. Build, mine, survive, get blown up by a creeper. The LAN button is right there.",
state: 'installed', players: '2100', tags: ['Sandbox', 'Survival', 'LAN'],
cover: { c1: '#15803d', c2: '#7c2d12', accent: '#fde68a', mood: 'sandbox' },
@@ -118,7 +118,7 @@ const GAMES = [
cover: { c1: '#ea580c', c2: '#1e293b', accent: '#22d3ee', mood: 'tech' },
},
{
id: 'quake3', title: 'Quake III Arena', size: 0.5, version: '2010.08.15',
id: 'quake3', title: 'Quake III Arena', size: 0.5, version: '2010.08.15', canHostServer: true,
desc: "Arena FPS in its most distilled form. Rocket jumps, rail-gun duels, and a netcode that's still the benchmark.",
state: 'downloading', progress: 0.71, speed: 12.8, peers: 3, players: '216', tags: ['FPS', 'Arena', 'LAN'],
cover: { c1: '#7f1d1d', c2: '#0a0a0a', accent: '#fbbf24', mood: 'dark' },
@@ -130,13 +130,13 @@ const GAMES = [
cover: { c1: '#1e3a8a', c2: '#172554', accent: '#22d3ee', mood: 'scifi' },
},
{
id: 'tf2', title: 'Team Fortress 2', size: 22.0, version: '2023.06.18',
id: 'tf2', title: 'Team Fortress 2', size: 22.0, version: '2023.06.18', canHostServer: true,
desc: "Class-based shooter with nine archetypes, absurd hats, and a meta with more history than most actual sports.",
state: 'local', players: '232', tags: ['FPS', 'Class-based'],
cover: { c1: '#b45309', c2: '#7c2d12', accent: '#fbbf24', mood: 'cartoon' },
},
{
id: 'ut2k4', title: 'Unreal Tournament 2004', size: 5.2, version: '2012.06.01',
id: 'ut2k4', title: 'Unreal Tournament 2004', size: 5.2, version: '2012.06.01', canHostServer: true,
desc: "Arena shooter at maximum velocity. Onslaught, Assault, Bombing Run — vehicles, jump boots, the announcer screaming HEADSHOT.",
state: 'none', players: '232', tags: ['FPS', 'Arena'],
cover: { c1: '#854d0e', c2: '#422006', accent: '#fde047', mood: 'arena' },
+48
View File
@@ -630,6 +630,22 @@
}
.act-download:hover { background: rgba(255,255,255,0.12); border-color: var(--bd-3); }
/* Start Server — secondary "primary" action sitting next to Play.
Uses the accent as a tinted fill + border so it reads as host-action
without competing with the green Play button. */
.act-server {
color: var(--t-1);
background: color-mix(in srgb, var(--accent) 14%, rgba(255,255,255,0.04));
border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
}
.act-server:hover {
background: color-mix(in srgb, var(--accent) 22%, rgba(255,255,255,0.04));
border-color: color-mix(in srgb, var(--accent) 75%, transparent);
filter: none;
}
.act-server svg { color: var(--accent); }
/* ─── Download progress (in place of action button when state === 'downloading') ─── */
.dl {
position: relative;
@@ -1135,3 +1151,35 @@
color: white;
box-shadow: 0 2px 8px -2px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.18);
}
/* ─── Settings: text input ─── */
.settings-text {
display: inline-flex;
align-items: center;
background: var(--bg-3);
border: 1px solid var(--bd-1);
border-radius: 8px;
padding: 0 12px;
height: 36px;
width: 220px;
transition: border-color .15s, box-shadow .15s, background .15s;
}
.settings-text:focus-within {
background: var(--bg-2);
border-color: var(--accent, #3b82f6);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent, #3b82f6) 22%, transparent);
}
.settings-text input {
flex: 1; min-width: 0;
background: transparent;
border: 0; outline: 0;
color: var(--t-1);
font: inherit;
font-size: 13.5px;
font-weight: 600;
letter-spacing: 0.1px;
}
.settings-text input::placeholder {
color: var(--t-3);
font-weight: 500;
}