Skip to main content

openapi_ui/
theme.rs

1//! Built-in light and dark themes.
2
3const LIGHT: &str = r#"
4:root {
5    --bg: #faf9f5;
6    --bg-sub: #f3f1ea;
7    --bg-card: #ffffff;
8    --bg-code: #f0ede4;
9    --bd: #e5e1d6;
10    --bd-sub: #ede9df;
11    --t1: #1f1e1d;
12    --t2: #5c5852;
13    --t3: #9c9589;
14    --t4: #c4beb4;
15    --accent: #c96442;
16    --accent-h: #b5572d;
17    --accent-bg: #f9ede8;
18    --green: #2d8a4e;
19    --orange: #b56613;
20    --red: #c0392b;
21    --purple: #7952b3;
22    --cyan: #1a7a8a;
23    --blue: #1c6bbb;
24    --s2xx: #2d8a4e;
25    --s3xx: #1a7a8a;
26    --s4xx: #b56613;
27    --s5xx: #c0392b;
28    --sb-t: #eceae2;
29    --sb-th: #c8c3b8;
30    --shadow-sm: 0 1px 2px rgba(31, 30, 29, 0.06);
31    --shadow-md: 0 4px 12px rgba(31, 30, 29, 0.1);
32}
33"#;
34
35const DARK: &str = r#"
36[data-theme="dark"] {
37    --bg: #1e1c19;
38    --bg-sub: #252320;
39    --bg-card: #2d2b27;
40    --bg-code: #201f1c;
41    --bd: #3a3733;
42    --bd-sub: #302e2b;
43    --t1: #ede8df;
44    --t2: #9c9487;
45    --t3: #6b6560;
46    --t4: #3d3a36;
47    --accent: #d4714f;
48    --accent-h: #e07d5a;
49    --accent-bg: #2e2119;
50    --green: #4caf72;
51    --orange: #d4943a;
52    --red: #e05c4b;
53    --purple: #a07fd0;
54    --cyan: #38bcd4;
55    --blue: #5a9fe0;
56    --s2xx: #4caf72;
57    --s3xx: #38bcd4;
58    --s4xx: #d4943a;
59    --s5xx: #e05c4b;
60    --sb-t: #252320;
61    --sb-th: #3a3733;
62    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
63    --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
64}
65"#;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum ThemeMode {
69    /// Light theme only
70    Light,
71    /// Dark theme only
72    Dark,
73    /// Both themes with user toggle (default)
74    #[default]
75    System,
76}
77
78impl std::str::FromStr for ThemeMode {
79    type Err = ();
80
81    /// Create a ThemeMode from a string slice.
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        Ok(match s.to_lowercase().as_str() {
84            "light" => ThemeMode::Light,
85            "dark" => ThemeMode::Dark,
86            _ => ThemeMode::System,
87        })
88    }
89}
90
91impl ThemeMode {
92    /// Convert ThemeMode to a string slice.
93    pub fn as_str(&self) -> &'static str {
94        match self {
95            ThemeMode::Light => "light",
96            ThemeMode::Dark => "dark",
97            ThemeMode::System => "system",
98        }
99    }
100
101    /// Returns the CSS variables for this theme mode.
102    pub fn get_css(&self) -> String {
103        match self {
104            ThemeMode::Light => LIGHT.to_string(),
105            ThemeMode::Dark => DARK.to_string(),
106            ThemeMode::System => format!("{}{}", LIGHT, DARK),
107        }
108    }
109}
110
111/// Returns the CSS for a theme by name (`"light"`, `"dark"`, or `"system"`).
112pub fn get_theme_css(theme: &str) -> String {
113    theme.parse().unwrap_or(ThemeMode::System).get_css()
114}
115
116/// Returns both light and dark theme CSS combined.
117pub fn get_complete_theme_css() -> String {
118    ThemeMode::System.get_css()
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_theme_mode_from_str() {
127        assert_eq!("dark".parse::<ThemeMode>().unwrap(), ThemeMode::Dark);
128        assert_eq!("DARK".parse::<ThemeMode>().unwrap(), ThemeMode::Dark);
129        assert_eq!("light".parse::<ThemeMode>().unwrap(), ThemeMode::Light);
130        assert_eq!("system".parse::<ThemeMode>().unwrap(), ThemeMode::System);
131        assert_eq!("".parse::<ThemeMode>().unwrap(), ThemeMode::System);
132    }
133
134    #[test]
135    fn test_theme_mode_get_css() {
136        let light_css = ThemeMode::Light.get_css();
137        assert!(light_css.contains(":root"));
138        assert!(!light_css.contains("[data-theme=\"dark\"]"));
139
140        let dark_css = ThemeMode::Dark.get_css();
141        assert!(dark_css.contains("[data-theme=\"dark\"]"));
142
143        let system_css = ThemeMode::System.get_css();
144        assert!(system_css.contains(":root"));
145        assert!(system_css.contains("[data-theme=\"dark\"]"));
146    }
147
148    #[test]
149    fn test_get_theme_css() {
150        let css = get_theme_css("");
151        assert!(css.contains(":root"));
152        assert!(css.contains("[data-theme=\"dark\"]"));
153
154        let css = get_theme_css("dark");
155        assert!(css.contains("[data-theme=\"dark\"]"));
156
157        let css = get_theme_css("light");
158        assert!(css.contains(":root"));
159    }
160
161    #[test]
162    fn test_get_complete_theme_css() {
163        let css = get_complete_theme_css();
164        assert!(css.contains(":root"));
165        assert!(css.contains("[data-theme=\"dark\"]"));
166    }
167}