Skip to main content

dioxus_ui_system/atoms/
label.rs

1//! Label atom component
2//!
3//! Text label with typography and theming support.
4
5use crate::styles::Style;
6use crate::theme::tokens::Color;
7use crate::theme::{use_style, use_theme};
8use dioxus::prelude::*;
9
10/// Text sizes (typography presets)
11#[derive(Default, Clone, PartialEq)]
12pub enum TextSize {
13    ExtraSmall,
14    Small,
15    #[default]
16    Base,
17    Large,
18    ExtraLarge,
19    H1,
20    H2,
21    H3,
22    H4,
23}
24
25/// Text weights
26#[derive(Default, Clone, PartialEq)]
27pub enum TextWeight {
28    #[default]
29    Normal,
30    Medium,
31    Semibold,
32    Bold,
33}
34
35/// Text colors
36#[derive(Default, Clone, PartialEq)]
37pub enum TextColor {
38    #[default]
39    Default,
40    Muted,
41    Primary,
42    Secondary,
43    Destructive,
44    Success,
45    Warning,
46    Inverse,
47    Custom(Color),
48}
49
50/// Label/Text properties
51#[derive(Props, Clone, PartialEq)]
52pub struct LabelProps {
53    /// Text content
54    pub children: Element,
55    /// Typography size
56    #[props(default)]
57    pub size: TextSize,
58    /// Font weight
59    #[props(default)]
60    pub weight: TextWeight,
61    /// Text color
62    #[props(default)]
63    pub color: TextColor,
64    /// Custom inline styles
65    #[props(default)]
66    pub style: Option<String>,
67    /// Custom class name
68    #[props(default)]
69    pub class: Option<String>,
70    /// HTML element to render (defaults to span)
71    #[props(default)]
72    pub as_element: LabelElement,
73    /// Text alignment
74    #[props(default)]
75    pub align: TextAlign,
76    /// Truncate with ellipsis
77    #[props(default)]
78    pub truncate: bool,
79    /// For element (associates label with form input)
80    #[props(default)]
81    pub html_for: Option<String>,
82    /// Line clamp (number of lines)
83    #[props(default)]
84    pub line_clamp: Option<u8>,
85}
86
87/// HTML element options for Label
88#[derive(Default, Clone, PartialEq)]
89pub enum LabelElement {
90    #[default]
91    Span,
92    P,
93    Div,
94    Label,
95    H1,
96    H2,
97    H3,
98    H4,
99    H5,
100    H6,
101    Strong,
102    Em,
103    Small,
104}
105
106/// Text alignment
107#[derive(Default, Clone, PartialEq)]
108pub enum TextAlign {
109    #[default]
110    Left,
111    Center,
112    Right,
113    Justify,
114}
115
116impl TextAlign {
117    fn as_str(&self) -> &'static str {
118        match self {
119            TextAlign::Left => "left",
120            TextAlign::Center => "center",
121            TextAlign::Right => "right",
122            TextAlign::Justify => "justify",
123        }
124    }
125}
126
127/// Label/Text atom component
128///
129/// # Example
130/// ```rust,ignore
131/// use dioxus_ui_system::atoms::{Label, TextSize, TextWeight};
132///
133/// rsx! {
134///     Label {
135///         size: TextSize::H1,
136///         weight: TextWeight::Bold,
137///         "Hello, World!"
138///     }
139/// }
140/// ```
141#[component]
142pub fn Label(props: LabelProps) -> Element {
143    let _theme = use_theme();
144
145    let size = props.size.clone();
146    let weight = props.weight.clone();
147    let color = props.color.clone();
148    let align = props.align.clone();
149    let truncate = props.truncate;
150    let line_clamp = props.line_clamp;
151
152    // Memoized styles
153    let style = use_style(move |t| {
154        let typography_size = match size {
155            TextSize::ExtraSmall => "xs",
156            TextSize::Small => "sm",
157            TextSize::Base => "base",
158            TextSize::Large => "lg",
159            TextSize::ExtraLarge => "xl",
160            TextSize::H1 => "h1",
161            TextSize::H2 => "h2",
162            TextSize::H3 => "h3",
163            TextSize::H4 => "h4",
164        };
165
166        let base = Style::new()
167            .text(&t.typography, typography_size)
168            .text_align(align.as_str());
169
170        // Apply weight override if different from typography default
171        let base = match weight {
172            TextWeight::Normal => base.font_weight(400),
173            TextWeight::Medium => base.font_weight(500),
174            TextWeight::Semibold => base.font_weight(600),
175            TextWeight::Bold => base.font_weight(700),
176        };
177
178        // Apply color
179        let base = match &color {
180            TextColor::Default => base.text_color(&t.colors.foreground),
181            TextColor::Muted => base.text_color(&t.colors.muted_foreground),
182            TextColor::Primary => base.text_color(&t.colors.primary),
183            TextColor::Secondary => base.text_color(&t.colors.secondary_foreground),
184            TextColor::Destructive => base.text_color(&t.colors.destructive),
185            TextColor::Success => base.text_color(&t.colors.success),
186            TextColor::Warning => base.text_color(&t.colors.warning),
187            TextColor::Inverse => base.text_color(&t.colors.background),
188            TextColor::Custom(c) => base.text_color(c),
189        };
190
191        // Truncate
192        let base = if truncate {
193            Style {
194                overflow: Some("hidden".into()),
195                white_space: Some("nowrap".into()),
196                text_decoration: Some("ellipsis".into()),
197                ..base
198            }
199        } else {
200            base
201        };
202
203        // Line clamp
204        let base = if let Some(_clamp) = line_clamp {
205            Style {
206                overflow: Some("hidden".into()),
207                ..base
208            }
209        } else {
210            base
211        };
212
213        base.build()
214    });
215
216    // Combine with custom styles
217    let final_style = if let Some(custom) = &props.style {
218        format!("{} {}", style(), custom)
219    } else {
220        style()
221    };
222
223    let class = props.class.clone().unwrap_or_default();
224    let html_for = props.html_for.clone();
225
226    // Render the appropriate element
227    match props.as_element {
228        LabelElement::Span => rsx! {
229            span { style: "{final_style}", class: "{class}", {props.children} }
230        },
231        LabelElement::P => rsx! {
232            p { style: "{final_style}", class: "{class}", {props.children} }
233        },
234        LabelElement::Div => rsx! {
235            div { style: "{final_style}", class: "{class}", {props.children} }
236        },
237        LabelElement::Label => rsx! {
238            label { style: "{final_style}", class: "{class}", r#for: html_for, {props.children} }
239        },
240        LabelElement::H1 => rsx! {
241            h1 { style: "{final_style}", class: "{class}", {props.children} }
242        },
243        LabelElement::H2 => rsx! {
244            h2 { style: "{final_style}", class: "{class}", {props.children} }
245        },
246        LabelElement::H3 => rsx! {
247            h3 { style: "{final_style}", class: "{class}", {props.children} }
248        },
249        LabelElement::H4 => rsx! {
250            h4 { style: "{final_style}", class: "{class}", {props.children} }
251        },
252        LabelElement::H5 => rsx! {
253            h5 { style: "{final_style}", class: "{class}", {props.children} }
254        },
255        LabelElement::H6 => rsx! {
256            h6 { style: "{final_style}", class: "{class}", {props.children} }
257        },
258        LabelElement::Strong => rsx! {
259            strong { style: "{final_style}", class: "{class}", {props.children} }
260        },
261        LabelElement::Em => rsx! {
262            em { style: "{final_style}", class: "{class}", {props.children} }
263        },
264        LabelElement::Small => rsx! {
265            small { style: "{final_style}", class: "{class}", {props.children} }
266        },
267    }
268}
269
270/// Convenience component for heading text
271#[component]
272pub fn Heading(
273    children: Element,
274    #[props(default)] level: HeadingLevel,
275    #[props(default)] weight: TextWeight,
276    #[props(default)] color: TextColor,
277) -> Element {
278    let size = match level {
279        HeadingLevel::H1 => TextSize::H1,
280        HeadingLevel::H2 => TextSize::H2,
281        HeadingLevel::H3 => TextSize::H3,
282        HeadingLevel::H4 => TextSize::H4,
283    };
284
285    let element = match level {
286        HeadingLevel::H1 => LabelElement::H1,
287        HeadingLevel::H2 => LabelElement::H2,
288        HeadingLevel::H3 => LabelElement::H3,
289        HeadingLevel::H4 => LabelElement::H4,
290    };
291
292    rsx! {
293        Label {
294            size: size,
295            weight: weight,
296            color: color,
297            as_element: element,
298            {children}
299        }
300    }
301}
302
303/// Heading levels
304#[derive(Default, Clone, PartialEq)]
305pub enum HeadingLevel {
306    H1,
307    H2,
308    H3,
309    #[default]
310    H4,
311}
312
313/// Convenience component for muted/secondary text
314#[component]
315pub fn MutedText(children: Element, #[props(default)] size: TextSize) -> Element {
316    rsx! {
317        Label {
318            size: size,
319            color: TextColor::Muted,
320            {children}
321        }
322    }
323}