From 059c1e77205d70f5a8c34dc2e34440caa7f99d2a Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 18:46:49 +0200 Subject: [PATCH] feat(ui): redesign game-folder button as icon + label + dot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: ` / `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: `. (The unset variant currently isn't surfaced by `MainWindow`; eyeball it via DevTools by toggling the `dirbtn-set` class to `dirbtn-unset`.) --- .../src/components/topbar/DirectoryButton.tsx | 32 +++++++----- .../lanspread-tauri-deno-ts/src/lib/format.ts | 4 -- .../src/styles/launcher.css | 49 ++++++++++++------- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/crates/lanspread-tauri-deno-ts/src/components/topbar/DirectoryButton.tsx b/crates/lanspread-tauri-deno-ts/src/components/topbar/DirectoryButton.tsx index f56978b..eedbf74 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/topbar/DirectoryButton.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/topbar/DirectoryButton.tsx @@ -1,17 +1,27 @@ import { Icon } from '../Icon'; -import { truncatePath } from '../../lib/format'; interface Props { - path: string; + path: string | null; onClick: () => void; } -export const DirectoryButton = ({ path, onClick }: Props) => ( - -); +export const DirectoryButton = ({ path, onClick }: Props) => { + 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 ( + + ); +}; diff --git a/crates/lanspread-tauri-deno-ts/src/lib/format.ts b/crates/lanspread-tauri-deno-ts/src/lib/format.ts index 63587cb..6b8e46d 100644 --- a/crates/lanspread-tauri-deno-ts/src/lib/format.ts +++ b/crates/lanspread-tauri-deno-ts/src/lib/format.ts @@ -19,10 +19,6 @@ export const formatEtiVersion = (raw: string | undefined): string => { 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 => { if (!max || max <= 0) return '—'; return max === 1 ? '1' : `1–${max}`; diff --git a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css index 616ef49..242c9ea 100644 --- a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css +++ b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css @@ -384,44 +384,57 @@ color: var(--accent); } -/* Directory button */ +/* Game-folder button: icon + short label + status dot. + Full path lives in tooltip + aria-label, not on-screen. */ .dirbtn { + position: relative; display: inline-flex; align-items: center; gap: 8px; height: 36px; - padding: 0 12px; + padding: 0 14px 0 12px; background: var(--bg-2); border: 1px solid var(--bd-1); border-radius: 8px; - color: var(--t-2); + color: var(--t-1); font: inherit; font-size: 12.5px; + font-weight: 600; cursor: pointer; - max-width: 360px; + white-space: nowrap; + flex-shrink: 0; transition: border-color 0.15s, - color 0.15s; - flex-shrink: 1; - min-width: 0; + color 0.15s, + background 0.15s; } .dirbtn:hover { border-color: var(--bd-2); - color: var(--t-1); } .dirbtn-label { - color: var(--t-1); - font-weight: 600; + line-height: 1; +} +.dirbtn-status-dot { + width: 8px; + height: 8px; + margin-left: 2px; + border-radius: 999px; flex-shrink: 0; } -.dirbtn-path { - color: var(--t-3); - font-family: var(--font-mono); - font-size: 11.5px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; +.dirbtn-set .dirbtn-status-dot { + background: var(--ok); + box-shadow: 0 0 6px color-mix(in srgb, var(--ok) 70%, transparent); +} +.dirbtn-unset .dirbtn-status-dot { + background: var(--danger); + box-shadow: 0 0 6px color-mix(in srgb, var(--danger) 70%, transparent); +} +.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 */