1use 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
16pub use app::App;
18
19pub mod app;
21pub mod components;
22pub mod layout;
23pub mod pages;
24pub mod router;
25pub mod state;
26
27pub use components::*;
29pub use layout::*;
30pub use pages::{Admin, Dashboard, Login, NotFound, Plugins, Profile, Settings};
31pub use router::Route;
32pub use state::*;
33
34#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub enum Platform {
51 Desktop,
52 Mobile,
53 Tablet,
54 Web,
55}
56
57#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct Badge {
102 pub text: String,
103 pub color: String,
104 pub background: String,
105}
106
107#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct FooterLink {
136 pub label: String,
137 pub url: String,
138 pub external: bool,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
143pub struct BreakpointConfig {
144 pub mobile: u32, pub tablet: u32, pub desktop: u32, pub large: u32, }
149
150#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
245pub enum NotificationType {
246 Info,
247 Success,
248 Warning,
249 Error,
250 System,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct NotificationAction {
256 pub label: String,
257 pub action: String,
258 pub style: ActionStyle,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub enum ActionStyle {
264 Primary,
265 Secondary,
266 Danger,
267 Link,
268}
269
270pub 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 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 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 pub async fn register_theme(&self, theme: Theme) {
313 self.themes.write().await.insert(theme.id.clone(), theme);
314 }
315
316 pub async fn get_layout(&self, layout_id: &str) -> Option<UILayout> {
318 self.layouts.read().await.get(layout_id).cloned()
319 }
320
321 pub async fn get_theme(&self, theme_id: &str) -> Option<Theme> {
323 self.themes.read().await.get(theme_id).cloned()
324 }
325
326 pub async fn set_current_layout(&self, layout: UILayout) {
328 *self.current_layout.write().await = Some(layout);
329 }
330
331 pub async fn set_current_theme(&self, theme: Theme) {
333 *self.current_theme.write().await = Some(theme);
334 }
335
336 pub async fn current_layout(&self) -> Option<UILayout> {
338 self.current_layout.read().await.clone()
339 }
340
341 pub async fn current_theme(&self) -> Option<Theme> {
343 self.current_theme.read().await.clone()
344 }
345
346 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 if !layout.for_platforms.is_empty() && !layout.for_platforms.contains(&platform) {
353 continue;
354 }
355
356 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 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 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 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 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
621pub 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}