Files
lanspread/design/design_reference/launcher.jsx
T
ddidderr 27c71978d2 docs(design): add SoftLAN launcher redesign handoff and references
Add the `design/` directory containing the design handoff document and
HTML/React reference prototypes for the planned Steam-inspired redesign
of the launcher UI.

Contents:

- `design/README.md` — handoff spec. Defines screens (main library,
  game detail overlay, in-app Settings dialog), the game card anatomy,
  interaction behavior, transitions, state shape, design tokens
  (colors, typography, spacing, shadows) and out-of-scope items.
  Selects layout variant A (single-row top bar) as the primary
  direction. High-fidelity: colors / typography / spacing / animations
  are decided, pixel-fidelity to the mock is the goal.

- `design/design_reference/` — Babel-in-browser React prototypes built
  to communicate intended look and behavior. Includes:
  * `SoftLAN Launcher.html` — entry that wires React + Babel and
    mounts the design canvas with all variants side-by-side.
  * `styles.css` — full visual spec as CSS custom properties + named
    component classes (`.topbar`, `.seg`, `.card`, `.modal`, etc.).
  * `data.jsx` — mock game catalog plus filter/sort helpers and a
    mock STORAGE record used by the storage meter.
  * `components.jsx` — reusable building blocks (Icon set, GameCover
    placeholder generator, StateChip, ActionButton, GameCard,
    SegmentedFilters, UnderlineFilters, SearchField, SortMenu,
    StorageMeter, DirectoryButton, KebabMenu, GameDetailModal,
    SettingsDialog).
  * `launcher.jsx` — composes top bar + grid + modals into a complete
    launcher screen, in both `single`-row and `two`-row chrome
    variants.

These files are reference material, not production code. They are not
imported by the Vite/Tauri build and ship outside the frontend crate
(`crates/lanspread-tauri-deno-ts/`). They are committed so the design
intent is reviewable in-repo and surviving across implementations.

The accompanying production implementation against this spec is in
follow-up commits.

Trailer
-------
Refs: design/README.md (canonical handoff)
2026-05-19 19:59:36 +02:00

113 lines
4.5 KiB
React

// launcher.jsx — composes top bar + grid into a complete launcher screen
// Comes in two chrome variants: 'single' (one-row) and 'two' (two-row).
const DIR_PATH = '/home/pfs/Desktop/eti_games_AFTER_LAN_2025';
function applyFilterAndSort(games, filter, sort, query) {
let g = filterGames(games, filter);
if (query.trim()) {
const q = query.toLowerCase();
g = g.filter(x => x.title.toLowerCase().includes(q) || x.tags.some(t => t.toLowerCase().includes(q)));
}
if (sort === 'az') g = [...g].sort((a, b) => a.title.localeCompare(b.title));
else if (sort === 'size') g = [...g].sort((a, b) => b.size - a.size);
else if (sort === 'state') {
const order = { installed: 0, local: 1, none: 2 };
g = [...g].sort((a, b) => order[a.state] - order[b.state] || a.title.localeCompare(b.title));
} else if (sort === 'recent') {
const order = { installed: 0, local: 1, none: 2 };
g = [...g].sort((a, b) => order[a.state] - order[b.state] || b.version.localeCompare(a.version));
}
return g;
}
function Launcher({
variant,
tweaks, setTweak,
initialFilter = 'all', initialSort = 'recent', initialQuery = '',
initialOpenGame = null,
initialSettingsOpen = false,
}) {
const { density, aspect, accent, bg } = tweaks;
const [filter, setFilter] = useState(initialFilter);
const [sort, setSort] = useState(initialSort);
const [query, setQuery] = useState(initialQuery);
const [openGame, setOpenGame] = useState(initialOpenGame);
const [settingsOpen, setSettingsOpen] = useState(initialSettingsOpen);
const counts = useMemo(() => countByFilter(GAMES), []);
const list = useMemo(() => applyFilterAndSort(GAMES, filter, sort, query), [filter, sort, query]);
const menuItems = [
{ label: 'Settings', onClick: () => setSettingsOpen(true) },
{ label: 'Refresh library', onClick: () => {} },
'-',
{ label: 'Unpack logs', onClick: () => {} },
{ label: 'About SoftLAN', onClick: () => {} },
];
return (
<div className={`launcher launcher-${variant} bg-${bg} density-${density}`}
style={{ '--accent': accent }}>
{variant === 'single' ? (
<header className="topbar topbar-single">
<div className="brand">
<div className="brand-mark" style={{ background: accent }}>S</div>
<div className="brand-name">SoftLAN</div>
</div>
<SegmentedFilters value={filter} onChange={setFilter} counts={counts} accent={accent}/>
<SearchField value={query} onChange={setQuery} accent={accent} wide/>
<SortMenu value={sort} onChange={setSort} accent={accent}/>
<DirectoryButton path={DIR_PATH}/>
<KebabMenu items={menuItems}/>
</header>
) : (
<header className="topbar topbar-two">
<div className="topbar-row topbar-row1">
<div className="brand">
<div className="brand-mark" style={{ background: accent }}>S</div>
<div className="brand-name">SoftLAN <span className="brand-name-soft">Launcher</span></div>
</div>
<DirectoryButton path={DIR_PATH}/>
<div className="topbar-row1-right">
<StorageMeter accent={accent}/>
<KebabMenu items={menuItems}/>
</div>
</div>
<div className="topbar-row topbar-row2">
<UnderlineFilters value={filter} onChange={setFilter} counts={counts} accent={accent}/>
<div className="topbar-row2-right">
<SearchField value={query} onChange={setQuery} accent={accent} wide/>
<SortMenu value={sort} onChange={setSort} accent={accent}/>
</div>
</div>
</header>
)}
<main className="grid-wrap">
{variant === 'single' && (
<div className="results-bar">
<div className="results-count">
Showing <strong>{list.length}</strong> of {counts.all} games
</div>
<StorageMeter accent={accent} compact/>
</div>
)}
<div className="grid">
{list.map(g => (
<GameCard key={g.id} game={g} accent={accent} aspect={aspect}
onOpen={(game) => setOpenGame(game)}/>
))}
</div>
</main>
{openGame && <GameDetailModal game={openGame} accent={accent} onClose={() => setOpenGame(null)}/>}
{settingsOpen && setTweak && (
<SettingsDialog settings={tweaks} onChange={setTweak} onClose={() => setSettingsOpen(false)}/>
)}
</div>
);
}
window.Launcher = Launcher;