diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index 732c9d8..dedd734 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -1401,6 +1401,26 @@ a:hover { text-decoration: underline; } 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 { --fc: rgba(190, 140, 35, 0.8) !important; } diff --git a/clients/web/src/game/components/board.rs b/clients/web/src/game/components/board.rs index 1e4a5a3..7c57a28 100644 --- a/clients/web/src/game/components/board.rs +++ b/clients/web/src/game/components/board.rs @@ -294,6 +294,14 @@ pub fn Board( 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). let fields_from = |nums: &[u8], is_top_row: bool| -> Vec { nums.iter() @@ -583,6 +591,72 @@ pub fn Board( .collect() }} + // 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! { +
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); + } + } + > + + + + + +
+ } + .into_any() + })}
{label_bl}