b169a05c31
The previous design squeezed the full game-directory path into the
top-bar button as truncated `ui-monospace` (e.g.
`…s/Desktop/eti_games_AFTER_LAN_2025`). In practice the leading-ellipsis
truncation rarely showed the meaningful part of the path on real-world
configurations, ate horizontal space the new 3-zone top bar needs for
its actual primary controls, and competed with the filter / search /
sort cluster for attention.
Replace the inline path with an icon + short label + colored status
dot. The full path moves into the tooltip and `aria-label`, where it's
still one mouseover (and screen-reader friendly) away. The button now
communicates the *state* of the configuration at a glance — which is
what users actually need.
Two visual states, both 36 px tall with the same surface as the other
top-bar controls:
- **Set & valid** — label `Game folder`, green dot (`--ok`) with a
soft glow, default border, tooltip = full path.
- **Not set / invalid** — label `Set game folder`, red dot (`--danger`)
with a soft glow, a red-tinted border, and a faint red wash on
hover so the bad state reads as "this is what you need to fix".
Tooltip = `Please select a game folder`.
"Invalid" (a path is stored but doesn't exist on disk) is collapsed
into the same visual state as "not set" — the user's required action
is identical (open the picker, pick a folder), so a third state
isn't worth the visual budget yet. If we later want to surface a
*last-known* path so the user can re-attach an external drive,
introduce a distinct missing state then.
Implementation notes:
- `DirectoryButton` now takes a single `path: string | null` prop and
picks state from `!!(path && path.trim())`. Children are
`Icon.folder`, the label, and an 8 px `.dirbtn-status-dot` sibling
— the dot is an inline flex sibling, not a corner badge, because
the button is now wider than tall and a corner pin would feel
misplaced.
- `.dirbtn` is `inline-flex` with `padding: 0 14px 0 12px`, gap 8 px,
`white-space: nowrap`, and `flex-shrink: 0`. The `max-width: 360px`
cap from the path-truncation era is gone — the button is now
intrinsically sized.
- Dot glow uses `box-shadow: 0 0 6px color-mix(...)` so it still
reads through the launcher's translucent top-bar background.
- The Tweaks panel grows a dev-only `Game folder set` toggle (under
the new *Library* section) that flips a `gameFolderSet` flag wired
into the Launcher, so reviewers can see both states without
fiddling with real filesystem state. The README explicitly calls
this out as **dev-only** — production state comes from the
settings store, not a user-facing toggle.
The README gains a new *Game-folder button* section with the full
spec, a state table, and a rationale paragraph; the "Changes since v2"
list and the interactions list are updated to reflect the new label
and behavior.
Test Plan
- Open `design_reference/SoftLAN Launcher.html` and locate the Tweaks
panel's *Library → Game folder set* toggle.
- With the toggle **on**: the top-bar button shows `Game folder`
with a green dot; hovering the button reveals the full mock path
in the native tooltip.
- With the toggle **off**: the label switches to `Set game folder`,
the dot turns red, the border picks up a red tint, and hovering
the button reveals `Please select a game folder`.
- Inspect the button with a screen reader / DevTools accessibility
pane: the `aria-label` should read `Game folder: <path>` when set
and `Set game folder` when unset.
130 lines
6.1 KiB
HTML
130 lines
6.1 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="launcher.jsx"></script>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
|
|
<script type="text/babel">
|
|
const { useState } = React;
|
|
|
|
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
|
"accent": "#3b82f6",
|
|
"density": "normal",
|
|
"aspect": "square",
|
|
"bg": "gradient",
|
|
"username": "d",
|
|
"language": "en",
|
|
"gameFolderSet": true
|
|
}/*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
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<DesignCanvas>
|
|
<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"/>
|
|
<TweakToggle label="Game folder set" value={t.gameFolderSet}
|
|
onChange={(v) => setTweak('gameFolderSet', v)}/>
|
|
</TweaksPanel>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
|
|
</script>
|
|
</body>
|
|
</html>
|