Skip to main content

simular/edd/
style.rs

1//! Shared Style Constants for TUI/WASM Parity
2//!
3//! This module provides unified color and style constants to ensure
4//! visual consistency between TUI (ratatui) and WASM (Canvas/CSS) renders.
5//!
6//! # Architecture (OR-001 Compliant)
7//!
8//! All visual constants are defined ONCE here and used by both:
9//! - TUI: via `ratatui::style::Color` conversions
10//! - WASM: via CSS variable injection / Canvas fill styles
11//!
12//! # References
13//!
14//! - WCAG 2.1 AA contrast requirements
15//! - TPS Visual Control (見える化)
16
17/// Primary background color (darkest)
18pub const BG_PRIMARY: &str = "#0a0a1a";
19/// Secondary background color (panels)
20pub const BG_SECONDARY: &str = "#1a1a2e";
21/// Tertiary background color (canvas/content)
22pub const BG_TERTIARY: &str = "#0f0f23";
23
24/// Primary border color
25pub const BORDER: &str = "#333333";
26
27/// Primary text color (high contrast)
28pub const TEXT_PRIMARY: &str = "#e0e0e0";
29/// Secondary text color (labels, muted)
30pub const TEXT_SECONDARY: &str = "#888888";
31
32/// Accent color (teal/cyan) - success, highlights
33pub const ACCENT: &str = "#4ecdc4";
34/// Warning color (yellow/gold)
35pub const ACCENT_WARN: &str = "#ffd93d";
36/// Error color (red/coral)
37pub const ACCENT_ERROR: &str = "#ff6b6b";
38
39/// Probar brand color (testing indicator)
40pub const PROBAR: &str = "#ff6b6b";
41
42/// City node color (yellow dots on map)
43pub const CITY_NODE: &str = "#ffd93d";
44/// Tour path color (teal lines)
45pub const TOUR_PATH: &str = "#4ecdc4";
46
47/// Font family for monospace text
48pub const FONT_MONO: &str = "'JetBrains Mono', 'Fira Code', monospace";
49/// Font size base (rem)
50pub const FONT_SIZE_BASE: f64 = 0.75;
51/// Font size small (rem)
52pub const FONT_SIZE_SMALL: f64 = 0.65;
53
54/// Parse hex color to RGB tuple
55#[must_use]
56pub const fn hex_to_rgb(hex: &str) -> (u8, u8, u8) {
57    // Skip the '#' prefix
58    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/// Generate CSS custom properties string for injection
82#[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        // All color constants should be 7 chars (#RRGGBB)
128        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        // WCAG AA requires 4.5:1 contrast ratio for text
137        // TEXT_PRIMARY (#e0e0e0) on BG_PRIMARY (#0a0a1a) should pass
138        let (tr, tg, tb) = hex_to_rgb(TEXT_PRIMARY);
139        let (br, bg, bb) = hex_to_rgb(BG_PRIMARY);
140
141        // Simplified relative luminance (not exact WCAG formula)
142        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        // Contrast ratio approximation
146        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}