Skip to main content

dioxus_element_plug/components/
card.rs

1use dioxus::prelude::*;
2
3// Card CSS class constants
4pub const CARD: &str = "el-card";
5pub const CARD_HEADER: &str = "el-card__header";
6pub const CARD_BODY: &str = "el-card__body";
7pub const CARD_SHADOW_ALWAYS: &str = "el-card--always";
8pub const CARD_SHADOW_HOVER: &str = "el-card--hover";
9
10/// Card props for the theme-chalk styled card component
11#[derive(Props, Clone, PartialEq)]
12pub struct CardProps {
13    /// Card content
14    pub children: Element,
15
16    /// Card header content
17    #[props(default)]
18    pub header: Option<String>,
19
20    /// Card shadow behavior
21    #[props(default = "hover".to_string())]
22    pub shadow: String,
23
24    /// Card body style (inline CSS)
25    #[props(default)]
26    pub body_style: Option<String>,
27
28    /// Additional CSS classes
29    #[props(default)]
30    pub class: Option<String>,
31
32    /// Inline styles for the card
33    #[props(default)]
34    pub style: Option<String>,
35}
36
37/// A card component for organizing content
38///
39/// This component provides a container with optional header and customizable
40/// shadow behavior for organizing related content.
41///
42/// ## Example
43///
44/// ```rust,ignore
45/// use dioxus_element_plug::components::card::Card;
46///
47/// rsx! {
48///     Card {
49///         header: Some("Card Title".to_string()),
50///         shadow: "always".to_string(),
51///         
52///         div {
53///             "Card content goes here"
54///         }
55///     }
56/// }
57/// ```
58#[component]
59pub fn Card(props: CardProps) -> Element {
60    let mut class_names = vec!["el-card".to_string()];
61    
62    class_names.push(format!("is-{}-shadow", props.shadow));
63    
64    if let Some(ref custom_class) = props.class {
65        class_names.push(custom_class.to_string());
66    }
67    
68    let class_string = class_names.join(" ");
69    let card_style = props.style.unwrap_or_default();
70    let body_style = props.body_style.unwrap_or_default();
71    
72    rsx! {
73        div {
74            class: "{class_string}",
75            style: "{card_style}",
76            
77            if let Some(ref header_text) = props.header {
78                div {
79                    class: "el-card__header",
80                    "{header_text}"
81                }
82            }
83            
84            div {
85                class: "el-card__body",
86                style: "{body_style}",
87                {props.children}
88            }
89        }
90    }
91}
92
93/// Panel component for grouping controls
94#[derive(Props, Clone, PartialEq)]
95pub struct PanelProps {
96    /// Panel content
97    pub children: Element,
98
99    /// Panel title
100    #[props(default)]
101    pub title: Option<String>,
102
103    /// Panel subtitle
104    #[props(default)]
105    pub subtitle: Option<String>,
106
107    /// Panel type
108    #[props(default = "default".to_string())]
109    pub panel_type: String,
110
111    /// Whether the panel is collapsible
112    #[props(default = false)]
113    pub collapsible: bool,
114
115    /// Whether the panel is collapsed
116    #[props(default = false)]
117    pub collapsed: bool,
118
119    /// Additional CSS classes
120    #[props(default)]
121    pub class: Option<String>,
122
123    /// Inline styles
124    #[props(default)]
125    pub style: Option<String>,
126
127    /// Toggle event handler
128    #[props(default)]
129    pub on_toggle: Option<EventHandler<MouseEvent>>,
130}
131
132/// A panel component for organizing form controls
133///
134/// This component provides a collapsible container for organizing
135/// related form controls or content sections.
136#[component]
137pub fn Panel(props: PanelProps) -> Element {
138    let mut class_names = vec!["el-panel".to_string()];
139    
140    class_names.push(format!("el-panel--{}", props.panel_type));
141    
142    if props.collapsible {
143        class_names.push("is-collapsible".to_string());
144    }
145    
146    if props.collapsed {
147        class_names.push("is-collapsed".to_string());
148    }
149    
150    if let Some(ref custom_class) = props.class {
151        class_names.push(custom_class.to_string());
152    }
153    
154    let class_string = class_names.join(" ");
155    let style_string = props.style.unwrap_or_default();
156    
157    rsx! {
158        div {
159            class: "{class_string}",
160            style: "{style_string}",
161            
162            if props.collapsible {
163                div {
164                    class: "el-panel__header is-clickable",
165                    onclick: move |event| {
166                        if let Some(handler) = props.on_toggle {
167                            handler.call(event);
168                        }
169                    },
170                    
171                    if let Some(ref title_text) = props.title {
172                        h3 {
173                            class: "el-panel__title",
174                            "{title_text}"
175                        }
176                    }
177                    
178                    if let Some(ref sub_text) = props.subtitle {
179                        span {
180                            class: "el-panel__subtitle",
181                            "{sub_text}"
182                        }
183                    }
184                    
185                    i {
186                        class: if props.collapsed { "el-icon-arrow-down" } else { "el-icon-arrow-up" }
187                    }
188                }
189            } else if let Some(ref title_text) = props.title {
190                div {
191                    class: "el-panel__header",
192                    
193                    h3 {
194                        class: "el-panel__title",
195                        "{title_text}"
196                    }
197                    
198                    if let Some(ref sub_text) = props.subtitle {
199                        span {
200                            class: "el-panel__subtitle",
201                            "{sub_text}"
202                        }
203                    }
204                }
205            }
206            
207            if !props.collapsible || !props.collapsed {
208                div {
209                    class: "el-panel__body",
210                    {props.children}
211                }
212            }
213        }
214    }
215}
216
217/// Box component for flexible layouts
218#[derive(Props, Clone, PartialEq)]
219pub struct BoxProps {
220    /// Box content
221    pub children: Element,
222
223    /// Box padding
224    #[props(default)]
225    pub padding: Option<String>,
226
227    /// Box margin
228    #[props(default)]
229    pub margin: Option<String>,
230
231    /// Box border radius
232    #[props(default)]
233    pub border_radius: Option<String>,
234
235    /// Box background color
236    #[props(default)]
237    pub background: Option<String>,
238
239    /// Box border
240    #[props(default)]
241    pub border: Option<String>,
242
243    /// Box elevation/shadow
244    #[props(default)]
245    pub elevation: Option<u32>,
246
247    /// Additional CSS classes
248    #[props(default)]
249    pub class: Option<String>,
250
251    /// Inline styles
252    #[props(default)]
253    pub style: Option<String>,
254}
255
256/// A flexible box component for layouts
257///
258/// This component provides a customizable container with support for
259/// padding, margin, borders, shadows, and background styling.
260#[component]
261pub fn Box(props: BoxProps) -> Element {
262    let mut styles = vec![props.style.unwrap_or_default()];
263    
264    if let Some(padding) = props.padding {
265        styles.push(format!("padding: {};", padding));
266    }
267    
268    if let Some(margin) = props.margin {
269        styles.push(format!("margin: {};", margin));
270    }
271    
272    if let Some(border_radius) = props.border_radius {
273        styles.push(format!("border-radius: {};", border_radius));
274    }
275    
276    if let Some(background) = props.background {
277        styles.push(format!("background: {};", background));
278    }
279    
280    if let Some(border) = props.border {
281        styles.push(format!("border: {};", border));
282    }
283    
284    if let Some(elevation) = props.elevation {
285        let shadow = match elevation {
286            0 => "none",
287            1 => "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)",
288            2 => "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12)",
289            3 => "0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10)",
290            4 => "0 15px 25px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.05)",
291            5 => "0 20px 40px rgba(0,0,0,0.20)",
292            _ => "0 25px 50px rgba(0,0,0,0.25)",
293        };
294        styles.push(format!("box-shadow: {};", shadow));
295    }
296    
297    let style_string = styles.join("");
298    
299    let mut class_names = vec!["el-box".to_string()];
300    
301    if let Some(ref custom_class) = props.class {
302        class_names.push(custom_class.to_string());
303    }
304    
305    if let Some(elevation) = props.elevation {
306        class_names.push(format!("el-box--elevation-{}", elevation));
307    }
308    
309    let class_string = class_names.join(" ");
310    
311    rsx! {
312        div {
313            class: "{class_string}",
314            style: "{style_string}",
315            {props.children}
316        }
317    }
318}
319
320/// Accordion item for collapsible content
321#[derive(Clone, PartialEq)]
322pub struct AccordionItem {
323    /// Item title
324    pub title: String,
325    /// Item content
326    pub content: String,
327    /// Whether the item is disabled
328    pub disabled: bool,
329}
330
331/// Accordion component for collapsible sections
332#[derive(Props, Clone, PartialEq)]
333pub struct AccordionProps {
334    /// Accordion items
335    pub items: Vec<AccordionItem>,
336
337    /// Active item index (for controlled mode)
338    #[props(default)]
339    pub active_index: Option<usize>,
340
341    /// Whether multiple items can be open simultaneously
342    #[props(default = false)]
343    pub accordion: bool,
344
345    /// Whether to show animation
346    #[props(default = true)]
347    pub animated: bool,
348
349    /// Additional CSS classes
350    #[props(default)]
351    pub class: Option<String>,
352
353    /// Inline styles
354    #[props(default)]
355    pub style: Option<String>,
356
357    /// Change event handler
358    #[props(default)]
359    pub on_change: Option<EventHandler<usize>>,
360}
361
362/// An accordion component for organizing collapsible content
363///
364/// This component provides a way to organize content in collapsible sections
365/// that can be expanded or collapsed by the user.
366#[component]
367pub fn Accordion(props: AccordionProps) -> Element {
368    let mut class_names = vec!["el-accordion".to_string()];
369    
370    if props.accordion {
371        class_names.push("el-accordion--multiple".to_string());
372    }
373    
374    if props.animated {
375        class_names.push("el-accordion--animated".to_string());
376    }
377    
378    if let Some(ref custom_class) = props.class {
379        class_names.push(custom_class.to_string());
380    }
381    
382    let class_string = class_names.join(" ");
383    let style_string = props.style.unwrap_or_default();
384    
385    rsx! {
386        div {
387            class: "{class_string}",
388            style: "{style_string}",
389            
390            for (index, item) in props.items.iter().enumerate() {
391                div {
392                    class: "el-accordion__item",
393                    
394                    if !props.accordion || props.active_index == Some(index) {
395                        div {
396                            class: "el-accordion__header",
397                            
398                            button {
399                                class: "el-accordion__button",
400                                r#type: "button",
401                                disabled: item.disabled,
402                                onclick: move |_| {
403                                    if let Some(handler) = props.on_change {
404                                        handler.call(index);
405                                    }
406                                },
407                                
408                                span {
409                                    class: "el-accordion__title",
410                                    "{item.title}"
411                                }
412                                
413                                i {
414                                    class: "el-icon-arrow-down el-accordion__icon"
415                                }
416                            }
417                        }
418                        
419                        div {
420                            class: "el-accordion__content",
421                            
422                            div {
423                                class: "el-accordion__body",
424                                "{item.content}"
425                            }
426                        }
427                    } else {
428                        div {
429                            class: "el-accordion__header",
430                            
431                            button {
432                                class: "el-accordion__button",
433                                r#type: "button",
434                                disabled: item.disabled,
435                                onclick: move |_| {
436                                    if let Some(handler) = props.on_change {
437                                        handler.call(index);
438                                    }
439                                },
440                                
441                                span {
442                                    class: "el-accordion__title",
443                                    "{item.title}"
444                                }
445                                
446                                i {
447                                    class: "el-icon-arrow-right el-accordion__icon"
448                                }
449                            }
450                        }
451                    }
452                }
453            }
454        }
455    }
456}