qorzen_oxide/ui/layout/
main_layout.rs1use dioxus::prelude::*;
4
5use crate::ui::{
6 layout::{Footer, Header, Sidebar},
7 state::{ui::use_mobile_menu, ui::use_sidebar, use_app_state},
8};
9
10#[component]
12pub fn Layout(#[props] children: Element) -> Element {
13 let app_state = use_app_state();
14 let (sidebar_collapsed, toggle_sidebar, _) = use_sidebar();
15 let (mobile_menu_open, toggle_mobile_menu, set_mobile_menu_open) = use_mobile_menu();
16
17 let close_mobile_menu = use_callback(move |_: Event<MouseData>| {
19 set_mobile_menu_open(false);
20 });
21
22 rsx! {
23 div {
24 class: "min-h-screen bg-gray-50 flex flex-col",
25
26 Header {
28 on_menu_toggle: toggle_mobile_menu,
29 on_sidebar_toggle: toggle_sidebar
30 }
31
32 div {
34 class: "flex flex-1 overflow-hidden",
35
36 Sidebar {
38 collapsed: sidebar_collapsed,
39 mobile_open: mobile_menu_open,
40 on_close: close_mobile_menu
41 }
42
43 main {
45 class: format!(
46 "flex-1 overflow-y-auto transition-all duration-200 ease-in-out {}",
47 if sidebar_collapsed {
48 "lg:ml-16"
49 } else {
50 "lg:ml-64"
51 }
52 ),
53
54 div {
56 class: "container mx-auto px-4 sm:px-6 lg:px-8 py-6 max-w-7xl",
57
58 if let Some(error) = &app_state.error_message {
60 div {
61 class: "mb-6 bg-red-50 border border-red-200 rounded-md p-4",
62 div {
63 class: "flex items-center",
64 div {
65 class: "flex-shrink-0",
66 svg {
67 class: "h-5 w-5 text-red-400",
68 xmlns: "http://www.w3.org/2000/svg",
69 view_box: "0 0 20 20",
70 fill: "currentColor",
71 path {
72 fill_rule: "evenodd",
73 d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z",
74 clip_rule: "evenodd"
75 }
76 }
77 }
78 div {
79 class: "ml-3",
80 h3 {
81 class: "text-sm font-medium text-red-800",
82 "Error"
83 }
84 div {
85 class: "mt-2 text-sm text-red-700",
86 "{error}"
87 }
88 }
89 }
90 }
91 }
92
93 if app_state.is_loading {
95 div {
96 class: "mb-6 bg-blue-50 border border-blue-200 rounded-md p-4",
97 div {
98 class: "flex items-center",
99 div {
100 class: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600 mr-3"
101 }
102 span {
103 class: "text-blue-800 text-sm font-medium",
104 "Loading..."
105 }
106 }
107 }
108 }
109
110 {children}
112 }
113 }
114
115 if mobile_menu_open {
117 div {
118 class: "fixed inset-0 z-40 lg:hidden",
119 onclick: close_mobile_menu,
120
121 div {
123 class: "fixed inset-0 bg-gray-600 bg-opacity-75 transition-opacity"
124 }
125 }
126 }
127 }
128
129 Footer {}
131 }
132 }
133}
134
135#[component]
137pub fn ResponsiveLayout(
138 #[props(default = "".to_string())] mobile_class: String,
139 #[props(default = "".to_string())] tablet_class: String,
140 #[props(default = "".to_string())] desktop_class: String,
141 children: Element,
142) -> Element {
143 rsx! {
144 div {
145 class: format!(
146 "{} sm:{} md:{} lg:{}",
147 mobile_class,
148 tablet_class,
149 tablet_class,
150 desktop_class
151 ),
152 {children}
153 }
154 }
155}
156
157#[component]
159pub fn ContentWrapper(
160 #[props(default = "".to_string())] class: String,
161 children: Element,
162) -> Element {
163 rsx! {
164 div {
165 class: format!(
166 "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 {}",
167 class
168 ),
169 {children}
170 }
171 }
172}
173
174#[component]
176pub fn PageHeader(
177 title: String,
178 #[props(default = None)] subtitle: Option<String>,
179 #[props(default = None)] actions: Option<Element>,
180) -> Element {
181 rsx! {
182 div {
183 class: "mb-8",
184 div {
185 class: "md:flex md:items-center md:justify-between",
186 div {
187 class: "flex-1 min-w-0",
188 h1 {
189 class: "text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate",
190 "{title}"
191 }
192 if let Some(subtitle) = subtitle {
193 p {
194 class: "mt-1 text-sm text-gray-500",
195 "{subtitle}"
196 }
197 }
198 }
199 if let Some(actions) = actions {
200 div {
201 class: "mt-4 flex md:mt-0 md:ml-4",
202 {actions}
203 }
204 }
205 }
206 }
207 }
208}
209
210#[component]
212pub fn Card(
213 #[props(default = "".to_string())] class: String,
214 #[props(default = None)] title: Option<String>,
215 children: Element,
216) -> Element {
217 rsx! {
218 div {
219 class: format!(
220 "bg-white overflow-hidden shadow rounded-lg {}",
221 class
222 ),
223 if let Some(title) = title {
224 div {
225 class: "px-4 py-5 sm:px-6 border-b border-gray-200",
226 h3 {
227 class: "text-lg leading-6 font-medium text-gray-900",
228 "{title}"
229 }
230 }
231 }
232 div {
233 class: "px-4 py-5 sm:p-6",
234 {children}
235 }
236 }
237 }
238}
239
240#[component]
242pub fn Grid(
243 #[props(default = 1)] cols: u32,
244 #[props(default = 2)] sm_cols: u32,
245 #[props(default = 3)] md_cols: u32,
246 #[props(default = 4)] lg_cols: u32,
247 #[props(default = "gap-6".to_string())] gap: String,
248 #[props(default = "".to_string())] class: String,
249 children: Element,
250) -> Element {
251 rsx! {
252 div {
253 class: format!(
254 "grid grid-cols-{} sm:grid-cols-{} md:grid-cols-{} lg:grid-cols-{} {} {}",
255 cols, sm_cols, md_cols, lg_cols, gap, class
256 ),
257 {children}
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use dioxus::prelude::*;
266
267 #[test]
268 fn test_layout_component_creation() {
269 let _layout = rsx! {
271 Layout {
272 div { "Test content" }
273 }
274 };
275 }
276
277 #[test]
278 fn test_card_component_creation() {
279 let _card = rsx! {
280 Card {
281 title: "Test Card".to_string(),
282 div { "Card content" }
283 }
284 };
285 }
286
287 #[test]
288 fn test_page_header_component_creation() {
289 let _header = rsx! {
290 PageHeader {
291 title: "Test Page".to_string(),
292 subtitle: Some("Test subtitle".to_string())
293 }
294 };
295 }
296}