feat(ui): move game folder picker into settings

The design update moved game-folder configuration out of launcher chrome and
into Settings > Library. Follow that contract in the runtime UI without
changing the existing storage or Tauri directory commands.

The top bar now leaves its right edge for the kebab menu. Settings owns a new
Game folder row that shows a valid selected path with a neutral Change button,
or the red Not set state with a stronger Choose button when no accessible
directory is configured. Both the empty-library state and the Settings row
still use the existing native directory picker, so existing saved paths and
rescans keep their current behavior.

Keep useGameDirectory as the directory-state owner and expose the shared
hasGameDirectory boolean from that hook so the grid and Settings field agree on
what counts as configured.

Test Plan:
- git diff --cached --check
- just frontend-test
- just build

Refs: 62b409f4bfc4995c25461776107d28f52b24f30e
This commit is contained in:
2026-05-21 20:42:12 +02:00
parent eedfc0105d
commit 2e7a0cff2f
6 changed files with 128 additions and 82 deletions
@@ -14,6 +14,9 @@ import {
interface Props {
settings: UISettings;
gameDir: string;
hasGameDirectory: boolean;
onPickDirectory: () => void;
onChange: <K extends keyof UISettings>(key: K, value: UISettings[K]) => void;
onClose: () => void;
}
@@ -56,7 +59,37 @@ const SettingsTextInput = ({ value, placeholder, maxLength, onChange }: TextInpu
</div>
);
export const SettingsDialog = ({ settings, onChange, onClose }: Props) => (
interface GameFolderFieldProps {
path: string;
isValid: boolean;
onPickDirectory: () => void;
}
const GameFolderField = ({ path, isValid, onPickDirectory }: GameFolderFieldProps) => (
<div className={`folder-field ${isValid ? 'is-set' : 'is-unset'}`}>
<Icon.folder className="folder-field-icon" aria-hidden="true" />
<div className="folder-field-path" title={isValid ? path : 'No folder selected'}>
{isValid ? <bdi>{path}</bdi> : <span className="folder-field-empty">Not set</span>}
</div>
<button
type="button"
className="folder-field-btn"
aria-label={isValid ? 'Change game folder' : 'Choose game folder'}
onClick={onPickDirectory}
>
{isValid ? 'Change…' : 'Choose…'}
</button>
</div>
);
export const SettingsDialog = ({
settings,
gameDir,
hasGameDirectory,
onPickDirectory,
onChange,
onClose,
}: Props) => (
<Modal onClose={onClose} className="settings-modal">
<div className="settings-head">
<h2>Settings</h2>
@@ -110,6 +143,13 @@ export const SettingsDialog = ({ settings, onChange, onClose }: Props) => (
<section className="settings-section">
<div className="settings-section-title">Library</div>
<Row label="Game folder" hint="Parent directory where games are downloaded and installed">
<GameFolderField
path={gameDir}
isValid={hasGameDirectory}
onPickDirectory={onPickDirectory}
/>
</Row>
<Row label="Grid density" hint="How tightly cards are packed">
<SegmentedRadio
value={settings.density}
@@ -1,28 +0,0 @@
import { Icon } from '../Icon';
interface Props {
path: string | null;
exists: boolean;
onClick: () => void;
}
export const DirectoryButton = ({ path, exists, onClick }: Props) => {
const isSet = !!(path && path.trim());
const isValid = isSet && exists;
const label = isValid ? 'Game folder' : 'Set game folder';
const tooltip = isValid ? (path as string) : 'Please select a game folder';
const ariaLabel = isValid ? `Game folder: ${path}` : 'Set game folder';
return (
<button
type="button"
className={`dirbtn ${isValid ? 'dirbtn-set' : 'dirbtn-unset'}`}
title={tooltip}
aria-label={ariaLabel}
onClick={onClick}
>
<Icon.folder />
<span className="dirbtn-label">{label}</span>
</button>
);
};
@@ -2,7 +2,6 @@ import { Brand } from '../Brand';
import { SegmentedFilters } from './SegmentedFilters';
import { SearchField } from './SearchField';
import { SortMenu } from './SortMenu';
import { DirectoryButton } from './DirectoryButton';
import { KebabMenu, KebabItem } from './KebabMenu';
import { FilterCounts } from '../../lib/gameState';
@@ -17,9 +16,6 @@ interface Props {
setQuery: (value: string) => void;
sort: GameSort;
setSort: (value: GameSort) => void;
gameDir: string;
gameDirExists: boolean;
onPickDirectory: () => void;
kebabItems: ReadonlyArray<KebabItem>;
}
@@ -32,9 +28,6 @@ export const TopBar = ({
setQuery,
sort,
setSort,
gameDir,
gameDirExists,
onPickDirectory,
kebabItems,
}: Props) => (
<header className="topbar">
@@ -52,7 +45,6 @@ export const TopBar = ({
<SortMenu value={sort} onChange={setSort} />
</div>
<div className="topbar-right-tail">
<DirectoryButton path={gameDir} exists={gameDirExists} onClick={onPickDirectory} />
<KebabMenu items={kebabItems} />
</div>
</div>