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:
@@ -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)}/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: '2–64', 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: '2–64', 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: '2–32', 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: '2–32', 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: '2–32', 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: '2–32', 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: '2–32', 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: '2–16', 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: '2–16', 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: '1–8', 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: '2–100', 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: '2–16', 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: '2–32', 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: '2–32', tags: ['FPS', 'Arena'],
|
||||
cover: { c1: '#854d0e', c2: '#422006', accent: '#fde047', mood: 'arena' },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user