feat(ui): redesign game-folder button as icon + label + dot
Implements the v2 design's game-folder button in the Tauri launcher. The previous control squeezed the full game-directory path into the top-bar button as truncated monospace (e.g. `…s/Desktop/eti_games_AFTER_LAN_2025`). In practice the leading ellipsis rarely showed the meaningful part of the path, ate horizontal space the new 3-zone top bar needs for its primary controls, and competed with the filter / search / sort cluster for attention. The button now communicates the *state* of the configuration at a glance — an icon + short label + colored status dot — while the full path moves into the native tooltip and `aria-label`, where it stays one mouseover (and screen-reader friendly) away. Two visual states, both 36 px tall and sharing the surface of the other top-bar controls: - **Set & valid** (`.dirbtn-set`) — label `Game folder`, green dot (`--ok`) with a soft glow, default border, tooltip = full path. - **Not set / invalid** (`.dirbtn-unset`) — 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`. `DirectoryButton` now takes `path: string | null` and picks the state from `!!(path && path.trim())`. The `aria-label` carries the full state in words (`Game folder: <path>` / `Set game folder`) so screen readers don't have to interpret the colored dot. Note: in the current `MainWindow`, `gameDir` is gated upstream — if no directory is selected, `NoDirectoryState` is shown instead of the top bar, so the unset state will only surface here if we later validate disk existence and clear `gameDir`. The button accepts a nullable path anyway, so it's ready when that check lands. `truncatePath` in `lib/format.ts` was the only caller-less helper left behind and is removed. Test Plan - `npx tsc --noEmit` from the frontend crate — clean. - `just frontend-test` — passes. - Manual: `just run`, pick a valid game directory, and confirm the top-bar button reads `Game folder` with a green dot; hovering it reveals the full path in the OS tooltip. Inspect the DOM and confirm `aria-label` reads `Game folder: <path>`. (The unset variant currently isn't surfaced by `MainWindow`; eyeball it via DevTools by toggling the `dirbtn-set` class to `dirbtn-unset`.)
This commit is contained in:
@@ -1,17 +1,27 @@
|
|||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { truncatePath } from '../../lib/format';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string | null;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DirectoryButton = ({ path, onClick }: Props) => (
|
export const DirectoryButton = ({ path, onClick }: Props) => {
|
||||||
<button className="dirbtn" type="button" title={path || 'Choose a game directory'} onClick={onClick}>
|
const isSet = !!(path && path.trim());
|
||||||
|
const label = isSet ? 'Game folder' : 'Set game folder';
|
||||||
|
const tooltip = isSet ? (path as string) : 'Please select a game folder';
|
||||||
|
const ariaLabel = isSet ? `Game folder: ${path}` : 'Set game folder';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dirbtn ${isSet ? 'dirbtn-set' : 'dirbtn-unset'}`}
|
||||||
|
title={tooltip}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<Icon.folder />
|
<Icon.folder />
|
||||||
<span className="dirbtn-label">Game directory</span>
|
<span className="dirbtn-label">{label}</span>
|
||||||
<span className="dirbtn-path">
|
<span className="dirbtn-status-dot" aria-hidden="true" />
|
||||||
{path ? truncatePath(path) : 'choose…'}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ export const formatEtiVersion = (raw: string | undefined): string => {
|
|||||||
return raw;
|
return raw;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Truncate a path with a leading ellipsis when it exceeds the limit. */
|
|
||||||
export const truncatePath = (path: string, max = 36): string =>
|
|
||||||
path.length > max ? `…${path.slice(-(max - 1))}` : path;
|
|
||||||
|
|
||||||
export const formatPlayers = (max?: number): string => {
|
export const formatPlayers = (max?: number): string => {
|
||||||
if (!max || max <= 0) return '—';
|
if (!max || max <= 0) return '—';
|
||||||
return max === 1 ? '1' : `1–${max}`;
|
return max === 1 ? '1' : `1–${max}`;
|
||||||
|
|||||||
@@ -384,44 +384,57 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Directory button */
|
/* Game-folder button: icon + short label + status dot.
|
||||||
|
Full path lives in tooltip + aria-label, not on-screen. */
|
||||||
.dirbtn {
|
.dirbtn {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0 12px;
|
padding: 0 14px 0 12px;
|
||||||
background: var(--bg-2);
|
background: var(--bg-2);
|
||||||
border: 1px solid var(--bd-1);
|
border: 1px solid var(--bd-1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--t-2);
|
color: var(--t-1);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 360px;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.15s,
|
border-color 0.15s,
|
||||||
color 0.15s;
|
color 0.15s,
|
||||||
flex-shrink: 1;
|
background 0.15s;
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
.dirbtn:hover {
|
.dirbtn:hover {
|
||||||
border-color: var(--bd-2);
|
border-color: var(--bd-2);
|
||||||
color: var(--t-1);
|
|
||||||
}
|
}
|
||||||
.dirbtn-label {
|
.dirbtn-label {
|
||||||
color: var(--t-1);
|
line-height: 1;
|
||||||
font-weight: 600;
|
}
|
||||||
|
.dirbtn-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-left: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.dirbtn-path {
|
.dirbtn-set .dirbtn-status-dot {
|
||||||
color: var(--t-3);
|
background: var(--ok);
|
||||||
font-family: var(--font-mono);
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ok) 70%, transparent);
|
||||||
font-size: 11.5px;
|
}
|
||||||
white-space: nowrap;
|
.dirbtn-unset .dirbtn-status-dot {
|
||||||
overflow: hidden;
|
background: var(--danger);
|
||||||
text-overflow: ellipsis;
|
box-shadow: 0 0 6px color-mix(in srgb, var(--danger) 70%, transparent);
|
||||||
min-width: 0;
|
}
|
||||||
|
.dirbtn-unset {
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 35%, var(--bd-1));
|
||||||
|
}
|
||||||
|
.dirbtn-unset:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 55%, var(--bd-2));
|
||||||
|
background: color-mix(in srgb, var(--danger) 8%, var(--bg-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kebab menu */
|
/* Kebab menu */
|
||||||
|
|||||||
Reference in New Issue
Block a user