adui_dioxus/components/
card.rs

1use crate::components::config_provider::ComponentSize;
2use crate::components::skeleton::Skeleton;
3use dioxus::prelude::*;
4
5/// Props for the Card component (MVP subset).
6#[derive(Props, Clone, PartialEq)]
7pub struct CardProps {
8    /// Optional card title rendered in the header.
9    #[props(optional)]
10    pub title: Option<Element>,
11    /// Optional extra content rendered in the header's right area.
12    #[props(optional)]
13    pub extra: Option<Element>,
14    /// Whether to show a border around the card.
15    #[props(default = true)]
16    pub bordered: bool,
17    /// Visual density of the card paddings and typography.
18    #[props(optional)]
19    pub size: Option<ComponentSize>,
20    /// Loading state. When true, the body renders a simple skeleton instead
21    /// of the provided children.
22    #[props(default)]
23    pub loading: bool,
24    /// Whether the card should have a hover effect.
25    #[props(default)]
26    pub hoverable: bool,
27    /// Extra class name for the root element.
28    #[props(optional)]
29    pub class: Option<String>,
30    /// Inline style for the root element.
31    #[props(optional)]
32    pub style: Option<String>,
33    /// Card body content.
34    pub children: Element,
35}
36
37fn build_card_classes(
38    bordered: bool,
39    size: Option<ComponentSize>,
40    hoverable: bool,
41    extra_class: Option<String>,
42) -> String {
43    let mut class_list = vec!["adui-card".to_string()];
44    if bordered {
45        class_list.push("adui-card-bordered".into());
46    }
47    if hoverable {
48        class_list.push("adui-card-hoverable".into());
49    }
50    if let Some(sz) = size {
51        match sz {
52            ComponentSize::Small => class_list.push("adui-card-sm".into()),
53            ComponentSize::Middle => {}
54            ComponentSize::Large => class_list.push("adui-card-lg".into()),
55        }
56    }
57    if let Some(extra) = extra_class {
58        class_list.push(extra);
59    }
60    class_list.join(" ")
61}
62
63/// Ant Design flavored Card (MVP: basic card with optional title/extra/loading).
64#[component]
65pub fn Card(props: CardProps) -> Element {
66    let CardProps {
67        title,
68        extra,
69        bordered,
70        size,
71        loading,
72        hoverable,
73        class,
74        style,
75        children,
76    } = props;
77
78    let class_attr = build_card_classes(bordered, size, hoverable, class);
79    let style_attr = style.unwrap_or_default();
80
81    rsx! {
82        div { class: "{class_attr}", style: "{style_attr}",
83            if title.is_some() || extra.is_some() {
84                div { class: "adui-card-head",
85                    if let Some(head_title) = title {
86                        div { class: "adui-card-head-title", {head_title} }
87                    }
88                    if let Some(head_extra) = extra {
89                        div { class: "adui-card-head-extra", {head_extra} }
90                    }
91                }
92            }
93
94            div { class: "adui-card-body",
95                if loading {
96                    Skeleton {
97                        loading: Some(true),
98                        active: true,
99                        paragraph_rows: Some(3),
100                    }
101                } else {
102                    {children}
103                }
104            }
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn build_card_classes_includes_flags() {
115        let classes =
116            build_card_classes(true, Some(ComponentSize::Small), true, Some("extra".into()));
117
118        assert!(classes.contains("adui-card"));
119        assert!(classes.contains("adui-card-bordered"));
120        assert!(classes.contains("adui-card-hoverable"));
121        assert!(classes.contains("adui-card-sm"));
122        assert!(classes.contains("extra"));
123    }
124
125    #[test]
126    fn build_card_classes_handles_minimal_case() {
127        let classes = build_card_classes(false, None, false, None);
128        assert_eq!(classes, "adui-card");
129    }
130
131    #[test]
132    fn build_card_classes_bordered_only() {
133        let classes = build_card_classes(true, None, false, None);
134        assert!(classes.contains("adui-card"));
135        assert!(classes.contains("adui-card-bordered"));
136        assert!(!classes.contains("adui-card-hoverable"));
137    }
138
139    #[test]
140    fn build_card_classes_hoverable_only() {
141        let classes = build_card_classes(false, None, true, None);
142        assert!(classes.contains("adui-card"));
143        assert!(!classes.contains("adui-card-bordered"));
144        assert!(classes.contains("adui-card-hoverable"));
145    }
146
147    #[test]
148    fn build_card_classes_size_small() {
149        let classes = build_card_classes(false, Some(ComponentSize::Small), false, None);
150        assert!(classes.contains("adui-card"));
151        assert!(classes.contains("adui-card-sm"));
152    }
153
154    #[test]
155    fn build_card_classes_size_middle() {
156        let classes = build_card_classes(false, Some(ComponentSize::Middle), false, None);
157        assert!(classes.contains("adui-card"));
158        assert!(!classes.contains("adui-card-sm"));
159        assert!(!classes.contains("adui-card-lg"));
160    }
161
162    #[test]
163    fn build_card_classes_size_large() {
164        let classes = build_card_classes(false, Some(ComponentSize::Large), false, None);
165        assert!(classes.contains("adui-card"));
166        assert!(classes.contains("adui-card-lg"));
167    }
168
169    #[test]
170    fn build_card_classes_all_combinations() {
171        // Bordered + Hoverable
172        let classes = build_card_classes(true, None, true, None);
173        assert!(classes.contains("adui-card-bordered"));
174        assert!(classes.contains("adui-card-hoverable"));
175
176        // Bordered + Small
177        let classes = build_card_classes(true, Some(ComponentSize::Small), false, None);
178        assert!(classes.contains("adui-card-bordered"));
179        assert!(classes.contains("adui-card-sm"));
180
181        // Hoverable + Large
182        let classes = build_card_classes(false, Some(ComponentSize::Large), true, None);
183        assert!(classes.contains("adui-card-hoverable"));
184        assert!(classes.contains("adui-card-lg"));
185
186        // All flags
187        let classes = build_card_classes(
188            true,
189            Some(ComponentSize::Small),
190            true,
191            Some("custom-class".into()),
192        );
193        assert!(classes.contains("adui-card"));
194        assert!(classes.contains("adui-card-bordered"));
195        assert!(classes.contains("adui-card-hoverable"));
196        assert!(classes.contains("adui-card-sm"));
197        assert!(classes.contains("custom-class"));
198    }
199
200    #[test]
201    fn build_card_classes_with_extra_class() {
202        let classes = build_card_classes(false, None, false, Some("my-custom-class".into()));
203        assert!(classes.contains("adui-card"));
204        assert!(classes.contains("my-custom-class"));
205    }
206
207    #[test]
208    fn build_card_classes_multiple_extra_classes() {
209        // Note: The function only accepts one extra class string, but we can test it
210        let classes = build_card_classes(false, None, false, Some("class1 class2".into()));
211        assert!(classes.contains("adui-card"));
212        assert!(classes.contains("class1 class2"));
213    }
214
215    #[test]
216    fn build_card_classes_empty_extra_class() {
217        let classes = build_card_classes(false, None, false, Some(String::new()));
218        assert!(classes.contains("adui-card"));
219        // Empty string should still be added
220        let parts: Vec<&str> = classes.split(' ').collect();
221        assert!(parts.len() >= 1);
222    }
223
224    #[test]
225    fn card_props_defaults() {
226        // CardProps requires children
227        // bordered defaults to true
228        // loading defaults to false
229        // hoverable defaults to false
230    }
231}