tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
[weaver-app] add HexColourInput component
Orual
3 weeks ago
88bb15d1
20fb6a00
+184
3 changed files
expand all
collapse all
unified
split
crates
weaver-app
assets
styling
hex-colour-input.css
src
components
hex_colour_input.rs
mod.rs
+61
crates/weaver-app/assets/styling/hex-colour-input.css
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
/* Hex colour input with preview swatch */
2
+
3
+
.hex-colour-input {
4
+
display: flex;
5
+
flex-direction: column;
6
+
gap: 0.25rem;
7
+
}
8
+
9
+
.hex-colour-input-label {
10
+
font-size: 0.875rem;
11
+
font-weight: 500;
12
+
color: var(--color-text);
13
+
}
14
+
15
+
.hex-colour-input-field {
16
+
display: flex;
17
+
flex-direction: row;
18
+
align-items: center;
19
+
gap: 0.5rem;
20
+
padding: 0.25rem 0.5rem;
21
+
background-color: var(--color-surface);
22
+
border: 1px solid var(--color-border);
23
+
border-radius: 4px;
24
+
}
25
+
26
+
.hex-colour-input-swatch {
27
+
width: 1.5rem;
28
+
height: 1.5rem;
29
+
border-radius: 4px;
30
+
border: 1px solid var(--color-border);
31
+
flex-shrink: 0;
32
+
}
33
+
34
+
.hex-colour-input-hash {
35
+
color: var(--color-muted);
36
+
font-family: var(--font-mono);
37
+
font-size: 0.875rem;
38
+
user-select: none;
39
+
}
40
+
41
+
.hex-colour-input-text {
42
+
font-family: var(--font-mono);
43
+
font-size: 0.875rem;
44
+
text-transform: uppercase;
45
+
color: var(--color-text);
46
+
background-color: transparent;
47
+
border: none;
48
+
outline: none;
49
+
width: 5rem;
50
+
padding: 0;
51
+
}
52
+
53
+
.hex-colour-input-text::placeholder {
54
+
color: var(--color-muted);
55
+
text-transform: uppercase;
56
+
}
57
+
58
+
.hex-colour-input-field:focus-within {
59
+
border-color: var(--color-primary);
60
+
box-shadow: 0 0 0 1px var(--color-primary);
61
+
}
+120
crates/weaver-app/src/components/hex_colour_input.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! Hex colour input with preview swatch.
2
+
3
+
use dioxus::prelude::*;
4
+
5
+
/// Props for HexColourInput.
6
+
#[derive(Props, Clone, PartialEq)]
7
+
pub struct HexColourInputProps {
8
+
/// Current hex value (without #).
9
+
pub value: String,
10
+
/// Callback when value changes.
11
+
pub onchange: EventHandler<String>,
12
+
/// Label for the input.
13
+
#[props(default)]
14
+
pub label: Option<String>,
15
+
/// Placeholder text.
16
+
#[props(default = "000000".to_string())]
17
+
pub placeholder: String,
18
+
}
19
+
20
+
/// A hex colour input with a colour preview swatch.
21
+
#[component]
22
+
pub fn HexColourInput(props: HexColourInputProps) -> Element {
23
+
// Normalise the value for display (expand 3-char to 6-char).
24
+
let display_value = normalise_hex(&props.value);
25
+
26
+
// Check if we have a valid hex to display in the swatch.
27
+
let swatch_colour = if is_valid_hex(&display_value) {
28
+
format!("#{display_value}")
29
+
} else {
30
+
// Grey for invalid hex.
31
+
"var(--color-muted)".to_string()
32
+
};
33
+
34
+
rsx! {
35
+
document::Link { rel: "stylesheet", href: asset!("/assets/styling/hex-colour-input.css") }
36
+
37
+
div { class: "hex-colour-input",
38
+
if let Some(label) = &props.label {
39
+
label { class: "hex-colour-input-label", "{label}" }
40
+
}
41
+
42
+
div { class: "hex-colour-input-field",
43
+
div {
44
+
class: "hex-colour-input-swatch",
45
+
style: "background-color: {swatch_colour}",
46
+
}
47
+
span { class: "hex-colour-input-hash", "#" }
48
+
input {
49
+
class: "hex-colour-input-text",
50
+
r#type: "text",
51
+
maxlength: "6",
52
+
placeholder: props.placeholder.clone(),
53
+
value: display_value.clone(),
54
+
oninput: move |e| {
55
+
// Filter to hex chars only and uppercase.
56
+
let filtered: String = e
57
+
.value()
58
+
.chars()
59
+
.filter(|c| c.is_ascii_hexdigit())
60
+
.take(6)
61
+
.collect::<String>()
62
+
.to_uppercase();
63
+
props.onchange.call(filtered);
64
+
},
65
+
}
66
+
}
67
+
}
68
+
}
69
+
}
70
+
71
+
/// Normalise 3-char hex to 6-char (e.g., "ABC" -> "AABBCC").
72
+
fn normalise_hex(hex: &str) -> String {
73
+
let hex = hex.to_uppercase();
74
+
if hex.len() == 3 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
75
+
hex.chars().flat_map(|c| [c, c]).collect()
76
+
} else {
77
+
hex
78
+
}
79
+
}
80
+
81
+
/// Check if valid 6-digit hex.
82
+
fn is_valid_hex(hex: &str) -> bool {
83
+
hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit())
84
+
}
85
+
86
+
#[cfg(test)]
87
+
mod tests {
88
+
use super::*;
89
+
90
+
#[test]
91
+
fn test_normalise_hex_3_char() {
92
+
assert_eq!(normalise_hex("ABC"), "AABBCC");
93
+
assert_eq!(normalise_hex("abc"), "AABBCC");
94
+
assert_eq!(normalise_hex("123"), "112233");
95
+
}
96
+
97
+
#[test]
98
+
fn test_normalise_hex_6_char() {
99
+
assert_eq!(normalise_hex("AABBCC"), "AABBCC");
100
+
assert_eq!(normalise_hex("aabbcc"), "AABBCC");
101
+
}
102
+
103
+
#[test]
104
+
fn test_normalise_hex_invalid() {
105
+
// Invalid 3-char (not all hex) stays as-is.
106
+
assert_eq!(normalise_hex("GHI"), "GHI");
107
+
// Other lengths stay as-is.
108
+
assert_eq!(normalise_hex("AB"), "AB");
109
+
assert_eq!(normalise_hex("ABCDE"), "ABCDE");
110
+
}
111
+
112
+
#[test]
113
+
fn test_is_valid_hex() {
114
+
assert!(is_valid_hex("AABBCC"));
115
+
assert!(is_valid_hex("123456"));
116
+
assert!(!is_valid_hex("ABC")); // Too short.
117
+
assert!(!is_valid_hex("AABBCCDD")); // Too long.
118
+
assert!(!is_valid_hex("GHIJKL")); // Not hex.
119
+
}
120
+
}
+3
crates/weaver-app/src/components/mod.rs
···
359
pub mod checkbox;
360
pub mod toggle_group;
361
pub mod tooltip;
0
0
0
···
359
pub mod checkbox;
360
pub mod toggle_group;
361
pub mod tooltip;
362
+
363
+
mod hex_colour_input;
364
+
pub use hex_colour_input::HexColourInput;