radix_leptos_primitives/components/
skeleton.rs1use crate::utils::merge_classes;
2use leptos::children::Children;
3use leptos::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
41pub enum SkeletonVariant {
42 Text,
43 Circular,
44 Rectangular,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub enum SkeletonSize {
49 Small,
50 Medium,
51 Large,
52 ExtraLarge,
53}
54
55impl SkeletonVariant {
56 pub fn as_str(&self) -> &'static str {
57 match self {
58 SkeletonVariant::Text => "text",
59 SkeletonVariant::Circular => "circular",
60 SkeletonVariant::Rectangular => "rectangular",
61 }
62 }
63}
64
65impl SkeletonSize {
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 SkeletonSize::Small => "sm",
69 SkeletonSize::Medium => "md",
70 SkeletonSize::Large => "lg",
71 SkeletonSize::ExtraLarge => "xl",
72 }
73 }
74}
75
76#[component]
78pub fn Skeleton(
79 #[prop(optional)] class: Option<String>,
80 #[prop(optional)] style: Option<String>,
81 #[prop(optional)] variant: Option<SkeletonVariant>,
82 #[prop(optional)] size: Option<SkeletonSize>,
83 #[prop(optional)] width: Option<String>,
84 #[prop(optional)] height: Option<String>,
85 #[prop(optional)] lines: Option<usize>,
86 #[prop(optional)] animated: Option<bool>,
87) -> impl IntoView {
88 let variant = variant.unwrap_or(SkeletonVariant::Rectangular);
89 let size = size.unwrap_or(SkeletonSize::Medium);
90 let lines = lines.unwrap_or(1);
91 let animated = animated.unwrap_or(true);
92
93 let class = merge_classes(vec!["skeleton", variant.as_str(), size.as_str()].to_vec());
94
95 let mut style_attr = String::new();
96 if let Some(h) = height {
97 style_attr = format!("{}height: {};", style_attr, h);
98 }
99
100 match variant {
101 SkeletonVariant::Text => view! {
102 <div class=class style=style_attr>
103 {if lines > 1 {
104 (0..lines).map(|i| {
105 let line_class = if i == lines - 1 {
106 "skeleton-line skeleton-line-last"
107 } else {
108 "skeleton-line"
109 };
110 view! {
111 <div class=line_class></div>
112 }
113 }).collect::<Vec<_>>()
114 } else {
115 Vec::new()
116 }}
117 </div>
118 }
119 .into_any(),
120 SkeletonVariant::Circular => view! {
121 <div
122 class=class
123 style=style_attr
124 role="img"
125 aria-label="Loading"
126 ></div>
127 }
128 .into_any(),
129 SkeletonVariant::Rectangular => view! {
130 <div
131 class=class
132 style=style_attr
133 role="img"
134 aria-label="Loading"
135 ></div>
136 }
137 .into_any(),
138 }
139}
140
141#[component]
143pub fn SkeletonGroup(
144 #[prop(optional)] class: Option<String>,
145 #[prop(optional)] style: Option<String>,
146 #[prop(optional)] children: Option<Children>,
147 #[prop(optional)] spacing: Option<String>,
148) -> impl IntoView {
149 let spacing = spacing.unwrap_or_else(|| "1rem".to_string());
150
151 let class = merge_classes(vec!["skeleton-group", class.as_deref().unwrap_or("")].to_vec());
152
153 let style_attr = format!("{}gap: {};", style.unwrap_or_default(), spacing);
154
155 view! {
156 <div
157 class=class
158 style=style_attr
159 >
160 {children.map(|c| c())}
161 </div>
162 }
163}
164
165#[component]
167pub fn SkeletonText(
168 #[prop(optional)] class: Option<String>,
169 #[prop(optional)] style: Option<String>,
170 #[prop(optional)] lines: Option<usize>,
171 #[prop(optional)] animated: Option<bool>,
172) -> impl IntoView {
173 let lines = lines.unwrap_or(1);
174 let animated = animated.unwrap_or(true);
175
176 view! {
177 <Skeleton
178 class=class.unwrap_or_default()
179 style=style.unwrap_or_default()
180 variant=SkeletonVariant::Text
181 lines=lines
182 animated=animated
183 />
184 }
185}
186
187#[component]
189pub fn SkeletonAvatar(
190 #[prop(optional)] class: Option<String>,
191 #[prop(optional)] style: Option<String>,
192 #[prop(optional)] size: Option<SkeletonSize>,
193 #[prop(optional)] animated: Option<bool>,
194) -> impl IntoView {
195 let size = size.unwrap_or(SkeletonSize::Medium);
196 let animated = animated.unwrap_or(true);
197
198 view! {
199 <Skeleton
200 class=class.unwrap_or_default()
201 style=style.unwrap_or_default()
202 variant=SkeletonVariant::Circular
203 size=size
204 animated=animated
205 />
206 }
207}
208
209#[component]
211pub fn SkeletonButton(
212 #[prop(optional)] class: Option<String>,
213 #[prop(optional)] style: Option<String>,
214 #[prop(optional)] size: Option<SkeletonSize>,
215 #[prop(optional)] animated: Option<bool>,
216) -> impl IntoView {
217 let size = size.unwrap_or(SkeletonSize::Medium);
218 let animated = animated.unwrap_or(true);
219
220 view! {
221 <Skeleton
222 class=class.unwrap_or_default()
223 style=style.unwrap_or_default()
224 variant=SkeletonVariant::Rectangular
225 size=size
226 animated=animated
227 />
228 }
229}
230
231#[cfg(test)]
234mod tests {
235 use proptest::prelude::*;
236
237 #[test]
238 fn test_skeleton_component_creation() {}
239
240 #[test]
241 fn test_skeleton_with_variant_component_creation() {}
242
243 proptest! {
244 #[test]
245 fn test_skeleton_props(___class in ".*", ___style in ".*") {
246
247 }
248
249 #[test]
250 fn test_skeleton_variants(___variant_index in 0..3usize, ___size_index in 0..4usize) {
251
252 }
253
254 #[test]
255 fn test_skeleton_sizes(___size_index in 0..4usize) {
256
257 }
258
259 #[test]
260 fn test_skeleton_dimensions(_width in ".*", _height in ".*") {
261
262 }
263
264 #[test]
265 fn test_skeleton_lines(___lines in 1..10usize) {
266
267 }
268
269 #[test]
270 fn test_skeleton_animation(___animated: bool) {
271
272 }
273
274 #[test]
275 fn test_skeleton_group_props(___class in ".*", ___style in ".*", _spacing in ".*") {
276
277 }
278
279 #[test]
280 fn test_skeleton_text_props(___class in ".*", ___style in ".*", ___lines in 1..5usize) {
281
282 }
283
284 #[test]
285 fn test_skeleton_avatar_props(___class in ".*", ___style in ".*", ___size_index in 0..4usize) {
286
287 }
288
289 #[test]
290 fn test_skeleton_button_props(___class in ".*", ___style in ".*", ___size_index in 0..4usize) {
291
292 }
293 }
294}