polish(engine): standard empty/loading/error states (Phase L)
Test / fmt (pull_request) Successful in 5s
Test / test (pull_request) Successful in 4m20s

New ui_modal::spawn_empty_state — glyph + headline + optional detail,
centred, with next-step actions composed alongside rather than
configured in. Glyphs restricted to FiraMono-covered ranges (suits,
arrows, ASCII) per the section-10/11 Android constraint.

Swept every improvised surface onto it:
- Theme store: loading / error / empty-catalog branches
- Leaderboard: fetching / error / be-the-first branches
- You-hub Replays tab: dedicated no-replays state (selector and
  actions no longer spawn dead controls when history is empty)
- Stats: first-launch nudge above the em-dash grid

The Account tab's sync row was deliberately NOT swept: it is a live
status + action row, not an empty state, and its deeper treatment
belongs to Phase M's sync-transparency work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-13 20:03:09 -07:00
parent d4d0bde0c0
commit 02bcc8b4af
4 changed files with 207 additions and 67 deletions
+23 -17
View File
@@ -30,7 +30,7 @@ use crate::ui_modal::{ButtonVariant, ModalButton, spawn_modal_button};
use crate::ui_theme::{
ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBorder, RADIUS_SM, STATE_INFO,
STATE_WARNING, STREAK_MILESTONES, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4,
TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4,
};
/// Bevy resource wrapping the current stats.
@@ -719,25 +719,17 @@ pub(crate) fn spawn_stats_body(
},
))
.with_children(|body| {
// First-launch caption — sits above the grid as gentle nudge so
// First-launch empty state (Phase L) — sits above the grid so
// the wall of em-dashes reads as "nothing to track yet" rather
// than as broken state.
if is_first_launch {
body.spawn((
Text::new("Play a game to start tracking stats."),
TextFont {
font_size: TYPE_CAPTION,
..default()
},
TextColor(TEXT_SECONDARY),
Node {
margin: UiRect {
bottom: VAL_SPACE_2,
..default()
},
..default()
},
));
crate::ui_modal::spawn_empty_state(
body,
"\u{2663}",
"Play a game to start tracking stats.",
None,
font_res,
);
}
// --- primary stat cells grid ---
@@ -893,6 +885,20 @@ pub(crate) fn spawn_replays_body(
selected_index: usize,
font_res: Option<&FontResource>,
) {
// Standard empty state (Phase L) — the selector / Watch / Copy
// controls only spawn when there is something to select, so their
// handlers all no-op via their existing empty-query paths.
if replays.is_empty() {
crate::ui_modal::spawn_empty_state(
card,
"\u{2666}",
"No replays yet.",
Some("Every win records a replay here automatically."),
font_res,
);
return;
}
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
let font_row = TextFont {
font: font_handle,