Compare commits
No commits in common. "9bdb32b364c33fe719169a7adce36c7823845716" and "bceec1f8fe997cf5de55b1e95237f26d09420cbe" have entirely different histories.
9bdb32b364
...
bceec1f8fe
4 changed files with 148 additions and 263 deletions
|
|
@ -1154,105 +1154,66 @@ a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.game-over-actions { display: flex; gap: 0.75rem; justify-content: center; }
|
.game-over-actions { display: flex; gap: 0.75rem; justify-content: center; }
|
||||||
|
|
||||||
/* ── Score-area: position:relative wrapper for merged panel + scoring ── */
|
|
||||||
.score-area {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Scoring panels container — right of the hole counter ───────────── */
|
|
||||||
/* Stacked column, right-aligned, covering the free space in each row. */
|
|
||||||
/* overflow:visible lets tall panels float over the board below. */
|
|
||||||
.scoring-panels-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: flex-end;
|
|
||||||
padding: 4px 8px;
|
|
||||||
z-index: 10;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Scoring notification panel (§6b) ───────────────────────────────── */
|
/* ── Scoring notification panel (§6b) ───────────────────────────────── */
|
||||||
@keyframes scoring-panel-enter {
|
@keyframes scoring-panel-enter {
|
||||||
from { opacity: 0; transform: translateX(10px); }
|
from { transform: translateX(100%); }
|
||||||
to { opacity: 1; transform: translateX(0); }
|
to { transform: translateX(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoring-panel-wrapper {
|
.scoring-panel-wrapper {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
display: flex;
|
animation: scoring-panel-enter 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
flex-direction: column;
|
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
align-items: flex-end;
|
filter: drop-shadow(-4px 0 14px rgba(0,0,0,0.38));
|
||||||
gap: 3px;
|
|
||||||
animation: scoring-panel-enter 0.3s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "+" expand button: hidden while the panel is expanded */
|
.scoring-panel-wrapper.peeked {
|
||||||
.scoring-panel-wrapper:not(.scoring-minimized) .scoring-expand-btn {
|
transform: translateX(100%);
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full panel card: hidden once minimised */
|
.scoring-panel-wrapper.revealed {
|
||||||
.scoring-panel-wrapper.scoring-minimized .scoring-panel {
|
transform: translateX(0);
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "+" expand button ─────────────────────────────────────────────────── */
|
.scoring-panel-wrapper.peeked:not(.revealed) {
|
||||||
.scoring-expand-btn {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1;
|
|
||||||
background: var(--ui-parchment);
|
|
||||||
border: 1.5px solid var(--ui-gold-dark);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 2px 7px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--ui-ink);
|
|
||||||
opacity: 0.72;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.18);
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.scoring-expand-btn:hover { opacity: 1; }
|
|
||||||
|
|
||||||
/* ── Panel head: scoring total + "−" collapse link ──────────────────── */
|
|
||||||
.scoring-panel-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoring-collapse-btn {
|
/* ── Inline scoring panel (in bottom strip, expands downward) ───────── */
|
||||||
font-size: 0.78rem;
|
@keyframes scoring-expand-in {
|
||||||
line-height: 1;
|
from { opacity: 0; transform: translateY(-5px); }
|
||||||
background: none;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
border: none;
|
}
|
||||||
cursor: pointer;
|
|
||||||
color: rgba(0,0,0,0.35);
|
.scoring-panel-wrapper.scoring-panel-inline-wrap {
|
||||||
padding: 0 1px;
|
animation: none;
|
||||||
margin-left: auto;
|
filter: none;
|
||||||
flex-shrink: 0;
|
transform: none !important;
|
||||||
transition: color 0.15s;
|
pointer-events: auto;
|
||||||
|
cursor: default;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-panel-wrapper.scoring-panel-inline-wrap .scoring-panel {
|
||||||
|
animation: scoring-expand-in 0.28s ease-out;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.07);
|
||||||
|
padding: 0.4rem 0;
|
||||||
}
|
}
|
||||||
.scoring-collapse-btn:hover { color: rgba(0,0,0,0.65); }
|
|
||||||
|
|
||||||
/* ── Inner scoring card ─────────────────────────────────────────────── */
|
|
||||||
.scoring-panel {
|
.scoring-panel {
|
||||||
background: var(--ui-parchment);
|
background: var(--ui-parchment);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.45rem 0.85rem;
|
padding: 0.45rem 0.85rem;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.22);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
|
||||||
border-left: 3px solid var(--ui-green-accent);
|
border-left: 3px solid var(--ui-green-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
width: 320px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoring-total {
|
.scoring-total {
|
||||||
|
|
@ -1295,6 +1256,11 @@ a:hover { text-decoration: underline; }
|
||||||
right: auto;
|
right: auto;
|
||||||
left: calc(100% + 1rem);
|
left: calc(100% + 1rem);
|
||||||
}
|
}
|
||||||
|
.scoring-panel-wrapper.peeked,
|
||||||
|
.scoring-panel-wrapper.revealed {
|
||||||
|
transform: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Board wrapper ──────────────────────────────────────────────────── */
|
/* ── Board wrapper ──────────────────────────────────────────────────── */
|
||||||
|
|
@ -1435,26 +1401,6 @@ a:hover { text-decoration: underline; }
|
||||||
animation: exit-glow 2s ease-in-out infinite;
|
animation: exit-glow 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Exit sign (§8c) — circle+arrow outside the board ──────────────── */
|
|
||||||
.exit-btn {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.3;
|
|
||||||
transition: opacity 0.2s, transform 0.15s;
|
|
||||||
}
|
|
||||||
.exit-btn.exit-active {
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 1;
|
|
||||||
animation: exit-btn-pulse 1.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.exit-btn.exit-active:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
@keyframes exit-btn-pulse {
|
|
||||||
0%, 100% { filter: drop-shadow(0 0 3px rgba(200,160,20,0.3)); }
|
|
||||||
50% { filter: drop-shadow(0 0 9px rgba(200,160,20,0.85)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.field.jan-hovered {
|
.field.jan-hovered {
|
||||||
--fc: rgba(190, 140, 35, 0.8) !important;
|
--fc: rgba(190, 140, 35, 0.8) !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -294,14 +294,6 @@ pub fn Board(
|
||||||
exit_field_test = |f| matches!(f, 1..=6);
|
exit_field_test = |f| matches!(f, 1..=6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a clickable exit sign outside the board when bearing off is possible.
|
|
||||||
let has_exit_move = valid_sequences
|
|
||||||
.iter()
|
|
||||||
.any(|(m1, m2)| m1.get_to() == 0 || m2.get_to() == 0);
|
|
||||||
let show_exit_btn = all_in_exit && is_move_stage && has_exit_move;
|
|
||||||
let seqs_exit_cls = valid_sequences.clone();
|
|
||||||
let seqs_exit_click = valid_sequences.clone();
|
|
||||||
|
|
||||||
// `valid_sequences` is cloned per field (the Vec is small; Send-safe unlike Rc).
|
// `valid_sequences` is cloned per field (the Vec is small; Send-safe unlike Rc).
|
||||||
let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> {
|
let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> {
|
||||||
nums.iter()
|
nums.iter()
|
||||||
|
|
@ -591,72 +583,6 @@ pub fn Board(
|
||||||
.collect()
|
.collect()
|
||||||
}}
|
}}
|
||||||
</svg>
|
</svg>
|
||||||
// Exit sign: circle+arrow outside the board, next to the last exit field.
|
|
||||||
// White exits to the right (top-right quarter); Black exits to the left (top-left).
|
|
||||||
{show_exit_btn.then(|| {
|
|
||||||
let (pos_style, line_x1, line_x2, head_pts): (&str, &str, &str, &str) =
|
|
||||||
if is_white {
|
|
||||||
(
|
|
||||||
"position:absolute;right:-60px;top:15px;width:50px;height:50px",
|
|
||||||
"10", "31", "23,17 32,25 23,33",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
"position:absolute;left:-60px;top:15px;width:50px;height:50px",
|
|
||||||
"40", "19", "27,17 18,25 27,33",
|
|
||||||
)
|
|
||||||
};
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
title="Exit"
|
|
||||||
style=pos_style
|
|
||||||
class=move || {
|
|
||||||
let staged = staged_moves.get();
|
|
||||||
let sel = selected_origin.get();
|
|
||||||
let active = match sel {
|
|
||||||
Some(origin) => seqs_exit_cls.is_empty()
|
|
||||||
|| valid_dests_for(&seqs_exit_cls, &staged, origin)
|
|
||||||
.iter()
|
|
||||||
.any(|&d| d == 0),
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
if active { "exit-btn exit-active" } else { "exit-btn" }
|
|
||||||
}
|
|
||||||
on:click=move |_| {
|
|
||||||
if !is_move_stage { return; }
|
|
||||||
let staged = staged_moves.get_untracked();
|
|
||||||
if staged.len() >= 2 { return; }
|
|
||||||
let Some(origin) = selected_origin.get_untracked() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let valid = seqs_exit_click.is_empty()
|
|
||||||
|| valid_dests_for(&seqs_exit_click, &staged, origin)
|
|
||||||
.iter()
|
|
||||||
.any(|&d| d == 0);
|
|
||||||
if valid {
|
|
||||||
staged_moves.update(|v| v.push((origin, 0)));
|
|
||||||
selected_origin.set(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<svg width="50" height="50" viewBox="0 0 50 50">
|
|
||||||
<circle
|
|
||||||
cx="25" cy="25" r="20"
|
|
||||||
style="fill:rgba(10,20,10,0.75);stroke:rgba(210,170,30,0.75);stroke-width:2.5"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1=line_x1 y1="25" x2=line_x2 y2="25"
|
|
||||||
style="stroke:rgba(210,170,30,0.85);stroke-width:2.5;stroke-linecap:round"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points=head_pts
|
|
||||||
style="fill:none;stroke:rgba(210,170,30,0.85);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="zone-labels-row">
|
<div class="zone-labels-row">
|
||||||
<div class="zone-label zone-label-quarter">{label_bl}</div>
|
<div class="zone-label zone-label-quarter">{label_bl}</div>
|
||||||
|
|
|
||||||
|
|
@ -287,29 +287,16 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
// ── Merged scoreboard + scoring panels (above board) ─────────────
|
// ── Merged scoreboard (both players, above board) ────────────────
|
||||||
// score-area is position:relative so the scoring-panels-container
|
<MergedScorePanel
|
||||||
// can be absolute-positioned at the right of the hole counter.
|
my_score=my_score
|
||||||
<div class="score-area">
|
opp_score=opp_score
|
||||||
<MergedScorePanel
|
my_points_earned=my_pts_earned
|
||||||
my_score=my_score
|
opp_points_earned=opp_pts_earned
|
||||||
opp_score=opp_score
|
my_holes_gained=my_holes_gained_score
|
||||||
my_points_earned=my_pts_earned
|
opp_holes_gained=opp_holes_gained_score
|
||||||
opp_points_earned=opp_pts_earned
|
my_bredouille=my_bredouille_flash
|
||||||
my_holes_gained=my_holes_gained_score
|
/>
|
||||||
opp_holes_gained=opp_holes_gained_score
|
|
||||||
my_bredouille=my_bredouille_flash
|
|
||||||
/>
|
|
||||||
// Scoring detail panels — stacked at the right, overlapping if needed.
|
|
||||||
<div class="scoring-panels-container">
|
|
||||||
{my_scored_event.map(|event| view! {
|
|
||||||
<ScoringPanel event=event turn_stage=turn_stage_for_panel />
|
|
||||||
})}
|
|
||||||
{opp_scored_event.map(|event| view! {
|
|
||||||
<ScoringPanel event=event turn_stage=SerTurnStage::RollDice is_opponent=true />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// ── Board ────────────────────────────────────────────────────────
|
// ── Board ────────────────────────────────────────────────────────
|
||||||
<Board
|
<Board
|
||||||
|
|
@ -405,6 +392,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
// ── Scoring detail panels — expand downward in the strip ──────
|
||||||
|
{my_scored_event.map(|event| view! {
|
||||||
|
<ScoringPanel event=event turn_stage=turn_stage_for_panel inline=true />
|
||||||
|
})}
|
||||||
|
{opp_scored_event.map(|event| view! {
|
||||||
|
<ScoringPanel event=event turn_stage=SerTurnStage::RollDice is_opponent=true inline=true />
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// ── Pre-game ceremony overlay ─────────────────────────────────────
|
// ── Pre-game ceremony overlay ─────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,15 @@ use futures::channel::mpsc::UnboundedSender;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use std::sync::{
|
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
use trictrac_store::CheckerMove;
|
use trictrac_store::CheckerMove;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
|
||||||
|
|
||||||
use crate::app::NetCommand;
|
use crate::app::NetCommand;
|
||||||
use crate::game::trictrac::types::{JanEntry, PlayerAction, ScoredEvent, SerTurnStage};
|
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
use crate::game::trictrac::types::{JanEntry, PlayerAction, ScoredEvent, SerTurnStage};
|
||||||
|
|
||||||
use super::score_panel::jan_label;
|
use super::score_panel::jan_label;
|
||||||
|
|
||||||
|
|
@ -51,19 +48,14 @@ 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]
|
#[component]
|
||||||
pub fn ScoringPanel(
|
pub fn ScoringPanel(
|
||||||
event: ScoredEvent,
|
event: ScoredEvent,
|
||||||
turn_stage: SerTurnStage,
|
turn_stage: SerTurnStage,
|
||||||
#[prop(default = false)] is_opponent: bool,
|
#[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 {
|
) -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
let cmd_tx = use_context::<UnboundedSender<NetCommand>>()
|
let cmd_tx = use_context::<UnboundedSender<NetCommand>>()
|
||||||
|
|
@ -80,28 +72,91 @@ pub fn ScoringPanel(
|
||||||
"scoring-panel"
|
"scoring-panel"
|
||||||
};
|
};
|
||||||
|
|
||||||
// minimized: starts false (expanded), becomes true after 3.4 s unless
|
// ── Lifecycle signals ──────────────────────────────────────────────────
|
||||||
// the Hold/Go choice still needs the player's attention.
|
// peeked: added after 3.4 s (slide to peek strip)
|
||||||
let minimized = RwSignal::new(false);
|
// 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.
|
// ── Collect all moves from all jans for automatic arrow display ────────
|
||||||
let all_moves: Vec<(CheckerMove, CheckerMove)> = event
|
let all_moves: Vec<(CheckerMove, CheckerMove)> = event
|
||||||
.jans
|
.jans
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|e| e.moves.iter().cloned())
|
.flat_map(|e| e.moves.iter().cloned())
|
||||||
.collect();
|
.collect();
|
||||||
let all_moves_auto = all_moves.clone();
|
let all_moves_click = all_moves.clone();
|
||||||
let all_moves_expand = all_moves.clone();
|
let all_moves_enter = all_moves.clone();
|
||||||
let all_moves_enter = all_moves.clone();
|
|
||||||
|
|
||||||
let hovered_ctx = use_context::<RwSignal<Vec<(CheckerMove, CheckerMove)>>>();
|
let hovered_ctx = use_context::<RwSignal<Vec<(CheckerMove, CheckerMove)>>>();
|
||||||
|
|
||||||
|
// 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<Cell<bool>>, NOT RwSignal<bool>.
|
||||||
|
// 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<Cell<bool>>
|
||||||
|
// 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<AtomicBool> 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();
|
let jan_rows: Vec<_> = event.jans.into_iter().map(scoring_jan_row).collect();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
// ── Outer wrapper: overlay mode has slide/peek/reveal animation;
|
||||||
|
// inline mode suppresses all of that via scoring-panel-inline-wrap.
|
||||||
<div
|
<div
|
||||||
class="scoring-panel-wrapper"
|
class="scoring-panel-wrapper"
|
||||||
class:scoring-minimized=move || minimized.get()
|
class:scoring-panel-inline-wrap=inline
|
||||||
|
class:peeked=move || peeked.get()
|
||||||
|
class:revealed=move || revealed.get()
|
||||||
|
on:click=move |_| {
|
||||||
|
// Inline panels don't have the peek/reveal toggle.
|
||||||
|
if !inline {
|
||||||
|
if peeked.get_untracked() {
|
||||||
|
revealed.update(|r| *r = !*r);
|
||||||
|
}
|
||||||
|
if let Some(hm) = hovered_ctx {
|
||||||
|
if !revealed.get_untracked() {
|
||||||
|
hm.set(all_moves_click.clone());
|
||||||
|
} else {
|
||||||
|
hm.set(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
on:mouseenter=move |_| {
|
on:mouseenter=move |_| {
|
||||||
|
// Show all event moves as arrows while the cursor is inside.
|
||||||
if let Some(hm) = hovered_ctx {
|
if let Some(hm) = hovered_ctx {
|
||||||
hm.set(all_moves_enter.clone());
|
hm.set(all_moves_enter.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -112,44 +167,13 @@ pub fn ScoringPanel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
// "+" expand button — shown only when minimised (CSS hides it otherwise).
|
|
||||||
<button
|
|
||||||
class="scoring-expand-btn"
|
|
||||||
title="Show scoring details"
|
|
||||||
on:click=move |ev: leptos::web_sys::MouseEvent| {
|
|
||||||
ev.stop_propagation();
|
|
||||||
minimized.set(false);
|
|
||||||
if let Some(hm) = hovered_ctx {
|
|
||||||
hm.set(all_moves_expand.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
"+"
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// Full panel — hidden when minimised via CSS.
|
|
||||||
<div class=panel_class>
|
<div class=panel_class>
|
||||||
<div class="scoring-panel-head">
|
<div class="scoring-total">
|
||||||
<div class="scoring-total">
|
{move || if is_opponent {
|
||||||
{move || if is_opponent {
|
t_string!(i18n, opp_scored_pts, n = points_earned)
|
||||||
t_string!(i18n, opp_scored_pts, n = points_earned)
|
} else {
|
||||||
} else {
|
t_string!(i18n, scored_pts, n = points_earned)
|
||||||
t_string!(i18n, scored_pts, n = points_earned)
|
}}
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="scoring-collapse-btn"
|
|
||||||
title="Minimise"
|
|
||||||
on:click=move |ev: leptos::web_sys::MouseEvent| {
|
|
||||||
ev.stop_propagation();
|
|
||||||
minimized.set(true);
|
|
||||||
if let Some(hm) = hovered_ctx {
|
|
||||||
hm.set(vec![]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
"−"
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{jan_rows}
|
{jan_rows}
|
||||||
{(holes_gained > 0).then(|| view! {
|
{(holes_gained > 0).then(|| view! {
|
||||||
|
|
@ -170,22 +194,17 @@ pub fn ScoringPanel(
|
||||||
let dismissed = RwSignal::new(false);
|
let dismissed = RwSignal::new(false);
|
||||||
view! {
|
view! {
|
||||||
<div class="hold-go-buttons" class:hidden=move || dismissed.get()>
|
<div class="hold-go-buttons" class:hidden=move || dismissed.get()>
|
||||||
<button class="btn btn-secondary"
|
// stop_propagation so these buttons don't also toggle the panel
|
||||||
on:click=move |ev: leptos::web_sys::MouseEvent| {
|
<button class="btn btn-secondary" on:click=move |ev: leptos::web_sys::MouseEvent| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
dismissed.set(true);
|
dismissed.set(true);
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{t!(i18n, hold)}
|
{t!(i18n, hold)}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary" on:click=move |ev: leptos::web_sys::MouseEvent| {
|
||||||
on:click=move |ev: leptos::web_sys::MouseEvent| {
|
ev.stop_propagation();
|
||||||
ev.stop_propagation();
|
cmd_tx.unbounded_send(NetCommand::Action(PlayerAction::Go)).ok();
|
||||||
cmd_tx
|
}>
|
||||||
.unbounded_send(NetCommand::Action(PlayerAction::Go))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t!(i18n, go)}
|
{t!(i18n, go)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue