Skip to main content

dioxus_bootstrap_css/
placeholder.rs

1use dioxus::prelude::*;
2
3use crate::types::{Color, Size};
4
5/// Bootstrap Placeholder component — loading skeleton.
6///
7/// ```rust
8/// rsx! {
9///     Placeholder { width: 75 }
10///     Placeholder { width: 50, color: Color::Primary, size: Size::Lg }
11///     // Placeholder card
12///     Card {
13///         body: rsx! {
14///             PlaceholderParagraph { lines: 3 }
15///             Placeholder { width: 30, tag: "button" }
16///         },
17///     }
18/// }
19/// ```
20#[derive(Clone, PartialEq, Props)]
21pub struct PlaceholderProps {
22    /// Width percentage (1-100). Maps to Bootstrap's col-* classes.
23    #[props(default = 100)]
24    pub width: u8,
25    /// Placeholder color.
26    #[props(default)]
27    pub color: Option<Color>,
28    /// Placeholder size.
29    #[props(default)]
30    pub size: Size,
31    /// Use wave animation.
32    #[props(default)]
33    pub wave: bool,
34    /// Use glow animation.
35    #[props(default)]
36    pub glow: bool,
37    /// HTML tag to render (default: "span").
38    #[props(default = "span".to_string())]
39    pub tag: String,
40    /// Additional CSS classes.
41    #[props(default)]
42    pub class: String,
43}
44
45#[component]
46pub fn Placeholder(props: PlaceholderProps) -> Element {
47    let col = format!("col-{}", props.width.min(12));
48    let color_class = match &props.color {
49        Some(c) => format!(" bg-{c}"),
50        None => String::new(),
51    };
52    let size_class = match props.size {
53        Size::Sm => " placeholder-sm",
54        Size::Lg => " placeholder-lg",
55        Size::Md => "",
56    };
57
58    let full_class = if props.class.is_empty() {
59        format!("placeholder {col}{color_class}{size_class}")
60    } else {
61        format!("placeholder {col}{color_class}{size_class} {}", props.class)
62    };
63
64    let animation = if props.wave {
65        "placeholder-wave"
66    } else if props.glow {
67        "placeholder-glow"
68    } else {
69        ""
70    };
71
72    if animation.is_empty() {
73        rsx! {
74            span { class: "{full_class}", "aria-hidden": "true" }
75        }
76    } else {
77        rsx! {
78            p { class: "{animation}",
79                span { class: "{full_class}", "aria-hidden": "true" }
80            }
81        }
82    }
83}
84
85/// Quick placeholder paragraph with multiple lines.
86///
87/// ```rust
88/// rsx! {
89///     PlaceholderParagraph { lines: 3, glow: true }
90/// }
91/// ```
92#[derive(Clone, PartialEq, Props)]
93pub struct PlaceholderParagraphProps {
94    /// Number of placeholder lines.
95    #[props(default = 3)]
96    pub lines: usize,
97    /// Use wave animation.
98    #[props(default)]
99    pub wave: bool,
100    /// Use glow animation.
101    #[props(default)]
102    pub glow: bool,
103    /// Additional CSS classes.
104    #[props(default)]
105    pub class: String,
106}
107
108#[component]
109pub fn PlaceholderParagraph(props: PlaceholderParagraphProps) -> Element {
110    let animation = if props.wave {
111        " placeholder-wave"
112    } else if props.glow {
113        " placeholder-glow"
114    } else {
115        ""
116    };
117
118    let full_class = if props.class.is_empty() {
119        animation.trim().to_string()
120    } else {
121        format!("{} {}", animation.trim(), props.class)
122    };
123
124    // Vary widths for a natural look
125    let widths = [7, 9, 6, 8, 5, 10, 7, 4, 11, 6];
126
127    rsx! {
128        p { class: "{full_class}",
129            for i in 0..props.lines {
130                {
131                    let w = widths[i % widths.len()];
132                    let cls = format!("placeholder col-{w}");
133                    rsx! {
134                        span {
135                            class: "{cls}",
136                            "aria-hidden": "true",
137                        }
138                        " "
139                    }
140                }
141            }
142        }
143    }
144}