Skip to main content

dioxus_ui_system/atoms/
heading.rs

1//! Heading atom component
2//!
3//! Typography headings for content hierarchy (H1-H6).
4
5use crate::theme::use_theme;
6use dioxus::prelude::*;
7
8/// Heading level (H1-H6)
9#[derive(Default, Clone, PartialEq, Debug)]
10pub enum HeadingLevel {
11    #[default]
12    H1,
13    H2,
14    H3,
15    H4,
16    H5,
17    H6,
18}
19
20impl HeadingLevel {
21    /// Get the HTML tag name
22    #[allow(dead_code)]
23    fn as_tag(&self) -> &'static str {
24        match self {
25            HeadingLevel::H1 => "h1",
26            HeadingLevel::H2 => "h2",
27            HeadingLevel::H3 => "h3",
28            HeadingLevel::H4 => "h4",
29            HeadingLevel::H5 => "h5",
30            HeadingLevel::H6 => "h6",
31        }
32    }
33
34    /// Get the default font size in pixels
35    fn default_size(&self) -> u16 {
36        match self {
37            HeadingLevel::H1 => 36,
38            HeadingLevel::H2 => 30,
39            HeadingLevel::H3 => 24,
40            HeadingLevel::H4 => 20,
41            HeadingLevel::H5 => 18,
42            HeadingLevel::H6 => 16,
43        }
44    }
45}
46
47/// Heading properties
48#[derive(Props, Clone, PartialEq)]
49pub struct HeadingProps {
50    /// The heading level (H1-H6)
51    #[props(default = HeadingLevel::H1)]
52    pub level: HeadingLevel,
53    /// Custom font size (overrides default for level)
54    pub size: Option<u16>,
55    /// Font weight
56    #[props(default = 600)]
57    pub weight: u16,
58    /// Color override
59    pub color: Option<String>,
60    /// Additional CSS classes
61    #[props(default)]
62    pub class: Option<String>,
63    /// Additional inline styles
64    #[props(default)]
65    pub style: Option<String>,
66    /// Text alignment
67    #[props(default)]
68    pub align: Option<String>,
69    /// Children elements
70    pub children: Element,
71}
72
73/// Heading component for content hierarchy
74#[component]
75pub fn Heading(props: HeadingProps) -> Element {
76    let theme = use_theme();
77
78    let font_size = props.size.unwrap_or_else(|| props.level.default_size());
79    let color = props.color.unwrap_or_else(|| {
80        // Safe fallback for when theme isn't initialized yet (SSR/hydration)
81        theme
82            .tokens
83            .try_read()
84            .map(|t| t.colors.foreground.to_rgba())
85            .unwrap_or("#111827".to_string())
86    });
87
88    let align_css = props
89        .align
90        .as_ref()
91        .map(|a| format!("text-align: {};", a))
92        .unwrap_or_default();
93
94    let class_css = props
95        .class
96        .as_ref()
97        .map(|c| format!(" {}", c))
98        .unwrap_or_default();
99
100    let style_css = props
101        .style
102        .as_ref()
103        .map(|s| format!(" {}", s))
104        .unwrap_or_default();
105
106    let weight = props.weight;
107    let style = format!(
108        "font-size: {}px; font-weight: {}; color: {}; margin: 0; line-height: 1.2;{}{}",
109        font_size, weight, color, align_css, style_css
110    );
111
112    match props.level {
113        HeadingLevel::H1 => rsx! {
114            h1 { class: "heading heading-h1{class_css}", style: "{style}", {props.children} }
115        },
116        HeadingLevel::H2 => rsx! {
117            h2 { class: "heading heading-h2{class_css}", style: "{style}", {props.children} }
118        },
119        HeadingLevel::H3 => rsx! {
120            h3 { class: "heading heading-h3{class_css}", style: "{style}", {props.children} }
121        },
122        HeadingLevel::H4 => rsx! {
123            h4 { class: "heading heading-h4{class_css}", style: "{style}", {props.children} }
124        },
125        HeadingLevel::H5 => rsx! {
126            h5 { class: "heading heading-h5{class_css}", style: "{style}", {props.children} }
127        },
128        HeadingLevel::H6 => rsx! {
129            h6 { class: "heading heading-h6{class_css}", style: "{style}", {props.children} }
130        },
131    }
132}
133
134/// Paragraph properties
135#[derive(Props, Clone, PartialEq)]
136pub struct ParagraphProps {
137    /// Font size
138    #[props(default = 16)]
139    pub size: u16,
140    /// Line height
141    #[props(default = 1.6)]
142    pub line_height: f32,
143    /// Color override
144    pub color: Option<String>,
145    /// Text alignment
146    #[props(default)]
147    pub align: Option<String>,
148    /// Maximum width for readability (characters)
149    #[props(default = 75)]
150    pub max_chars: u16,
151    /// Additional CSS classes
152    #[props(default)]
153    pub class: Option<String>,
154    /// Additional inline styles
155    #[props(default)]
156    pub style: Option<String>,
157    /// Children elements
158    pub children: Element,
159}
160
161/// Paragraph component for body text
162#[component]
163pub fn Paragraph(props: ParagraphProps) -> Element {
164    let theme = use_theme();
165
166    let color = props.color.unwrap_or_else(|| {
167        // Safe fallback for when theme isn't initialized yet (SSR/hydration)
168        theme
169            .tokens
170            .try_read()
171            .map(|t| t.colors.foreground.to_rgba())
172            .unwrap_or("#111827".to_string())
173    });
174
175    let align_css = props
176        .align
177        .as_ref()
178        .map(|a| format!("text-align: {};", a))
179        .unwrap_or_default();
180
181    let class_css = props
182        .class
183        .as_ref()
184        .map(|c| format!(" {}", c))
185        .unwrap_or_default();
186
187    let style_css = props
188        .style
189        .as_ref()
190        .map(|s| format!(" {}", s))
191        .unwrap_or_default();
192
193    let max_width = format!("max-width: {}ch;", props.max_chars);
194
195    let line_height = props.line_height;
196
197    rsx! {
198        p {
199            class: "paragraph{class_css}",
200            style: "font-size: {props.size}px; line-height: {line_height}; color: {color}; margin: 0; {max_width} {align_css}{style_css}",
201            {props.children}
202        }
203    }
204}
205
206/// Caption/Helper text properties
207#[derive(Props, Clone, PartialEq)]
208pub struct CaptionProps {
209    /// Font size
210    #[props(default = 12)]
211    pub size: u8,
212    /// Color variant
213    #[props(default = CaptionColor::Muted)]
214    pub color: CaptionColor,
215    /// Additional CSS classes
216    #[props(default)]
217    pub class: Option<String>,
218    /// Children elements
219    pub children: Element,
220}
221
222/// Caption color variants
223#[derive(Default, Clone, PartialEq, Debug)]
224pub enum CaptionColor {
225    #[default]
226    Muted,
227    Secondary,
228    Error,
229    Success,
230    Custom(String),
231}
232
233/// Caption component for supplementary text
234#[component]
235pub fn Caption(props: CaptionProps) -> Element {
236    let theme = use_theme();
237
238    let color = match props.color {
239        CaptionColor::Muted => theme
240            .tokens
241            .try_read()
242            .map(|t| t.colors.muted.to_rgba())
243            .unwrap_or("#6b7280".to_string()),
244        CaptionColor::Secondary => theme
245            .tokens
246            .try_read()
247            .map(|t| t.colors.secondary.to_rgba())
248            .unwrap_or("#4b5563".to_string()),
249        CaptionColor::Error => theme
250            .tokens
251            .try_read()
252            .map(|t| t.colors.destructive.to_rgba())
253            .unwrap_or("#dc2626".to_string()),
254        CaptionColor::Success => "#16a34a".to_string(),
255        CaptionColor::Custom(c) => c,
256    };
257
258    let class_css = props
259        .class
260        .as_ref()
261        .map(|c| format!(" {}", c))
262        .unwrap_or_default();
263
264    rsx! {
265        span {
266            class: "caption{class_css}",
267            style: "font-size: {props.size}px; color: {color}; line-height: 1.4;",
268            {props.children}
269        }
270    }
271}
272
273/// Blockquote properties
274#[derive(Props, Clone, PartialEq)]
275pub struct BlockquoteProps {
276    /// Border color
277    pub border_color: Option<String>,
278    /// Background color
279    pub background: Option<String>,
280    /// Additional CSS classes
281    #[props(default)]
282    pub class: Option<String>,
283    /// Citation/attribution
284    #[props(default)]
285    pub cite: Option<String>,
286    /// Children elements (quote content)
287    pub children: Element,
288}
289
290/// Blockquote component for quoted content
291#[component]
292pub fn Blockquote(props: BlockquoteProps) -> Element {
293    let theme = use_theme();
294
295    let border_color = props.border_color.unwrap_or_else(|| {
296        theme
297            .tokens
298            .try_read()
299            .map(|t| t.colors.primary.to_rgba())
300            .unwrap_or("#3b82f6".to_string())
301    });
302
303    let background = props.background.unwrap_or_else(|| {
304        theme
305            .tokens
306            .try_read()
307            .map(|t| t.colors.muted.to_rgba())
308            .unwrap_or("#f3f4f6".to_string())
309    });
310
311    let class_css = props
312        .class
313        .as_ref()
314        .map(|c| format!(" {}", c))
315        .unwrap_or_default();
316
317    rsx! {
318        blockquote {
319            class: "blockquote{class_css}",
320            style: "margin: 0; padding: 16px 20px; border-left: 4px solid {border_color}; background: {background}; border-radius: 0 8px 8px 0;",
321            {props.children}
322
323            if let Some(cite) = props.cite {
324                footer {
325                    style: "margin-top: 12px; font-size: 14px; color: {theme.tokens.read().colors.muted.to_rgba()}; font-style: normal;",
326                    "— {cite}"
327                }
328            }
329        }
330    }
331}