Skip to main content

stratum_components/typography/
text.rs

1//! Styled Text component with size, weight, and color control.
2
3use crate::common::{Size, merge_classes};
4use stratum_core::render::RenderOutput;
5
6/// Font weight options.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
8pub enum FontWeight {
9    Light,
10    #[default]
11    Normal,
12    Medium,
13    Semibold,
14    Bold,
15}
16
17impl FontWeight {
18    pub fn class(&self) -> &'static str {
19        match self {
20            FontWeight::Light => "font-light",
21            FontWeight::Normal => "font-normal",
22            FontWeight::Medium => "font-medium",
23            FontWeight::Semibold => "font-semibold",
24            FontWeight::Bold => "font-bold",
25        }
26    }
27}
28
29/// Text color semantic options.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub enum TextColor {
32    #[default]
33    Default,
34    Muted,
35    Primary,
36    Destructive,
37}
38
39impl TextColor {
40    pub fn class(&self) -> &'static str {
41        match self {
42            TextColor::Default => "text-foreground",
43            TextColor::Muted => "text-muted-foreground",
44            TextColor::Primary => "text-primary",
45            TextColor::Destructive => "text-destructive",
46        }
47    }
48}
49
50/// Properties for the styled Text component.
51#[derive(Debug, Clone, PartialEq, Default)]
52pub struct TextProps {
53    pub size: Size,
54    pub weight: FontWeight,
55    pub color: TextColor,
56    pub as_element: Option<String>,
57    pub class: Option<String>,
58}
59
60pub struct Text;
61
62impl Text {
63    pub fn classes(props: &TextProps) -> String {
64        let size_cls = match props.size {
65            Size::Xs => "text-xs",
66            Size::Sm => "text-sm",
67            Size::Md => "text-base",
68            Size::Lg => "text-lg",
69            Size::Xl => "text-xl",
70        };
71
72        let computed = format!(
73            "{} {} {}",
74            size_cls,
75            props.weight.class(),
76            props.color.class(),
77        );
78        merge_classes(&computed, &props.class)
79    }
80
81    pub fn render(props: &TextProps) -> RenderOutput {
82        let tag = props.as_element.as_deref().unwrap_or("p");
83        RenderOutput::new()
84            .with_tag(tag)
85            .with_class(Self::classes(props))
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn text_default_classes() {
95        let props = TextProps::default();
96        let classes = Text::classes(&props);
97        assert!(classes.contains("text-base"));
98        assert!(classes.contains("font-normal"));
99        assert!(classes.contains("text-foreground"));
100    }
101
102    #[test]
103    fn text_custom_size_weight() {
104        let props = TextProps {
105            size: Size::Lg,
106            weight: FontWeight::Bold,
107            ..Default::default()
108        };
109        let classes = Text::classes(&props);
110        assert!(classes.contains("text-lg"));
111        assert!(classes.contains("font-bold"));
112    }
113
114    #[test]
115    fn text_muted_color() {
116        let props = TextProps {
117            color: TextColor::Muted,
118            ..Default::default()
119        };
120        let classes = Text::classes(&props);
121        assert!(classes.contains("text-muted-foreground"));
122    }
123
124    #[test]
125    fn text_default_tag_is_p() {
126        let props = TextProps::default();
127        let output = Text::render(&props);
128        assert_eq!(output.effective_tag(), "p");
129    }
130
131    #[test]
132    fn text_custom_tag() {
133        let props = TextProps {
134            as_element: Some("span".to_string()),
135            ..Default::default()
136        };
137        let output = Text::render(&props);
138        assert_eq!(output.effective_tag(), "span");
139    }
140}