From 88bb15d1cc872772edc5ef1d27534b3cbbe9253e Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 11 Jan 2026 16:47:44 -0500 Subject: [PATCH] [weaver-app] add HexColourInput component --- .../assets/styling/hex-colour-input.css | 61 +++++++++ .../src/components/hex_colour_input.rs | 120 ++++++++++++++++++ crates/weaver-app/src/components/mod.rs | 3 + 3 files changed, 184 insertions(+) create mode 100644 crates/weaver-app/assets/styling/hex-colour-input.css create mode 100644 crates/weaver-app/src/components/hex_colour_input.rs diff --git a/crates/weaver-app/assets/styling/hex-colour-input.css b/crates/weaver-app/assets/styling/hex-colour-input.css new file mode 100644 index 0000000..c50ce63 --- /dev/null +++ b/crates/weaver-app/assets/styling/hex-colour-input.css @@ -0,0 +1,61 @@ +/* Hex colour input with preview swatch */ + +.hex-colour-input { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.hex-colour-input-label { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text); +} + +.hex-colour-input-field { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0.5rem; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 4px; +} + +.hex-colour-input-swatch { + width: 1.5rem; + height: 1.5rem; + border-radius: 4px; + border: 1px solid var(--color-border); + flex-shrink: 0; +} + +.hex-colour-input-hash { + color: var(--color-muted); + font-family: var(--font-mono); + font-size: 0.875rem; + user-select: none; +} + +.hex-colour-input-text { + font-family: var(--font-mono); + font-size: 0.875rem; + text-transform: uppercase; + color: var(--color-text); + background-color: transparent; + border: none; + outline: none; + width: 5rem; + padding: 0; +} + +.hex-colour-input-text::placeholder { + color: var(--color-muted); + text-transform: uppercase; +} + +.hex-colour-input-field:focus-within { + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary); +} diff --git a/crates/weaver-app/src/components/hex_colour_input.rs b/crates/weaver-app/src/components/hex_colour_input.rs new file mode 100644 index 0000000..0ebc5df --- /dev/null +++ b/crates/weaver-app/src/components/hex_colour_input.rs @@ -0,0 +1,120 @@ +//! 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, +} + +/// 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); + }, + } + } + } + } +} + +/// 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. + } +} diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index b37aff1..ea3da4e 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -359,3 +359,6 @@ pub mod slider; pub mod checkbox; pub mod toggle_group; pub mod tooltip; + +mod hex_colour_input; +pub use hex_colour_input::HexColourInput; -- 2.51.2