// ── Board ────────────────────────────────────────────────────────
impl IntoView {
})
}}
- // ── Scoring detail panels — expand downward in the strip ──────
- {my_scored_event.map(|event| view! {
-
- })}
- {opp_scored_event.map(|event| view! {
-
- })}
// ── Pre-game ceremony overlay ─────────────────────────────────────
diff --git a/clients/web/src/game/components/scoring.rs b/clients/web/src/game/components/scoring.rs
index 69e1ca1..79fd433 100644
--- a/clients/web/src/game/components/scoring.rs
+++ b/clients/web/src/game/components/scoring.rs
@@ -2,15 +2,18 @@ use futures::channel::mpsc::UnboundedSender;
#[cfg(target_arch = "wasm32")]
use gloo_timers::future::TimeoutFuture;
use leptos::prelude::*;
+#[cfg(target_arch = "wasm32")]
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc,
+};
use trictrac_store::CheckerMove;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
-#[cfg(target_arch = "wasm32")]
-use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use crate::app::NetCommand;
-use crate::i18n::*;
use crate::game::trictrac::types::{JanEntry, PlayerAction, ScoredEvent, SerTurnStage};
+use crate::i18n::*;
use super::score_panel::jan_label;
@@ -48,14 +51,19 @@ fn scoring_jan_row(entry: JanEntry) -> impl IntoView {
}
}
+/// Scoring detail panel, shown to the right of the hole counter in the merged
+/// score panel area.
+///
+/// Lifecycle:
+/// 1. Mounts expanded — shows all jan details and draws board arrows.
+/// 2. After 3.4 s the arrows clear and the panel auto-minimises to a small "+"
+/// button (unless Hold/Go buttons are still needed).
+/// 3. The "+" / "−" buttons let the player toggle between states at any time.
#[component]
pub fn ScoringPanel(
event: ScoredEvent,
turn_stage: SerTurnStage,
#[prop(default = false)] is_opponent: bool,
- /// When true the panel renders inline in the bottom strip instead of as
- /// a right-side overlay: no slide-in animation, no peek/reveal behaviour.
- #[prop(default = false)] inline: bool,
) -> impl IntoView {
let i18n = use_i18n();
let cmd_tx = use_context::>()
@@ -72,91 +80,28 @@ pub fn ScoringPanel(
"scoring-panel"
};
- // ── Lifecycle signals ──────────────────────────────────────────────────
- // peeked: added after 3.4 s (slide to peek strip)
- // revealed: added on first hover of the peek strip (stay open permanently)
- let peeked = RwSignal::new(false);
- let revealed = RwSignal::new(false);
+ // minimized: starts false (expanded), becomes true after 3.4 s unless
+ // the Hold/Go choice still needs the player's attention.
+ let minimized = RwSignal::new(false);
- // ── Collect all moves from all jans for automatic arrow display ────────
+ // Collect all moves from all jans for automatic arrow display.
let all_moves: Vec<(CheckerMove, CheckerMove)> = event
.jans
.iter()
.flat_map(|e| e.moves.iter().cloned())
.collect();
- let all_moves_click = all_moves.clone();
- let all_moves_enter = all_moves.clone();
+ let all_moves_auto = all_moves.clone();
+ let all_moves_expand = all_moves.clone();
+ let all_moves_enter = all_moves.clone();
let hovered_ctx = use_context::>>();
-
- // On mount: show all this event's moves as board arrows immediately,
- // then after 3.4 s slide to peek and clear the arrows.
- //
- // Two important constraints:
- // 1. The initial hm.set() must be deferred (spawn_local, not sync in body)
- // to avoid writing a reactive signal mid-render while Board reads it —
- // that triggers Leptos's cycle guard → `unreachable` WASM panic.
- // 2. The cancellation flag must be Rc>, NOT RwSignal.
- // RwSignal is a NodeId into Leptos's arena; the arena slot is freed
- // when ScoringPanel's owner drops (on every GameScreen remount). If the
- // 3.4 s future outlives the component and calls is_alive.get_untracked()
- // on a freed slot, that also panics with `unreachable`. Rc>
- // is reference-counted outside the arena and stays valid for as long as
- // the future holds onto it.
- #[cfg(target_arch = "wasm32")]
- if let Some(hm) = hovered_ctx {
- let is_alive = Arc::new(AtomicBool::new(true));
- let is_alive_cleanup = is_alive.clone();
- // on_cleanup requires Send + Sync; Arc satisfies both.
- on_cleanup(move || is_alive_cleanup.store(false, Ordering::Relaxed));
-
- spawn_local(async move {
- // Show arrows (runs in the next microtask, after render settles).
- hm.set(all_moves);
-
- TimeoutFuture::new(3_400).await;
- // Guard: component may have been destroyed while we were waiting.
- // is_alive was set to false by on_cleanup, which runs before Leptos
- // frees the signal arena slots — so peeked is still valid iff this
- // returns true.
- if !is_alive.load(Ordering::Relaxed) {
- return;
- }
- hm.set(vec![]);
- // Inline panels are always visible; only the overlay variant peeks.
- if !inline {
- peeked.set(true);
- }
- });
- }
-
let jan_rows: Vec<_> = event.jans.into_iter().map(scoring_jan_row).collect();
view! {
- // ── Outer wrapper: overlay mode has slide/peek/reveal animation;
- // inline mode suppresses all of that via scoring-panel-inline-wrap.
+ // "+" expand button — shown only when minimised (CSS hides it otherwise).
+
+
+ // Full panel — hidden when minimised via CSS.
-
- {move || if is_opponent {
- t_string!(i18n, opp_scored_pts, n = points_earned)
- } else {
- t_string!(i18n, scored_pts, n = points_earned)
- }}
+
+
+ {move || if is_opponent {
+ t_string!(i18n, opp_scored_pts, n = points_earned)
+ } else {
+ t_string!(i18n, scored_pts, n = points_earned)
+ }}
+