qorzen_oxide/ui/
mod.rs

1// src/ui/mod.rs - UI system coordinator
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use dioxus::prelude::*;
7use serde::{Deserialize, Serialize};
8use tokio::sync::RwLock;
9use uuid::Uuid;
10
11use crate::auth::{Permission, User, UserSession};
12use crate::error::Result;
13use crate::manager::{ManagedState, Manager, ManagerStatus, PlatformRequirements};
14use crate::plugin::MenuItem;
15
16// Re-export main app component
17pub use app::App;
18
19// Module declarations
20pub mod app;
21pub mod components;
22pub mod layout;
23pub mod pages;
24pub mod router;
25pub mod state;
26
27// Re-exports for convenience
28pub use components::*;
29pub use layout::*;
30pub use pages::{Admin, Dashboard, Login, NotFound, Plugins, Profile, Settings};
31pub use router::Route;
32pub use state::*;
33
34/// UI layout configuration
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
36pub struct UILayout {
37    pub layout_id: String,
38    pub name: String,
39    pub for_roles: Vec<String>,
40    pub for_platforms: Vec<Platform>,
41    pub header: HeaderConfig,
42    pub sidebar: SidebarConfig,
43    pub main_content: MainContentConfig,
44    pub footer: FooterConfig,
45    pub breakpoints: BreakpointConfig,
46}
47
48/// Platform types for UI adaptation
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub enum Platform {
51    Desktop,
52    Mobile,
53    Tablet,
54    Web,
55}
56
57/// Header configuration
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
59pub struct HeaderConfig {
60    pub show_logo: bool,
61    pub show_user_menu: bool,
62    pub show_notifications: bool,
63    pub menu_items: Vec<MenuItem>,
64    pub quick_actions: Vec<QuickAction>,
65}
66
67/// Quick action button
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct QuickAction {
70    pub id: String,
71    pub label: String,
72    pub icon: String,
73    pub action: String,
74    pub required_permissions: Vec<Permission>,
75}
76
77/// Sidebar configuration
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
79pub struct SidebarConfig {
80    pub show: bool,
81    pub collapsible: bool,
82    pub default_collapsed: bool,
83    pub navigation_items: Vec<NavigationItem>,
84    pub plugin_panels: Vec<PluginPanel>,
85}
86
87/// Navigation item
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
89pub struct NavigationItem {
90    pub id: String,
91    pub label: String,
92    pub icon: Option<String>,
93    pub route: String,
94    pub required_permissions: Vec<Permission>,
95    pub badge: Option<Badge>,
96    pub children: Vec<NavigationItem>,
97}
98
99/// Badge for navigation items
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct Badge {
102    pub text: String,
103    pub color: String,
104    pub background: String,
105}
106
107/// Plugin panel in sidebar
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct PluginPanel {
110    pub plugin_id: String,
111    pub component_id: String,
112    pub title: String,
113    pub collapsible: bool,
114    pub default_collapsed: bool,
115}
116
117/// Main content area configuration
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
119pub struct MainContentConfig {
120    pub padding: String,
121    pub max_width: Option<String>,
122    pub background: String,
123}
124
125/// Footer configuration
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
127pub struct FooterConfig {
128    pub show: bool,
129    pub content: String,
130    pub links: Vec<FooterLink>,
131}
132
133/// Footer link
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct FooterLink {
136    pub label: String,
137    pub url: String,
138    pub external: bool,
139}
140
141/// Responsive breakpoint configuration
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
143pub struct BreakpointConfig {
144    pub mobile: u32,  // 0-767px
145    pub tablet: u32,  // 768-1023px
146    pub desktop: u32, // 1024px+
147    pub large: u32,   // 1440px+
148}
149
150/// Theme configuration
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
152pub struct Theme {
153    pub id: String,
154    pub name: String,
155    pub colors: ColorPalette,
156    pub typography: Typography,
157    pub spacing: Spacing,
158    pub shadows: Shadows,
159    pub animations: Animations,
160}
161
162/// Color palette
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
164pub struct ColorPalette {
165    pub primary: String,
166    pub secondary: String,
167    pub accent: String,
168    pub background: String,
169    pub surface: String,
170    pub error: String,
171    pub warning: String,
172    pub success: String,
173    pub info: String,
174    pub text_primary: String,
175    pub text_secondary: String,
176    pub border: String,
177}
178
179/// Typography configuration
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
181pub struct Typography {
182    pub font_family: String,
183    pub font_size_base: String,
184    pub font_weight_normal: u16,
185    pub font_weight_bold: u16,
186    pub line_height: f32,
187    pub heading_scale: f32,
188}
189
190/// Spacing configuration
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
192pub struct Spacing {
193    pub unit: String,
194    pub xs: String,
195    pub sm: String,
196    pub md: String,
197    pub lg: String,
198    pub xl: String,
199}
200
201/// Shadow configuration
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
203pub struct Shadows {
204    pub sm: String,
205    pub md: String,
206    pub lg: String,
207    pub xl: String,
208}
209
210/// Animation configuration
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
212pub struct Animations {
213    pub duration_fast: String,
214    pub duration_normal: String,
215    pub duration_slow: String,
216    pub easing: String,
217}
218
219/// Application state for UI
220#[derive(Debug, Clone)]
221pub struct AppState {
222    pub current_user: Option<User>,
223    pub current_session: Option<UserSession>,
224    pub current_layout: UILayout,
225    pub current_theme: Theme,
226    pub is_loading: bool,
227    pub error_message: Option<String>,
228    pub notifications: Vec<Notification>,
229}
230
231/// Notification
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct Notification {
234    pub id: Uuid,
235    pub title: String,
236    pub message: String,
237    pub notification_type: NotificationType,
238    pub timestamp: chrono::DateTime<chrono::Utc>,
239    pub read: bool,
240    pub actions: Vec<NotificationAction>,
241}
242
243/// Notification types
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub enum NotificationType {
246    Info,
247    Success,
248    Warning,
249    Error,
250    System,
251}
252
253/// Notification action
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct NotificationAction {
256    pub label: String,
257    pub action: String,
258    pub style: ActionStyle,
259}
260
261/// Action button styles
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub enum ActionStyle {
264    Primary,
265    Secondary,
266    Danger,
267    Link,
268}
269
270/// UI Layout Manager
271pub struct UILayoutManager {
272    state: ManagedState,
273    layouts: Arc<RwLock<HashMap<String, UILayout>>>,
274    themes: Arc<RwLock<HashMap<String, Theme>>>,
275    current_layout: Arc<RwLock<Option<UILayout>>>,
276    current_theme: Arc<RwLock<Option<Theme>>>,
277}
278
279impl std::fmt::Debug for UILayoutManager {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.debug_struct("UILayoutManager").finish()
282    }
283}
284
285impl Default for UILayoutManager {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291impl UILayoutManager {
292    /// Creates a new UI layout manager
293    pub fn new() -> Self {
294        Self {
295            state: ManagedState::new(Uuid::new_v4(), "ui_layout_manager"),
296            layouts: Arc::new(RwLock::new(HashMap::new())),
297            themes: Arc::new(RwLock::new(HashMap::new())),
298            current_layout: Arc::new(RwLock::new(None)),
299            current_theme: Arc::new(RwLock::new(None)),
300        }
301    }
302
303    /// Registers a layout
304    pub async fn register_layout(&self, layout: UILayout) {
305        self.layouts
306            .write()
307            .await
308            .insert(layout.layout_id.clone(), layout);
309    }
310
311    /// Registers a theme
312    pub async fn register_theme(&self, theme: Theme) {
313        self.themes.write().await.insert(theme.id.clone(), theme);
314    }
315
316    /// Gets a layout by ID
317    pub async fn get_layout(&self, layout_id: &str) -> Option<UILayout> {
318        self.layouts.read().await.get(layout_id).cloned()
319    }
320
321    /// Gets a theme by ID
322    pub async fn get_theme(&self, theme_id: &str) -> Option<Theme> {
323        self.themes.read().await.get(theme_id).cloned()
324    }
325
326    /// Sets the current layout
327    pub async fn set_current_layout(&self, layout: UILayout) {
328        *self.current_layout.write().await = Some(layout);
329    }
330
331    /// Sets the current theme
332    pub async fn set_current_theme(&self, theme: Theme) {
333        *self.current_theme.write().await = Some(theme);
334    }
335
336    /// Gets the current layout
337    pub async fn current_layout(&self) -> Option<UILayout> {
338        self.current_layout.read().await.clone()
339    }
340
341    /// Gets the current theme
342    pub async fn current_theme(&self) -> Option<Theme> {
343        self.current_theme.read().await.clone()
344    }
345
346    /// Finds appropriate layout for user and platform
347    pub async fn find_layout_for_user(&self, user: &User, platform: Platform) -> Option<UILayout> {
348        let layouts = self.layouts.read().await;
349
350        for layout in layouts.values() {
351            // Check if layout supports this platform
352            if !layout.for_platforms.is_empty() && !layout.for_platforms.contains(&platform) {
353                continue;
354            }
355
356            // Check if user has required roles
357            if layout.for_roles.is_empty() {
358                return Some(layout.clone());
359            }
360
361            for user_role in &user.roles {
362                if layout.for_roles.contains(&user_role.id) {
363                    return Some(layout.clone());
364                }
365            }
366        }
367
368        None
369    }
370
371    /// Gets default layout
372    pub async fn default_layout(&self) -> UILayout {
373        UILayout {
374            layout_id: "default".to_string(),
375            name: "Default Layout".to_string(),
376            for_roles: Vec::new(),
377            for_platforms: vec![Platform::Desktop, Platform::Web],
378            header: HeaderConfig {
379                show_logo: true,
380                show_user_menu: true,
381                show_notifications: true,
382                menu_items: Vec::new(),
383                quick_actions: Vec::new(),
384            },
385            sidebar: SidebarConfig {
386                show: true,
387                collapsible: true,
388                default_collapsed: false,
389                navigation_items: Vec::new(),
390                plugin_panels: Vec::new(),
391            },
392            main_content: MainContentConfig {
393                padding: "1rem".to_string(),
394                max_width: None,
395                background: "#ffffff".to_string(),
396            },
397            footer: FooterConfig {
398                show: true,
399                content: "© 2024 Qorzen".to_string(),
400                links: Vec::new(),
401            },
402            breakpoints: BreakpointConfig {
403                mobile: 768,
404                tablet: 1024,
405                desktop: 1440,
406                large: 1920,
407            },
408        }
409    }
410
411    /// Gets default theme
412    pub async fn default_theme(&self) -> Theme {
413        Theme {
414            id: "default".to_string(),
415            name: "Default Theme".to_string(),
416            colors: ColorPalette {
417                primary: "#3b82f6".to_string(),
418                secondary: "#64748b".to_string(),
419                accent: "#8b5cf6".to_string(),
420                background: "#ffffff".to_string(),
421                surface: "#f8fafc".to_string(),
422                error: "#ef4444".to_string(),
423                warning: "#f59e0b".to_string(),
424                success: "#10b981".to_string(),
425                info: "#06b6d4".to_string(),
426                text_primary: "#1e293b".to_string(),
427                text_secondary: "#64748b".to_string(),
428                border: "#e2e8f0".to_string(),
429            },
430            typography: Typography {
431                font_family: "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif"
432                    .to_string(),
433                font_size_base: "16px".to_string(),
434                font_weight_normal: 400,
435                font_weight_bold: 600,
436                line_height: 1.5,
437                heading_scale: 1.25,
438            },
439            spacing: Spacing {
440                unit: "rem".to_string(),
441                xs: "0.25rem".to_string(),
442                sm: "0.5rem".to_string(),
443                md: "1rem".to_string(),
444                lg: "1.5rem".to_string(),
445                xl: "2rem".to_string(),
446            },
447            shadows: Shadows {
448                sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)".to_string(),
449                md: "0 4px 6px -1px rgba(0, 0, 0, 0.1)".to_string(),
450                lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1)".to_string(),
451                xl: "0 25px 50px -12px rgba(0, 0, 0, 0.25)".to_string(),
452            },
453            animations: Animations {
454                duration_fast: "150ms".to_string(),
455                duration_normal: "300ms".to_string(),
456                duration_slow: "500ms".to_string(),
457                easing: "cubic-bezier(0.4, 0, 0.2, 1)".to_string(),
458            },
459        }
460    }
461}
462
463#[cfg(not(target_arch = "wasm32"))]
464#[async_trait::async_trait]
465impl Manager for UILayoutManager {
466    fn name(&self) -> &str {
467        "ui_layout_manager"
468    }
469
470    fn id(&self) -> Uuid {
471        self.state.id()
472    }
473
474    async fn initialize(&mut self) -> Result<()> {
475        self.state
476            .set_state(crate::manager::ManagerState::Initializing)
477            .await;
478
479        // Register default layout and theme
480        let default_layout = self.default_layout().await;
481        let default_theme = self.default_theme().await;
482
483        self.register_layout(default_layout.clone()).await;
484        self.register_theme(default_theme.clone()).await;
485
486        self.set_current_layout(default_layout).await;
487        self.set_current_theme(default_theme).await;
488
489        self.state
490            .set_state(crate::manager::ManagerState::Running)
491            .await;
492        Ok(())
493    }
494
495    async fn shutdown(&mut self) -> Result<()> {
496        self.state
497            .set_state(crate::manager::ManagerState::ShuttingDown)
498            .await;
499        self.state
500            .set_state(crate::manager::ManagerState::Shutdown)
501            .await;
502        Ok(())
503    }
504
505    async fn status(&self) -> ManagerStatus {
506        let mut status = self.state.status().await;
507
508        status.add_metadata(
509            "layouts_count",
510            serde_json::Value::from(self.layouts.read().await.len()),
511        );
512        status.add_metadata(
513            "themes_count",
514            serde_json::Value::from(self.themes.read().await.len()),
515        );
516
517        if let Some(layout) = self.current_layout().await {
518            status.add_metadata(
519                "current_layout",
520                serde_json::Value::String(layout.layout_id),
521            );
522        }
523
524        if let Some(theme) = self.current_theme().await {
525            status.add_metadata("current_theme", serde_json::Value::String(theme.id));
526        }
527
528        status
529    }
530
531    fn platform_requirements(&self) -> PlatformRequirements {
532        PlatformRequirements {
533            requires_filesystem: false,
534            requires_network: false,
535            requires_database: false,
536            requires_native_apis: false,
537            minimum_permissions: vec!["ui.access".to_string()],
538        }
539    }
540}
541
542#[cfg(target_arch = "wasm32")]
543#[async_trait::async_trait(?Send)]
544impl Manager for UILayoutManager {
545    fn name(&self) -> &str {
546        "ui_layout_manager"
547    }
548
549    fn id(&self) -> Uuid {
550        self.state.id()
551    }
552
553    async fn initialize(&mut self) -> Result<()> {
554        self.state
555            .set_state(crate::manager::ManagerState::Initializing)
556            .await;
557
558        // Register default layout and theme
559        let default_layout = self.default_layout().await;
560        let default_theme = self.default_theme().await;
561
562        self.register_layout(default_layout.clone()).await;
563        self.register_theme(default_theme.clone()).await;
564
565        self.set_current_layout(default_layout).await;
566        self.set_current_theme(default_theme).await;
567
568        self.state
569            .set_state(crate::manager::ManagerState::Running)
570            .await;
571        Ok(())
572    }
573
574    async fn shutdown(&mut self) -> Result<()> {
575        self.state
576            .set_state(crate::manager::ManagerState::ShuttingDown)
577            .await;
578        self.state
579            .set_state(crate::manager::ManagerState::Shutdown)
580            .await;
581        Ok(())
582    }
583
584    async fn status(&self) -> ManagerStatus {
585        let mut status = self.state.status().await;
586
587        status.add_metadata(
588            "layouts_count",
589            serde_json::Value::from(self.layouts.read().await.len()),
590        );
591        status.add_metadata(
592            "themes_count",
593            serde_json::Value::from(self.themes.read().await.len()),
594        );
595
596        if let Some(layout) = self.current_layout().await {
597            status.add_metadata(
598                "current_layout",
599                serde_json::Value::String(layout.layout_id),
600            );
601        }
602
603        if let Some(theme) = self.current_theme().await {
604            status.add_metadata("current_theme", serde_json::Value::String(theme.id));
605        }
606
607        status
608    }
609
610    fn platform_requirements(&self) -> PlatformRequirements {
611        PlatformRequirements {
612            requires_filesystem: false,
613            requires_network: false,
614            requires_database: false,
615            requires_native_apis: false,
616            minimum_permissions: vec!["ui.access".to_string()],
617        }
618    }
619}
620
621/// Main app entry point - simple wrapper for the App component
622pub fn app() -> Element {
623    rsx! { App {} }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn test_default_layout_creation() {
632        let rt = tokio::runtime::Runtime::new().unwrap();
633        rt.block_on(async {
634            let manager = UILayoutManager::new();
635            let layout = manager.default_layout().await;
636
637            assert_eq!(layout.layout_id, "default");
638            assert!(layout.header.show_logo);
639            assert!(layout.sidebar.show);
640        });
641    }
642
643    #[test]
644    fn test_platform_equality() {
645        assert_eq!(Platform::Desktop, Platform::Desktop);
646        assert_ne!(Platform::Desktop, Platform::Mobile);
647    }
648}