radix_leptos_primitives/components/
avatar.rs

1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Avatar component - User profile images with fallbacks
7#[component]
8pub fn Avatar(
9    #[prop(optional)] class: Option<String>,
10    #[prop(optional)] style: Option<String>,
11    #[prop(optional)] children: Option<Children>,
12    #[prop(optional)] src: Option<String>,
13    #[prop(optional)] alt: Option<String>,
14    #[prop(optional)] fallback: Option<String>,
15    #[prop(optional)] size: Option<AvatarSize>,
16    #[prop(optional)] shape: Option<AvatarShape>,
17    #[prop(optional)] loading: Option<AvatarLoading>,
18    #[prop(optional)] on_load: Option<Callback<()>>,
19    #[prop(optional)] on_error: Option<Callback<()>>,
20) -> impl IntoView {
21    let src = src.unwrap_or_default();
22    let alt = alt.unwrap_or_else(|| "Avatar".to_string());
23    let fallback = fallback.unwrap_or_else(|| "?".to_string());
24    let size = size.unwrap_or_default();
25    let shape = shape.unwrap_or_default();
26    let loading = loading.unwrap_or_default();
27
28    let class = merge_classes(vec![
29        "avatar",
30        &size.to_class(),
31        &shape.to_class(),
32        &loading.to_class(),
33        class.as_deref().unwrap_or(""),
34    ]);
35
36    view! {
37        <div
38            class=class
39            style=style
40            role="img"
41            aria-label=alt
42            data-src=src
43            data-fallback=fallback
44            data-size=size.to_string()
45            data-shape=shape.to_string()
46            data-loading=loading.to_string()
47        >
48            {children.map(|c| c())}
49        </div>
50    }
51}
52
53/// Avatar Image component
54#[component]
55pub fn AvatarImage(
56    #[prop(optional)] class: Option<String>,
57    #[prop(optional)] style: Option<String>,
58    #[prop(optional)] src: Option<String>,
59    #[prop(optional)] alt: Option<String>,
60    #[prop(optional)] on_load: Option<Callback<()>>,
61    #[prop(optional)] on_error: Option<Callback<()>>,
62) -> impl IntoView {
63    let src = src.unwrap_or_default();
64    let alt = alt.unwrap_or_else(|| "Avatar image".to_string());
65
66    let class = merge_classes(vec!["avatar-image", class.as_deref().unwrap_or("")]);
67
68    let handle_load = move |_| {
69        if let Some(callback) = on_load {
70            callback.run(());
71        }
72    };
73
74    let handle_error = move |_| {
75        if let Some(callback) = on_error {
76            callback.run(());
77        }
78    };
79
80    view! {
81        <img
82            class=class
83            style=style
84            src=src
85            alt=alt
86            on:load=handle_load
87            on:error=handle_error
88        />
89    }
90}
91
92/// Avatar Fallback component
93#[component]
94pub fn AvatarFallback(
95    #[prop(optional)] class: Option<String>,
96    #[prop(optional)] style: Option<String>,
97    #[prop(optional)] children: Option<Children>,
98    #[prop(optional)] text: Option<String>,
99) -> impl IntoView {
100    let text = text.unwrap_or_else(|| "?".to_string());
101
102    let class = merge_classes(vec!["avatar-fallback", class.as_deref().unwrap_or("")]);
103
104    view! {
105        <div
106            class=class
107            style=style
108            role="img"
109            aria-label="Avatar fallback"
110        >
111            {children.map(|c| c())}
112        </div>
113    }
114}
115
116/// Avatar Group component
117#[component]
118pub fn AvatarGroup(
119    #[prop(optional)] class: Option<String>,
120    #[prop(optional)] style: Option<String>,
121    #[prop(optional)] children: Option<Children>,
122    #[prop(optional)] maxvisible: Option<usize>,
123    #[prop(optional)] spacing: Option<AvatarSpacing>,
124) -> impl IntoView {
125    let maxvisible = maxvisible.unwrap_or(5);
126    let spacing = spacing.unwrap_or_default();
127
128    let class = merge_classes(vec![
129        "avatar-group",
130        &spacing.to_class(),
131        class.as_deref().unwrap_or(""),
132    ]);
133
134    view! {
135        <div
136            class=class
137            style=style
138            role="group"
139            aria-label="Avatar group"
140            data-max-visible=maxvisible
141            data-spacing=spacing.to_string()
142        >
143            {children.map(|c| c())}
144        </div>
145    }
146}
147
148/// Avatar Size enum
149#[derive(Debug, Clone, Copy, PartialEq, Default)]
150pub enum AvatarSize {
151    #[default]
152    Small,
153    Medium,
154    Large,
155    ExtraLarge,
156    Custom(f64),
157}
158
159impl AvatarSize {
160    pub fn to_class(&self) -> &'static str {
161        match self {
162            AvatarSize::Small => "size-small",
163            AvatarSize::Medium => "size-medium",
164            AvatarSize::Large => "size-large",
165            AvatarSize::ExtraLarge => "size-extra-large",
166            AvatarSize::Custom(_) => "size-custom",
167        }
168    }
169
170    pub fn to_string(&self) -> String {
171        match self {
172            AvatarSize::Small => "small".to_string(),
173            AvatarSize::Medium => "medium".to_string(),
174            AvatarSize::Large => "large".to_string(),
175            AvatarSize::ExtraLarge => "extra-large".to_string(),
176            AvatarSize::Custom(size) => format!("custom-{}", size),
177        }
178    }
179}
180
181/// Avatar Shape enum
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
183pub enum AvatarShape {
184    #[default]
185    Circle,
186    Square,
187    Rounded,
188}
189
190impl AvatarShape {
191    pub fn to_class(&self) -> &'static str {
192        match self {
193            AvatarShape::Circle => "shape-circle",
194            AvatarShape::Square => "shape-square",
195            AvatarShape::Rounded => "shape-rounded",
196        }
197    }
198
199    pub fn to_string(&self) -> &'static str {
200        match self {
201            AvatarShape::Circle => "circle",
202            AvatarShape::Square => "square",
203            AvatarShape::Rounded => "rounded",
204        }
205    }
206}
207
208/// Avatar Loading enum
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210pub enum AvatarLoading {
211    #[default]
212    Eager,
213    Lazy,
214}
215
216impl AvatarLoading {
217    pub fn to_class(&self) -> &'static str {
218        match self {
219            AvatarLoading::Eager => "loading-eager",
220            AvatarLoading::Lazy => "loading-lazy",
221        }
222    }
223
224    pub fn to_string(&self) -> &'static str {
225        match self {
226            AvatarLoading::Eager => "eager",
227            AvatarLoading::Lazy => "lazy",
228        }
229    }
230}
231
232/// Avatar Spacing enum
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
234pub enum AvatarSpacing {
235    #[default]
236    Tight,
237    Normal,
238    Loose,
239}
240
241impl AvatarSpacing {
242    pub fn to_class(&self) -> &'static str {
243        match self {
244            AvatarSpacing::Tight => "spacing-tight",
245            AvatarSpacing::Normal => "spacing-normal",
246            AvatarSpacing::Loose => "spacing-loose",
247        }
248    }
249
250    pub fn to_string(&self) -> &'static str {
251        match self {
252            AvatarSpacing::Tight => "tight",
253            AvatarSpacing::Normal => "normal",
254            AvatarSpacing::Loose => "loose",
255        }
256    }
257}
258
259/// Helper function to merge CSS classes
260
261#[cfg(test)]
262mod tests {
263    use proptest::prelude::*;
264    use wasm_bindgen_test::*;
265
266    wasm_bindgen_test_configure!(run_in_browser);
267
268    // Unit Tests
269    #[test]
270    fn test_avatar_creation() {}
271    #[test]
272    fn test_avatar_with_class() {}
273    #[test]
274    fn test_avatar_with_style() {}
275    #[test]
276    fn test_avatar_with_src() {}
277    #[test]
278    fn test_avatar_with_alt() {}
279    #[test]
280    fn test_avatar_with_fallback() {}
281    #[test]
282    fn test_avatar_with_size() {}
283    #[test]
284    fn test_avatar_with_shape() {}
285    #[test]
286    fn test_avatar_withloading() {}
287    #[test]
288    fn test_avatar_on_load() {}
289    #[test]
290    fn test_avatar_on_error() {}
291
292    // Avatar Image tests
293    #[test]
294    fn test_avatar_image_creation() {}
295    #[test]
296    fn test_avatar_image_with_class() {}
297    #[test]
298    fn test_avatar_image_with_src() {}
299    #[test]
300    fn test_avatar_image_with_alt() {}
301    #[test]
302    fn test_avatar_image_on_load() {}
303    #[test]
304    fn test_avatar_image_on_error() {}
305
306    // Avatar Fallback tests
307    #[test]
308    fn test_avatar_fallback_creation() {}
309    #[test]
310    fn test_avatar_fallback_with_class() {}
311    #[test]
312    fn test_avatar_fallback_with_text() {}
313
314    // Avatar Group tests
315    #[test]
316    fn test_avatar_group_creation() {}
317    #[test]
318    fn test_avatar_group_with_class() {}
319    #[test]
320    fn test_avatar_group_maxvisible() {}
321    #[test]
322    fn test_avatar_group_spacing() {}
323
324    // Avatar Size tests
325    #[test]
326    fn test_avatar_size_default() {}
327    #[test]
328    fn test_avatar_size_small() {}
329    #[test]
330    fn test_avatar_size_medium() {}
331    #[test]
332    fn test_avatar_size_large() {}
333    #[test]
334    fn test_avatar_size_extra_large() {}
335    #[test]
336    fn test_avatar_size_custom() {}
337
338    // Avatar Shape tests
339    #[test]
340    fn test_avatar_shape_default() {}
341    #[test]
342    fn test_avatar_shape_circle() {}
343    #[test]
344    fn test_avatar_shape_square() {}
345    #[test]
346    fn test_avatar_shape_rounded() {}
347
348    // Avatar Loading tests
349    #[test]
350    fn test_avatarloading_default() {}
351    #[test]
352    fn test_avatarloading_eager() {}
353    #[test]
354    fn test_avatarloading_lazy() {}
355
356    // Avatar Spacing tests
357    #[test]
358    fn test_avatar_spacing_default() {}
359    #[test]
360    fn test_avatar_spacing_tight() {}
361    #[test]
362    fn test_avatar_spacing_normal() {}
363    #[test]
364    fn test_avatar_spacing_loose() {}
365
366    // Helper function tests
367    #[test]
368    fn test_merge_classes_empty() {}
369    #[test]
370    fn test_merge_classes_single() {}
371    #[test]
372    fn test_merge_classes_multiple() {}
373    #[test]
374    fn test_merge_classes_with_empty() {}
375
376    // Property-based Tests
377    #[test]
378    fn test_avatar_property_based() {
379        proptest!(|(____class in ".*", __style in ".*")| {
380
381        });
382    }
383
384    #[test]
385    fn test_avatar_size_validation() {
386        proptest!(|(____size in 10.0..200.0f64)| {
387
388        });
389    }
390
391    #[test]
392    fn test_avatar_group_validation() {
393        proptest!(|(____maxvisible in 1..20usize)| {
394
395        });
396    }
397
398    // Integration Tests
399    #[test]
400    fn test_avatar_imageloading() {}
401    #[test]
402    fn test_avatar_fallback_display() {}
403    #[test]
404    fn test_avatar_group_overflow() {}
405    #[test]
406    fn test_avatar_accessibility() {}
407    #[test]
408    fn test_avatar_responsive_behavior() {}
409
410    // Performance Tests
411    #[test]
412    fn test_avatar_large_groups() {}
413    #[test]
414    fn test_avatar_render_performance() {}
415    #[test]
416    fn test_avatar_memory_usage() {}
417    #[test]
418    fn test_avatar_imageloading_performance() {}
419}