Skip to main content

dioxus_ui_system/molecules/
skeleton.rs

1//! Skeleton molecule component
2//!
3//! Use to show a placeholder while content is loading.
4
5use dioxus::prelude::*;
6use crate::theme::{use_theme, use_style};
7use crate::styles::Style;
8
9/// Skeleton properties
10#[derive(Props, Clone, PartialEq)]
11pub struct SkeletonProps {
12    /// Width of the skeleton
13    #[props(default)]
14    pub width: Option<String>,
15    /// Height of the skeleton
16    #[props(default)]
17    pub height: Option<String>,
18    /// Whether to show animation
19    #[props(default = true)]
20    pub animate: bool,
21    /// Border radius
22    #[props(default)]
23    pub rounded: Option<String>,
24    /// Custom inline styles
25    #[props(default)]
26    pub style: Option<String>,
27    /// Custom class name
28    #[props(default)]
29    pub class: Option<String>,
30}
31
32/// Skeleton molecule component
33#[component]
34pub fn Skeleton(props: SkeletonProps) -> Element {
35    let _theme = use_theme();
36    
37    let width = props.width.unwrap_or_else(|| "100%".to_string());
38    let height = props.height.unwrap_or_else(|| "20px".to_string());
39    let rounded = props.rounded.unwrap_or_else(|| "4px".to_string());
40    
41    let skeleton_style = use_style(move |t| {
42        Style::new()
43            .bg(&t.colors.muted)
44            .build()
45    });
46    
47    let animation = if props.animate {
48        "animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
49    } else {
50        ""
51    };
52    
53    rsx! {
54        div {
55            style: "{skeleton_style} width: {width}; height: {height}; border-radius: {rounded}; {animation} {props.style.clone().unwrap_or_default()}",
56            class: "{props.class.clone().unwrap_or_default()}",
57        }
58    }
59}
60
61/// Skeleton circle variant
62#[derive(Props, Clone, PartialEq)]
63pub struct SkeletonCircleProps {
64    /// Size of the circle (width and height)
65    #[props(default = "40".to_string())]
66    pub size: String,
67    /// Whether to show animation
68    #[props(default = true)]
69    pub animate: bool,
70    /// Custom inline styles
71    #[props(default)]
72    pub style: Option<String>,
73}
74
75/// Skeleton circle component
76#[component]
77pub fn SkeletonCircle(props: SkeletonCircleProps) -> Element {
78    let _theme = use_theme();
79    
80    let skeleton_style = use_style(move |t| {
81        Style::new()
82            .bg(&t.colors.muted)
83            .rounded_full()
84            .build()
85    });
86    
87    let animation = if props.animate {
88        "animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
89    } else {
90        ""
91    };
92    
93    rsx! {
94        div {
95            style: "{skeleton_style} width: {props.size}px; height: {props.size}px; {animation} {props.style.clone().unwrap_or_default()}",
96        }
97    }
98}
99
100/// Skeleton text variant with multiple lines
101#[derive(Props, Clone, PartialEq)]
102pub struct SkeletonTextProps {
103    /// Number of lines
104    #[props(default = 3)]
105    pub lines: usize,
106    /// Whether to show animation
107    #[props(default = true)]
108    pub animate: bool,
109    /// Last line width (as percentage)
110    #[props(default = 60)]
111    pub last_line_width: u8,
112    /// Custom inline styles
113    #[props(default)]
114    pub style: Option<String>,
115}
116
117/// Skeleton text component
118#[component]
119pub fn SkeletonText(props: SkeletonTextProps) -> Element {
120    let _theme = use_theme();
121    
122    let container_style = use_style(|t| {
123        Style::new()
124            .flex()
125            .flex_col()
126            .gap(&t.spacing, "sm")
127            .build()
128    });
129    
130    rsx! {
131        div {
132            style: "{container_style} {props.style.clone().unwrap_or_default()}",
133            
134            for i in 0..props.lines {
135                Skeleton {
136                    width: if i == props.lines - 1 {
137                        Some(format!("{}%", props.last_line_width))
138                    } else {
139                        Some("100%".to_string())
140                    },
141                    height: Some("12px".to_string()),
142                    animate: props.animate,
143                    rounded: Some("6px".to_string()),
144                }
145            }
146        }
147    }
148}
149
150/// Skeleton card variant
151#[derive(Props, Clone, PartialEq)]
152pub struct SkeletonCardProps {
153    /// Whether to show animation
154    #[props(default = true)]
155    pub animate: bool,
156    /// Show avatar placeholder
157    #[props(default = true)]
158    pub show_avatar: bool,
159    /// Number of text lines
160    #[props(default = 2)]
161    pub text_lines: usize,
162    /// Custom inline styles
163    #[props(default)]
164    pub style: Option<String>,
165}
166
167/// Skeleton card component
168#[component]
169pub fn SkeletonCard(props: SkeletonCardProps) -> Element {
170    let _theme = use_theme();
171    
172    let card_style = use_style(|t| {
173        Style::new()
174            .w_full()
175            .rounded(&t.radius, "lg")
176            .border(1, &t.colors.border)
177            .p(&t.spacing, "lg")
178            .flex()
179            .flex_col()
180            .gap(&t.spacing, "md")
181            .build()
182    });
183    
184    rsx! {
185        div {
186            style: "{card_style} {props.style.clone().unwrap_or_default()}",
187            
188            if props.show_avatar {
189                div {
190                    style: "display: flex; align-items: center; gap: 12px;",
191                    
192                    SkeletonCircle {
193                        size: "48".to_string(),
194                        animate: props.animate,
195                    }
196                    
197                    div {
198                        style: "flex: 1;",
199                        Skeleton {
200                            width: Some("120px".to_string()),
201                            height: Some("14px".to_string()),
202                            animate: props.animate,
203                            rounded: Some("4px".to_string()),
204                        }
205                    }
206                }
207            }
208            
209            SkeletonText {
210                lines: props.text_lines,
211                animate: props.animate,
212                last_line_width: 80,
213            }
214        }
215    }
216}