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