diff --git a/solitaire_engine/src/cursor_plugin.rs b/solitaire_engine/src/cursor_plugin.rs index 0e367ce..85923a5 100644 --- a/solitaire_engine/src/cursor_plugin.rs +++ b/solitaire_engine/src/cursor_plugin.rs @@ -50,8 +50,17 @@ use crate::ui_theme::{ /// Kept in sync with the `marker_colour` constant there. const MARKER_DEFAULT: Color = Color::srgba(1.0, 1.0, 1.0, 0.08); -/// Green tint applied to pile markers that are valid drop targets during drag. -const MARKER_VALID: Color = Color::srgba(0.15, 0.85, 0.25, 0.55); +/// Lime tint applied to pile markers that are valid drop targets during +/// a drag. Same RGB as the design-system [`STATE_SUCCESS`] token at 55% +/// alpha, so the in-game "this is a legal target" colour stays +/// consistent with foundation-completion flourishes and other +/// valid-move signals. Spelled as a literal because `Alpha::with_alpha` +/// is not yet a `const` trait method on stable; the tracking test +/// below pins the RGB to `STATE_SUCCESS` so a palette swap can't drift +/// the two apart silently. Distinct from [`DROP_TARGET_FILL`] (10% +/// alpha) because the marker sprite is thin and would otherwise wash +/// out at a similar opacity. +const MARKER_VALID: Color = Color::srgba(0.675, 0.761, 0.404, 0.55); /// Marker component on a parent entity that owns one drop-target overlay /// (a translucent fill plus four outline edges as children). The wrapped @@ -524,6 +533,22 @@ mod tests { ); } + #[test] + fn marker_valid_rgb_tracks_state_success_token() { + // `MARKER_VALID` is spelled as a literal because + // `Alpha::with_alpha` is not a `const` trait method on stable. + // This test pins its RGB to `STATE_SUCCESS` so a future + // palette swap that updates the token but forgets the marker + // fails loudly here. + use crate::ui_theme::STATE_SUCCESS; + let valid = MARKER_VALID.to_srgba(); + let success = STATE_SUCCESS.to_srgba(); + assert!((valid.red - success.red).abs() < 1e-6); + assert!((valid.green - success.green).abs() < 1e-6); + assert!((valid.blue - success.blue).abs() < 1e-6); + assert!((valid.alpha - 0.55).abs() < 1e-6); + } + // ----------------------------------------------------------------------- // pick_cursor_icon priority-order tests // ----------------------------------------------------------------------- diff --git a/solitaire_engine/src/selection_plugin.rs b/solitaire_engine/src/selection_plugin.rs index 60d9fce..a6fd588 100644 --- a/solitaire_engine/src/selection_plugin.rs +++ b/solitaire_engine/src/selection_plugin.rs @@ -48,6 +48,7 @@ use crate::input_plugin::{best_destination, best_tableau_destination_for_stack}; use crate::layout::LayoutResource; use crate::pause_plugin::PausedResource; use crate::resources::{DragState, GameStateResource}; +use crate::ui_theme::{ACCENT_PRIMARY, STATE_SUCCESS, STATE_WARNING}; // --------------------------------------------------------------------------- // Public types @@ -660,14 +661,18 @@ fn update_selection_highlight( }; let card_size = layout.0.card_size; - // Choose colours per mode: cyan in source-pick, gold while lifted. + // Highlight tints follow the Terminal palette's semantic state + // tokens: cyan focus/selection while picking the source, gold + // attention/commitment once the cards are lifted, lime valid-move + // tint on the destination. Alphas are kept non-zero so the card + // face beneath remains readable through the wash. let lifted = kbd_drag.is_lifted(); let source_color = if lifted { - Color::srgba(1.0, 0.84, 0.0, 0.6) + STATE_WARNING.with_alpha(0.6) } else { - Color::srgba(0.0, 1.0, 1.0, 0.5) + ACCENT_PRIMARY.with_alpha(0.5) }; - let dest_color = Color::srgba(0.0, 1.0, 0.4, 0.6); + let dest_color = STATE_SUCCESS.with_alpha(0.6); // Resolve the source pile from KeyboardDragState (when lifted) or // SelectionState (otherwise). Lifted takes precedence so the gold diff --git a/solitaire_engine/src/ui_modal.rs b/solitaire_engine/src/ui_modal.rs index a239e0d..79cc1b9 100644 --- a/solitaire_engine/src/ui_modal.rs +++ b/solitaire_engine/src/ui_modal.rs @@ -135,7 +135,8 @@ pub const MODAL_ENTER_START_SCALE: f32 = 0.96; /// Visual emphasis tier applied to a [`ModalButton`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ButtonVariant { - /// Loud yellow CTA — Confirm, Play Again. One per modal; right-aligned. + /// Cyan CTA (`ACCENT_PRIMARY`) — Confirm, Play Again, Resume. One per + /// modal; right-aligned in the actions row. Primary, /// Mid-emphasis — Cancel, Close, Done. Secondary, @@ -332,14 +333,17 @@ pub fn spawn_modal_button( }; let label_color = match variant { - // Primary buttons sit on the loud yellow accent — dark text on - // top reads well and passes AAA contrast. + // Primary buttons sit on the cyan accent — `BG_BASE` text on + // top reads well and passes AAA contrast against `#6fc2ef`. ButtonVariant::Primary => BG_BASE, ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_PRIMARY, }; let caption_color = match variant { - // Use a slightly muted version of the label colour so the chip - // reads as a secondary detail without disappearing. + // Muted near-black on the cyan Primary so the hotkey chip reads + // as a secondary detail without disappearing. Deliberately a + // pure-black-at-alpha rather than `BG_BASE.with_alpha(...)`: + // `BG_BASE` is `#151515` (not 0,0,0), so the alpha-on-cyan + // composite would tint slightly cooler than intended here. ButtonVariant::Primary => Color::srgba(0.0, 0.0, 0.0, 0.55), ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_SECONDARY, }; @@ -395,9 +399,10 @@ fn hover_bg(variant: ButtonVariant) -> Color { } } -/// Pressed-state background colour. Primary swaps to the magenta -/// secondary accent for a moment of celebration; Secondary darkens to -/// the base elevation; Tertiary darkens further. +/// Pressed-state background colour. Primary swaps to the lavender +/// secondary accent (`ACCENT_SECONDARY`, `#e1a3ee`) for a moment of +/// celebration; Secondary darkens to the base elevation; Tertiary +/// darkens further to `BG_ELEVATED_PRESSED`. fn pressed_bg(variant: ButtonVariant) -> Color { match variant { ButtonVariant::Primary => ACCENT_SECONDARY,