Skip to main content

armature_admin/
config.rs

1//! Admin configuration
2
3use serde::{Deserialize, Serialize};
4
5/// Admin dashboard configuration
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AdminConfig {
8    /// Dashboard title
9    pub title: String,
10    /// Base URL path (e.g., "/admin")
11    pub base_path: String,
12    /// Theme settings
13    pub theme: Theme,
14    /// Items per page for lists
15    pub items_per_page: usize,
16    /// Maximum items per page allowed
17    pub max_items_per_page: usize,
18    /// Require authentication
19    pub require_auth: bool,
20    /// Enable search globally
21    pub enable_search: bool,
22    /// Enable export functionality
23    pub enable_export: bool,
24    /// Date format
25    pub date_format: String,
26    /// DateTime format
27    pub datetime_format: String,
28    /// Logo URL
29    pub logo_url: Option<String>,
30    /// Favicon URL
31    pub favicon_url: Option<String>,
32    /// Custom CSS
33    pub custom_css: Option<String>,
34    /// Custom JavaScript
35    pub custom_js: Option<String>,
36    /// Footer text
37    pub footer_text: Option<String>,
38}
39
40impl AdminConfig {
41    /// Format a raw date value using the configured `date_format`.
42    ///
43    /// The value is parsed as an RFC 3339 / ISO-8601 date; on parse failure the
44    /// input is returned unchanged so unexpected shapes still render.
45    pub fn format_date(&self, raw: &str) -> String {
46        chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d")
47            .map(|d| d.format(&self.date_format).to_string())
48            .unwrap_or_else(|_| raw.to_string())
49    }
50
51    /// Format a raw datetime value using the configured `datetime_format`.
52    ///
53    /// Accepts RFC 3339 timestamps (with or without timezone); on parse failure
54    /// the input is returned unchanged.
55    pub fn format_datetime(&self, raw: &str) -> String {
56        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(raw) {
57            return dt.format(&self.datetime_format).to_string();
58        }
59        if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S") {
60            return dt.format(&self.datetime_format).to_string();
61        }
62        raw.to_string()
63    }
64}
65
66impl Default for AdminConfig {
67    fn default() -> Self {
68        Self {
69            title: "Admin Dashboard".to_string(),
70            base_path: "/admin".to_string(),
71            theme: Theme::default(),
72            items_per_page: 25,
73            max_items_per_page: 100,
74            require_auth: true,
75            enable_search: true,
76            enable_export: true,
77            date_format: "%Y-%m-%d".to_string(),
78            datetime_format: "%Y-%m-%d %H:%M:%S".to_string(),
79            logo_url: None,
80            favicon_url: None,
81            custom_css: None,
82            custom_js: None,
83            footer_text: None,
84        }
85    }
86}
87
88/// Theme configuration
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Theme {
91    /// Theme name/preset
92    pub name: ThemePreset,
93    /// Primary color
94    pub primary_color: String,
95    /// Secondary color
96    pub secondary_color: String,
97    /// Accent color
98    pub accent_color: String,
99    /// Background color
100    pub background_color: String,
101    /// Surface color (cards, etc.)
102    pub surface_color: String,
103    /// Text color
104    pub text_color: String,
105    /// Muted text color
106    pub text_muted_color: String,
107    /// Border color
108    pub border_color: String,
109    /// Success color
110    pub success_color: String,
111    /// Warning color
112    pub warning_color: String,
113    /// Error color
114    pub error_color: String,
115    /// Sidebar width
116    pub sidebar_width: String,
117    /// Border radius
118    pub border_radius: String,
119    /// Font family
120    pub font_family: String,
121}
122
123impl Default for Theme {
124    fn default() -> Self {
125        Self::dark()
126    }
127}
128
129impl Theme {
130    /// Dark theme preset
131    pub fn dark() -> Self {
132        Self {
133            name: ThemePreset::Dark,
134            primary_color: "#6366f1".to_string(),    // Indigo
135            secondary_color: "#8b5cf6".to_string(),  // Violet
136            accent_color: "#22d3ee".to_string(),     // Cyan
137            background_color: "#0f172a".to_string(), // Slate 900
138            surface_color: "#1e293b".to_string(),    // Slate 800
139            text_color: "#f8fafc".to_string(),       // Slate 50
140            text_muted_color: "#94a3b8".to_string(), // Slate 400
141            border_color: "#334155".to_string(),     // Slate 700
142            success_color: "#22c55e".to_string(),    // Green
143            warning_color: "#f59e0b".to_string(),    // Amber
144            error_color: "#ef4444".to_string(),      // Red
145            sidebar_width: "260px".to_string(),
146            border_radius: "0.5rem".to_string(),
147            font_family: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
148                .to_string(),
149        }
150    }
151
152    /// Light theme preset
153    pub fn light() -> Self {
154        Self {
155            name: ThemePreset::Light,
156            primary_color: "#4f46e5".to_string(),    // Indigo
157            secondary_color: "#7c3aed".to_string(),  // Violet
158            accent_color: "#0891b2".to_string(),     // Cyan
159            background_color: "#f8fafc".to_string(), // Slate 50
160            surface_color: "#ffffff".to_string(),    // White
161            text_color: "#0f172a".to_string(),       // Slate 900
162            text_muted_color: "#64748b".to_string(), // Slate 500
163            border_color: "#e2e8f0".to_string(),     // Slate 200
164            success_color: "#16a34a".to_string(),    // Green
165            warning_color: "#d97706".to_string(),    // Amber
166            error_color: "#dc2626".to_string(),      // Red
167            sidebar_width: "260px".to_string(),
168            border_radius: "0.5rem".to_string(),
169            font_family: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
170                .to_string(),
171        }
172    }
173
174    /// Corporate blue theme
175    pub fn corporate() -> Self {
176        Self {
177            name: ThemePreset::Corporate,
178            primary_color: "#2563eb".to_string(),    // Blue
179            secondary_color: "#1d4ed8".to_string(),  // Blue darker
180            accent_color: "#0ea5e9".to_string(),     // Sky
181            background_color: "#f1f5f9".to_string(), // Slate 100
182            surface_color: "#ffffff".to_string(),
183            text_color: "#1e293b".to_string(),       // Slate 800
184            text_muted_color: "#64748b".to_string(), // Slate 500
185            border_color: "#cbd5e1".to_string(),     // Slate 300
186            success_color: "#059669".to_string(),    // Emerald
187            warning_color: "#ca8a04".to_string(),    // Yellow
188            error_color: "#dc2626".to_string(),      // Red
189            sidebar_width: "240px".to_string(),
190            border_radius: "0.375rem".to_string(),
191            font_family:
192                "'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
193                    .to_string(),
194        }
195    }
196
197    /// Generate CSS variables
198    pub fn to_css_variables(&self) -> String {
199        format!(
200            r#":root {{
201  --admin-primary: {};
202  --admin-secondary: {};
203  --admin-accent: {};
204  --admin-bg: {};
205  --admin-surface: {};
206  --admin-text: {};
207  --admin-text-muted: {};
208  --admin-border: {};
209  --admin-success: {};
210  --admin-warning: {};
211  --admin-error: {};
212  --admin-sidebar-width: {};
213  --admin-radius: {};
214  --admin-font: {};
215}}"#,
216            self.primary_color,
217            self.secondary_color,
218            self.accent_color,
219            self.background_color,
220            self.surface_color,
221            self.text_color,
222            self.text_muted_color,
223            self.border_color,
224            self.success_color,
225            self.warning_color,
226            self.error_color,
227            self.sidebar_width,
228            self.border_radius,
229            self.font_family,
230        )
231    }
232}
233
234/// Theme presets
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
236pub enum ThemePreset {
237    #[default]
238    Dark,
239    Light,
240    Corporate,
241    Custom,
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_default_config() {
250        let config = AdminConfig::default();
251        assert_eq!(config.title, "Admin Dashboard");
252        assert_eq!(config.base_path, "/admin");
253        assert_eq!(config.items_per_page, 25);
254    }
255
256    #[test]
257    fn test_theme_presets() {
258        let dark = Theme::dark();
259        assert_eq!(dark.name, ThemePreset::Dark);
260
261        let light = Theme::light();
262        assert_eq!(light.name, ThemePreset::Light);
263    }
264
265    #[test]
266    fn test_css_variables() {
267        let theme = Theme::dark();
268        let css = theme.to_css_variables();
269        assert!(css.contains("--admin-primary:"));
270        assert!(css.contains("--admin-bg:"));
271    }
272}