//! Hex colour input with preview swatch. use dioxus::prelude::*; /// Props for HexColourInput. #[derive(Props, Clone, PartialEq)] pub struct HexColourInputProps { /// Current hex value (without #). pub value: String, /// Callback when value changes. pub onchange: EventHandler, /// Label for the input. #[props(default)] pub label: Option, /// Placeholder text. #[props(default = "000000".to_string())] pub placeholder: String, /// Callback when input receives focus. #[props(default)] pub onfocus: Option>, } /// A hex colour input with a colour preview swatch. #[component] pub fn HexColourInput(props: HexColourInputProps) -> Element { // Normalise the value for display (expand 3-char to 6-char). let display_value = normalise_hex(&props.value); // Check if we have a valid hex to display in the swatch. let swatch_colour = if is_valid_hex(&display_value) { format!("#{display_value}") } else { // Grey for invalid hex. "var(--color-muted)".to_string() }; rsx! { document::Link { rel: "stylesheet", href: asset!("/assets/styling/hex-colour-input.css") } div { class: "hex-colour-input", if let Some(label) = &props.label { label { class: "hex-colour-input-label", "{label}" } } div { class: "hex-colour-input-field", div { class: "hex-colour-input-swatch", style: "background-color: {swatch_colour}", } span { class: "hex-colour-input-hash", "#" } input { class: "hex-colour-input-text", r#type: "text", maxlength: "6", placeholder: props.placeholder.clone(), value: display_value.clone(), oninput: move |e| { // Filter to hex chars only and uppercase. let filtered: String = e .value() .chars() .filter(|c| c.is_ascii_hexdigit()) .take(6) .collect::() .to_uppercase(); props.onchange.call(filtered); }, onfocus: move |e| { if let Some(handler) = &props.onfocus { handler.call(e); } }, } } } } } /// Normalise 3-char hex to 6-char (e.g., "ABC" -> "AABBCC"). fn normalise_hex(hex: &str) -> String { let hex = hex.to_uppercase(); if hex.len() == 3 && hex.chars().all(|c| c.is_ascii_hexdigit()) { hex.chars().flat_map(|c| [c, c]).collect() } else { hex } } /// Check if valid 6-digit hex. fn is_valid_hex(hex: &str) -> bool { hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_normalise_hex_3_char() { assert_eq!(normalise_hex("ABC"), "AABBCC"); assert_eq!(normalise_hex("abc"), "AABBCC"); assert_eq!(normalise_hex("123"), "112233"); } #[test] fn test_normalise_hex_6_char() { assert_eq!(normalise_hex("AABBCC"), "AABBCC"); assert_eq!(normalise_hex("aabbcc"), "AABBCC"); } #[test] fn test_normalise_hex_invalid() { // Invalid 3-char (not all hex) stays as-is. assert_eq!(normalise_hex("GHI"), "GHI"); // Other lengths stay as-is. assert_eq!(normalise_hex("AB"), "AB"); assert_eq!(normalise_hex("ABCDE"), "ABCDE"); } #[test] fn test_is_valid_hex() { assert!(is_valid_hex("AABBCC")); assert!(is_valid_hex("123456")); assert!(!is_valid_hex("ABC")); // Too short. assert!(!is_valid_hex("AABBCCDD")); // Too long. assert!(!is_valid_hex("GHIJKL")); // Not hex. } }