6e28f736e8
The single-row top bar was a flat flex row, which made the search field drift left or right depending on how wide the surrounding clusters were. Rework it as a 3-column CSS grid (left zone, search, right zone) so the search input lands at the geometric center of the window regardless of the side-zone widths. Mechanics: - `.topbar-single` becomes `display: grid` with `grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr)` and a 16 px column gap. The middle (auto) column holds only the search, which is capped at `flex: 0 1 360px` so it cannot push into the side columns. - The left and right zones are flex containers with `justify-content: space-between`, so brand pins far-left while filter pills hug the search, and sort hugs the search while the kebab pins far-right. The filter pills are now grouped semantically with the search (they scope it) instead of floating next to the brand. - A `@container launcher (max-width: 1100px)` rule collapses the layout back to a single nowrap flex row at narrow widths — the geometric centering stops reading at narrow widths and would otherwise force awkward truncation, so we abandon it rather than fight it. - The `.launcher` root opts into container queries via `container-type: inline-size; container-name: launcher`. `launcher.jsx` now wraps the existing children in `.topbar-left`, `.topbar-center`, `.topbar-right` (plus a `.topbar-left-trail` / `.topbar-right-lead` for the inner space-between alignment), but each control component is otherwise untouched. The README's "Top bar" section is rewritten to spec the new layout, and a new "Changes since v2" section calls it out at the top of the handoff. The game-directory button line is left as-is in this commit and addressed separately. Test Plan - Open `design_reference/SoftLAN Launcher.html` in a static server and inspect variant A at full width: the search input's horizontal center should match the window's horizontal center, regardless of accent / density choices in the Tweaks panel. - Shrink the launcher artboard below 1100 px and confirm the row collapses to a single left-to-right strip with no overlap.
123 lines
4.9 KiB
React
123 lines
4.9 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, gameFolderSet } = 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="topbar-left">
|
|
<div className="brand">
|
|
<div className="brand-mark" style={{ background: accent }}>S</div>
|
|
<div className="brand-name">SoftLAN</div>
|
|
</div>
|
|
<div className="topbar-left-trail">
|
|
<SegmentedFilters value={filter} onChange={setFilter} counts={counts} accent={accent}/>
|
|
</div>
|
|
</div>
|
|
<div className="topbar-center">
|
|
<SearchField value={query} onChange={setQuery} accent={accent} wide/>
|
|
</div>
|
|
<div className="topbar-right">
|
|
<div className="topbar-right-lead">
|
|
<SortMenu value={sort} onChange={setSort} accent={accent}/>
|
|
</div>
|
|
<DirectoryButton path={gameFolderSet === false ? null : DIR_PATH}/>
|
|
<KebabMenu items={menuItems}/>
|
|
</div>
|
|
</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={gameFolderSet === false ? null : 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;
|