diff --git a/client_web/src/components/scoring.rs b/client_web/src/components/scoring.rs
index 4a19a81..ab44ec4 100644
--- a/client_web/src/components/scoring.rs
+++ b/client_web/src/components/scoring.rs
@@ -1,12 +1,6 @@
use futures::channel::mpsc::UnboundedSender;
-#[cfg(target_arch = "wasm32")]
-use gloo_timers::future::TimeoutFuture;
use leptos::prelude::*;
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::*;
@@ -14,10 +8,6 @@ use crate::trictrac::types::{JanEntry, PlayerAction, ScoredEvent, SerTurnStage};
use super::score_panel::jan_label;
-/// One row in the scoring panel. Sets the hovered-moves context on enter
-/// (so board shows arrows for that jan's moves), but does NOT clear on
-/// leave — clearing is handled by the outer wrapper's mouseleave so that
-/// arrows persist while the pointer moves between rows.
fn scoring_jan_row(entry: JanEntry) -> impl IntoView {
let i18n = use_i18n();
let hovered = use_context::>>();
@@ -35,6 +25,11 @@ fn scoring_jan_row(entry: JanEntry) -> impl IntoView {
h.set(moves_hover.clone());
}
}
+ on:mouseleave=move |_| {
+ if let Some(h) = hovered {
+ h.set(vec![]);
+ }
+ }
>
{move || jan_label(&jan)}{move || if is_double {
@@ -63,147 +58,51 @@ pub fn ScoringPanel(
let holes_total = event.holes_total;
let bredouille = event.bredouille;
let show_hold_go = !is_opponent && turn_stage == SerTurnStage::HoldOrGoChoice;
- let panel_class = if is_opponent {
- "scoring-panel scoring-panel-opp"
- } else {
- "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);
-
- // ── 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 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![]);
- peeked.set(true);
- });
- }
+ let panel_class = if is_opponent { "scoring-panel scoring-panel-opp" } else { "scoring-panel" };
let jan_rows: Vec<_> = event.jans.into_iter().map(scoring_jan_row).collect();
view! {
- // ── Outer wrapper: owns the slide / peek / reveal animation ───────
- // pointer-events are on by default (parent .side-panel sets none,
- // and .scoring-panel-wrapper overrides back to auto in CSS).
-
-
-
- {move || if is_opponent {
- t_string!(i18n, opp_scored_pts, n = points_earned)
- } else {
- t_string!(i18n, scored_pts, n = points_earned)
- }}
-