Rebuild the web UI on Bootstrap 5 with the dark felt theme
CI / Build and push docker image (push) Failing after 39s

The lobby was hand-rolled CSS and looked plain. Vendor Bootstrap 5.3
(MIT) under web/vendor so builds stay offline, load it in dark mode and
bridge the felt/gold palette onto Bootstrap's CSS variables in a new
theme.css; style.css now only owns the game table, cards and scoring
summary.

Rebuild the lobby, waiting room and shared chrome with Bootstrap
components: a shared navbar, creation form as cards with schema-driven
controls, roster as a list group with seat/team badges, a seats progress
bar and a copy-to-clipboard button on the join code. History,
leaderboard, connection/error states and the 404 page use cards, tables
and buttons too; the play table itself is unchanged.

The copy feedback timer is cancelled when the waiting room is rebuilt by
another player joining, fixing a disposed-signal panic that froze the
page mid-match.
This commit is contained in:
2026-09-21 23:40:44 +08:00
parent ed8a004ec8
commit 12fa3f5aae
15 changed files with 774 additions and 486 deletions
+1
View File
@@ -1,3 +1,4 @@
pub mod card;
pub mod nav;
pub mod summary;
pub mod toast;
+38
View File
@@ -0,0 +1,38 @@
//! Shared Bootstrap navbar used by the lobby, history and leaderboard pages.
use sycamore::prelude::*;
fn nav_link(href: &'static str, label: &'static str, active: &str) -> View {
let class = if href == active {
"nav-link active"
} else {
"nav-link"
};
let aria = if href == active { "page" } else { "" };
view! {
li(class="nav-item") {
a(class=class, href=href, aria-current=aria) { (label) }
}
}
}
/// Renders the app navbar. `active` is the current route (`/`, `/history`,
/// `/leaderboard`); `whoami` builds the right-aligned slot for the
/// signed-in user and the log-out action.
pub fn app_nav(active: &'static str, whoami: impl Fn() -> View + 'static) -> View {
view! {
nav(class="navbar navbar-expand flex-wrap") {
div(class="container-fluid") {
a(class="navbar-brand", href="/") {
span(class="text-warning") { "♠" }
" tavolo"
}
ul(class="navbar-nav me-auto") {
(nav_link("/", "Lobby", active))
(nav_link("/history", "My matches", active))
(nav_link("/leaderboard", "Leaderboard", active))
}
(move || whoami())
}
}
}
}
+3 -3
View File
@@ -246,7 +246,7 @@ pub fn hand_summary_modal(
// whole modal (which would swap DOM nodes under the user's cursor).
let deadline_ms = js_sys::Date::parse(deadline);
view! {
p(class="hint") {
p(class="text-secondary small") {
"Auto-continuing in "
(move || {
((deadline_ms - now.get_clone()) / 1000.0).ceil().max(0.0) as i32
@@ -263,11 +263,11 @@ pub fn hand_summary_modal(
let action = if already_acked {
let waiting_text = format!("Waiting for {}…", waiting.join(", "));
view! {
button(class="button primary", disabled=true) { (waiting_text) }
button(class="btn btn-warning", disabled=true) { (waiting_text) }
}
} else {
view! {
button(class="button primary", on:click=move |_| {
button(class="btn btn-warning", on:click=move |_| {
if let Some(s) = socket.get_clone() {
s.ack();
}
+1 -1
View File
@@ -15,6 +15,6 @@ pub fn toast(error: Signal<Option<String>>) -> View {
}
});
view! {
(move || error.get_clone().map(|e| view! { div(class="toast") { (e) } }))
(move || error.get_clone().map(|e| view! { div(class="app-toast") { (e) } }))
}
}