polish(engine): standard empty/loading/error states (Phase L)
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:
@@ -24,13 +24,13 @@ use crate::font_plugin::FontResource;
|
||||
use crate::settings_plugin::{SettingsResource, SettingsStoragePath};
|
||||
use crate::sync_plugin::SyncProviderResource;
|
||||
use crate::ui_modal::{
|
||||
ButtonVariant, ModalScrim, ScrimDismissible, spawn_modal, spawn_modal_actions,
|
||||
spawn_modal_button, spawn_modal_header,
|
||||
ButtonVariant, ModalScrim, ScrimDismissible, spawn_empty_state, spawn_modal,
|
||||
spawn_modal_actions, spawn_modal_button, spawn_modal_header,
|
||||
};
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, BG_ELEVATED, BORDER_SUBTLE, RADIUS_SM, STATE_INFO, TEXT_DISABLED, TEXT_PRIMARY,
|
||||
TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4,
|
||||
Z_MODAL_PANEL, Z_PAUSE_DIALOG,
|
||||
ACCENT_PRIMARY, BG_ELEVATED, BORDER_SUBTLE, RADIUS_SM, TEXT_DISABLED, TEXT_PRIMARY,
|
||||
TEXT_SECONDARY, TYPE_BODY, TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4, Z_MODAL_PANEL,
|
||||
Z_PAUSE_DIALOG,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -536,11 +536,6 @@ fn spawn_leaderboard_screen(
|
||||
font_size: TYPE_CAPTION,
|
||||
..default()
|
||||
};
|
||||
let font_status = TextFont {
|
||||
font: font_handle.clone(),
|
||||
font_size: TYPE_BODY_LG,
|
||||
..default()
|
||||
};
|
||||
let font_row = TextFont {
|
||||
font: font_handle.clone(),
|
||||
font_size: TYPE_BODY,
|
||||
@@ -647,30 +642,31 @@ fn spawn_leaderboard_screen(
|
||||
.with_children(|body| {
|
||||
match data {
|
||||
LeaderboardResource::Idle => {
|
||||
body.spawn((
|
||||
Text::new("Fetching\u{2026}"),
|
||||
font_status.clone(),
|
||||
TextColor(STATE_INFO),
|
||||
));
|
||||
spawn_empty_state(
|
||||
body,
|
||||
"\u{21BB}",
|
||||
"Fetching the leaderboard\u{2026}",
|
||||
None,
|
||||
font_res,
|
||||
);
|
||||
}
|
||||
LeaderboardResource::Error(_) => {
|
||||
body.spawn((
|
||||
Text::new("Couldn't reach the leaderboard. Try again later."),
|
||||
font_status.clone(),
|
||||
TextColor(TEXT_SECONDARY),
|
||||
));
|
||||
spawn_empty_state(
|
||||
body,
|
||||
"!",
|
||||
"Couldn't reach the leaderboard.",
|
||||
Some("Try again later."),
|
||||
font_res,
|
||||
);
|
||||
}
|
||||
LeaderboardResource::Loaded(rows) if rows.is_empty() => {
|
||||
body.spawn((
|
||||
Text::new("Be the first on the leaderboard."),
|
||||
font_status.clone(),
|
||||
TextColor(TEXT_PRIMARY),
|
||||
));
|
||||
body.spawn((
|
||||
Text::new("Win a game and opt in to appear here."),
|
||||
font_row.clone(),
|
||||
TextColor(TEXT_SECONDARY),
|
||||
));
|
||||
spawn_empty_state(
|
||||
body,
|
||||
"\u{2660}",
|
||||
"Be the first on the leaderboard.",
|
||||
Some("Win a game and opt in to appear here."),
|
||||
font_res,
|
||||
);
|
||||
}
|
||||
LeaderboardResource::Loaded(rows) => {
|
||||
// Column headers
|
||||
|
||||
Reference in New Issue
Block a user