1pub const BG_PRIMARY: &str = "#0a0a1a";
19pub const BG_SECONDARY: &str = "#1a1a2e";
21pub const BG_TERTIARY: &str = "#0f0f23";
23
24pub const BORDER: &str = "#333333";
26
27pub const TEXT_PRIMARY: &str = "#e0e0e0";
29pub const TEXT_SECONDARY: &str = "#888888";
31
32pub const ACCENT: &str = "#4ecdc4";
34pub const ACCENT_WARN: &str = "#ffd93d";
36pub const ACCENT_ERROR: &str = "#ff6b6b";
38
39pub const PROBAR: &str = "#ff6b6b";
41
42pub const CITY_NODE: &str = "#ffd93d";
44pub const TOUR_PATH: &str = "#4ecdc4";
46
47pub const FONT_MONO: &str = "'JetBrains Mono', 'Fira Code', monospace";
49pub const FONT_SIZE_BASE: f64 = 0.75;
51pub const FONT_SIZE_SMALL: f64 = 0.65;
53
54#[must_use]
56pub const fn hex_to_rgb(hex: &str) -> (u8, u8, u8) {
57 let bytes = hex.as_bytes();
59 let offset = if bytes[0] == b'#' { 1 } else { 0 };
60
61 let r = hex_byte(bytes[offset], bytes[offset + 1]);
62 let g = hex_byte(bytes[offset + 2], bytes[offset + 3]);
63 let b = hex_byte(bytes[offset + 4], bytes[offset + 5]);
64
65 (r, g, b)
66}
67
68const fn hex_byte(hi: u8, lo: u8) -> u8 {
69 hex_digit(hi) * 16 + hex_digit(lo)
70}
71
72const fn hex_digit(c: u8) -> u8 {
73 match c {
74 b'0'..=b'9' => c - b'0',
75 b'a'..=b'f' => c - b'a' + 10,
76 b'A'..=b'F' => c - b'A' + 10,
77 _ => 0,
78 }
79}
80
81#[must_use]
83pub fn css_variables() -> String {
84 format!(
85 r":root {{
86 --bg-primary: {BG_PRIMARY};
87 --bg-secondary: {BG_SECONDARY};
88 --bg-tertiary: {BG_TERTIARY};
89 --border: {BORDER};
90 --text-primary: {TEXT_PRIMARY};
91 --text-secondary: {TEXT_SECONDARY};
92 --accent: {ACCENT};
93 --accent-warn: {ACCENT_WARN};
94 --accent-error: {ACCENT_ERROR};
95 --probar: {PROBAR};
96}}"
97 )
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_hex_to_rgb_with_hash() {
106 assert_eq!(hex_to_rgb("#4ecdc4"), (78, 205, 196));
107 assert_eq!(hex_to_rgb("#ffd93d"), (255, 217, 61));
108 assert_eq!(hex_to_rgb("#ff6b6b"), (255, 107, 107));
109 }
110
111 #[test]
112 fn test_hex_to_rgb_bg_colors() {
113 assert_eq!(hex_to_rgb(BG_PRIMARY), (10, 10, 26));
114 assert_eq!(hex_to_rgb(BG_SECONDARY), (26, 26, 46));
115 }
116
117 #[test]
118 fn test_css_variables_contains_all_colors() {
119 let css = css_variables();
120 assert!(css.contains("--bg-primary"));
121 assert!(css.contains("--accent"));
122 assert!(css.contains("#4ecdc4"));
123 }
124
125 #[test]
126 fn test_colors_are_valid_hex() {
127 assert_eq!(BG_PRIMARY.len(), 7);
129 assert_eq!(ACCENT.len(), 7);
130 assert_eq!(ACCENT_WARN.len(), 7);
131 assert!(BG_PRIMARY.starts_with('#'));
132 }
133
134 #[test]
135 fn test_contrast_accessibility() {
136 let (tr, tg, tb) = hex_to_rgb(TEXT_PRIMARY);
139 let (br, bg, bb) = hex_to_rgb(BG_PRIMARY);
140
141 let text_lum = (f64::from(tr) + f64::from(tg) + f64::from(tb)) / 3.0 / 255.0;
143 let bg_lum = (f64::from(br) + f64::from(bg) + f64::from(bb)) / 3.0 / 255.0;
144
145 let ratio = (text_lum + 0.05) / (bg_lum + 0.05);
147 assert!(
148 ratio > 4.5,
149 "Text contrast ratio should exceed WCAG AA: {ratio}"
150 );
151 }
152}