Skip to main content

stratum_components/data_display/
card.rs

1//! Styled Card compound component: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter.
2
3use crate::common::merge_classes;
4use stratum_core::render::RenderOutput;
5
6// --- Card ---
7
8#[derive(Debug, Clone, PartialEq, Default)]
9pub struct CardProps {
10    pub class: Option<String>,
11}
12
13pub struct Card;
14
15impl Card {
16    const BASE: &'static str = "rounded-xl border bg-card text-card-foreground shadow";
17
18    pub fn classes(props: &CardProps) -> String {
19        merge_classes(Self::BASE, &props.class)
20    }
21
22    pub fn render(props: &CardProps) -> RenderOutput {
23        RenderOutput::new()
24            .with_tag("div")
25            .with_class(Self::classes(props))
26    }
27}
28
29// --- CardHeader ---
30
31#[derive(Debug, Clone, PartialEq, Default)]
32pub struct CardHeaderProps {
33    pub class: Option<String>,
34}
35
36pub struct CardHeader;
37
38impl CardHeader {
39    const BASE: &'static str = "flex flex-col space-y-1.5 p-6";
40
41    pub fn classes(props: &CardHeaderProps) -> String {
42        merge_classes(Self::BASE, &props.class)
43    }
44
45    pub fn render(props: &CardHeaderProps) -> RenderOutput {
46        RenderOutput::new()
47            .with_tag("div")
48            .with_class(Self::classes(props))
49    }
50}
51
52// --- CardTitle ---
53
54#[derive(Debug, Clone, PartialEq, Default)]
55pub struct CardTitleProps {
56    pub class: Option<String>,
57}
58
59pub struct CardTitle;
60
61impl CardTitle {
62    const BASE: &'static str = "font-semibold leading-none tracking-tight";
63
64    pub fn classes(props: &CardTitleProps) -> String {
65        merge_classes(Self::BASE, &props.class)
66    }
67
68    pub fn render(props: &CardTitleProps) -> RenderOutput {
69        RenderOutput::new()
70            .with_tag("h3")
71            .with_class(Self::classes(props))
72    }
73}
74
75// --- CardDescription ---
76
77#[derive(Debug, Clone, PartialEq, Default)]
78pub struct CardDescriptionProps {
79    pub class: Option<String>,
80}
81
82pub struct CardDescription;
83
84impl CardDescription {
85    const BASE: &'static str = "text-sm text-muted-foreground";
86
87    pub fn classes(props: &CardDescriptionProps) -> String {
88        merge_classes(Self::BASE, &props.class)
89    }
90
91    pub fn render(props: &CardDescriptionProps) -> RenderOutput {
92        RenderOutput::new()
93            .with_tag("p")
94            .with_class(Self::classes(props))
95    }
96}
97
98// --- CardContent ---
99
100#[derive(Debug, Clone, PartialEq, Default)]
101pub struct CardContentProps {
102    pub class: Option<String>,
103}
104
105pub struct CardContent;
106
107impl CardContent {
108    const BASE: &'static str = "p-6 pt-0";
109
110    pub fn classes(props: &CardContentProps) -> String {
111        merge_classes(Self::BASE, &props.class)
112    }
113
114    pub fn render(props: &CardContentProps) -> RenderOutput {
115        RenderOutput::new()
116            .with_tag("div")
117            .with_class(Self::classes(props))
118    }
119}
120
121// --- CardFooter ---
122
123#[derive(Debug, Clone, PartialEq, Default)]
124pub struct CardFooterProps {
125    pub class: Option<String>,
126}
127
128pub struct CardFooter;
129
130impl CardFooter {
131    const BASE: &'static str = "flex items-center p-6 pt-0";
132
133    pub fn classes(props: &CardFooterProps) -> String {
134        merge_classes(Self::BASE, &props.class)
135    }
136
137    pub fn render(props: &CardFooterProps) -> RenderOutput {
138        RenderOutput::new()
139            .with_tag("div")
140            .with_class(Self::classes(props))
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn card_classes() {
150        let props = CardProps::default();
151        let classes = Card::classes(&props);
152        assert!(classes.contains("rounded-xl"));
153        assert!(classes.contains("bg-card"));
154    }
155
156    #[test]
157    fn card_header_tag() {
158        let props = CardHeaderProps::default();
159        let output = CardHeader::render(&props);
160        assert_eq!(output.effective_tag(), "div");
161        assert!(output.class_string().contains("p-6"));
162    }
163
164    #[test]
165    fn card_title_tag() {
166        let props = CardTitleProps::default();
167        let output = CardTitle::render(&props);
168        assert_eq!(output.effective_tag(), "h3");
169    }
170
171    #[test]
172    fn card_description_tag() {
173        let props = CardDescriptionProps::default();
174        let output = CardDescription::render(&props);
175        assert_eq!(output.effective_tag(), "p");
176    }
177
178    #[test]
179    fn card_content_classes() {
180        let props = CardContentProps::default();
181        let classes = CardContent::classes(&props);
182        assert!(classes.contains("p-6"));
183    }
184
185    #[test]
186    fn card_footer_classes() {
187        let props = CardFooterProps::default();
188        let classes = CardFooter::classes(&props);
189        assert!(classes.contains("flex"));
190    }
191}