feat(engine): Phase B home hierarchy — Continue card, hero New Game, deal-options disclosure
Test / test (pull_request) Failing after 10s
Test / test (pull_request) Failing after 10s
Restructures the Home overlay per docs/ui-redesign-2026-07.md Phase B: - Continue card (mode / elapsed / score) when a game is in progress; clicking returns to the table - Hero New Game replays the persisted Settings::last_mode (new field, serde-default Classic) with one tap; locked modes fall back to Classic - Deal options (draw 1/3, winnable-only, difficulty tiers) move into a disclosure under the hero — off the Home top level (decision 2) - Compact symmetric 2x3 mode grid; descriptions on wide viewports only - Stats strip moves to the bottom (right pane on wide) - Two-pane body on wide viewports (>= 1000 logical px) via new ui_modal::spawn_modal_sized (default card stays 720 px) - Cancel becomes "Back to table" and only renders while a live (un-won) game exists 10 new home_plugin tests; all 936 engine tests + clippy green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
# Menu UX Redesign — July 2026
|
# Menu UX Redesign — July 2026
|
||||||
|
|
||||||
Status: PLANNING. Visual identity (Terminal / base16-eighties) is settled and
|
Status: IN PROGRESS. A / E / C / G shipped in v0.43.0; B implemented on
|
||||||
out of scope — this is about **structure and interaction**, not colors or type.
|
`feat/home-hierarchy` (2026-07-13); F / H and the I–M backlog remain open.
|
||||||
|
Visual identity (Terminal / base16-eighties) is settled and out of
|
||||||
|
scope — this is about **structure and interaction**, not colors or type.
|
||||||
|
|
||||||
## Diagnosis (from code survey, 2026-07-07)
|
## Diagnosis (from code survey, 2026-07-07)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ use std::io;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use solitaire_core::{DrawStockConfig, game_state::DifficultyLevel};
|
use solitaire_core::{
|
||||||
|
DrawStockConfig,
|
||||||
|
game_state::{DifficultyLevel, GameMode},
|
||||||
|
};
|
||||||
|
|
||||||
const SETTINGS_FILE_NAME: &str = "settings.json";
|
const SETTINGS_FILE_NAME: &str = "settings.json";
|
||||||
|
|
||||||
@@ -248,6 +251,12 @@ pub struct Settings {
|
|||||||
/// cleanly to `None` via `#[serde(default)]`.
|
/// cleanly to `None` via `#[serde(default)]`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub last_difficulty: Option<DifficultyLevel>,
|
pub last_difficulty: Option<DifficultyLevel>,
|
||||||
|
/// Mode of the last game the player launched from the home overlay.
|
||||||
|
/// The home hero "New Game" button replays this mode with one tap.
|
||||||
|
/// Older `settings.json` files written before this field existed
|
||||||
|
/// deserialize cleanly to `GameMode::Classic` via `#[serde(default)]`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub last_mode: GameMode,
|
||||||
/// Custom public name displayed on the leaderboard. When `None`, the
|
/// Custom public name displayed on the leaderboard. When `None`, the
|
||||||
/// player's server `username` is used instead. Trimmed to 32 characters
|
/// player's server `username` is used instead. Trimmed to 32 characters
|
||||||
/// before submission. Older `settings.json` files written before this
|
/// before submission. Older `settings.json` files written before this
|
||||||
@@ -415,6 +424,7 @@ impl Default for Settings {
|
|||||||
disable_smart_default_size: false,
|
disable_smart_default_size: false,
|
||||||
replay_move_interval_secs: default_replay_move_interval_secs(),
|
replay_move_interval_secs: default_replay_move_interval_secs(),
|
||||||
last_difficulty: None,
|
last_difficulty: None,
|
||||||
|
last_mode: GameMode::Classic,
|
||||||
leaderboard_display_name: None,
|
leaderboard_display_name: None,
|
||||||
leaderboard_opted_in: false,
|
leaderboard_opted_in: false,
|
||||||
take_from_foundation: true,
|
take_from_foundation: true,
|
||||||
|
|||||||
+1105
-307
File diff suppressed because it is too large
Load Diff
@@ -178,6 +178,26 @@ pub fn spawn_modal<M: Component, F>(
|
|||||||
z_panel: i32,
|
z_panel: i32,
|
||||||
build_card: F,
|
build_card: F,
|
||||||
) -> Entity
|
) -> Entity
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut ChildSpawnerCommands),
|
||||||
|
{
|
||||||
|
spawn_modal_sized(commands, plugin_marker, z_panel, MODAL_CARD_MAX_WIDTH, build_card)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default maximum card width in logical pixels — every [`spawn_modal`]
|
||||||
|
/// caller gets this. Surfaces that lay out side-by-side panes (e.g. the
|
||||||
|
/// two-pane Home on wide viewports) can raise it via [`spawn_modal_sized`].
|
||||||
|
pub const MODAL_CARD_MAX_WIDTH: f32 = 720.0;
|
||||||
|
|
||||||
|
/// [`spawn_modal`] with an explicit card `max_width`. Behaviour is
|
||||||
|
/// otherwise identical — same scrim, enter animation, and card chrome.
|
||||||
|
pub fn spawn_modal_sized<M: Component, F>(
|
||||||
|
commands: &mut Commands,
|
||||||
|
plugin_marker: M,
|
||||||
|
z_panel: i32,
|
||||||
|
max_width: f32,
|
||||||
|
build_card: F,
|
||||||
|
) -> Entity
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut ChildSpawnerCommands),
|
F: FnOnce(&mut ChildSpawnerCommands),
|
||||||
{
|
{
|
||||||
@@ -235,7 +255,7 @@ where
|
|||||||
padding: UiRect::all(VAL_SPACE_5),
|
padding: UiRect::all(VAL_SPACE_5),
|
||||||
border: UiRect::all(Val::Px(1.0)),
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
border_radius: BorderRadius::all(Val::Px(RADIUS_LG)),
|
border_radius: BorderRadius::all(Val::Px(RADIUS_LG)),
|
||||||
max_width: Val::Px(720.0),
|
max_width: Val::Px(max_width),
|
||||||
align_items: AlignItems::Stretch,
|
align_items: AlignItems::Stretch,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user