Skip to main content

maud_ui/primitives/
carousel.rs

1//! Carousel component — horizontal slide viewer with arrow/dot navigation.
2
3use maud::{html, Markup, PreEscaped};
4
5/// Carousel rendering properties
6#[derive(Clone, Debug)]
7pub struct Props {
8    /// Unique element id
9    pub id: String,
10    /// Each slide's content as Markup
11    pub items: Vec<Markup>,
12    /// Show dot indicators at bottom (default true)
13    pub show_dots: bool,
14    /// Show prev/next arrows (default true)
15    pub show_arrows: bool,
16    /// Wrap around when reaching the end (default false)
17    pub loop_slides: bool,
18    /// Auto-advance slides (default false)
19    pub auto_play: bool,
20    /// Accessible label for the carousel region
21    pub aria_label: String,
22}
23
24impl Default for Props {
25    fn default() -> Self {
26        Self {
27            id: "carousel".to_string(),
28            items: vec![],
29            show_dots: true,
30            show_arrows: true,
31            loop_slides: false,
32            auto_play: false,
33            aria_label: "Carousel".to_string(),
34        }
35    }
36}
37
38/// Render a carousel with the given properties
39pub fn render(props: Props) -> Markup {
40    let total = props.items.len();
41    let loop_str = if props.loop_slides { "true" } else { "false" };
42    let autoplay_str = if props.auto_play { "true" } else { "false" };
43
44    html! {
45        div class="mui-carousel"
46            data-mui="carousel"
47            id=(props.id)
48            role="region"
49            aria-roledescription="carousel"
50            aria-label=(props.aria_label)
51            data-loop=(loop_str)
52            data-autoplay=(autoplay_str)
53        {
54            div class="mui-carousel__viewport" {
55                div class="mui-carousel__container" aria-live="off" {
56                    @for (i, item) in props.items.iter().enumerate() {
57                        div class="mui-carousel__slide"
58                            role="group"
59                            aria-roledescription="slide"
60                            aria-label=(format!("Slide {} of {}", i + 1, total))
61                        {
62                            (item)
63                        }
64                    }
65                }
66            }
67            @if props.show_arrows {
68                button type="button" class="mui-carousel__prev" aria-label="Previous slide" {
69                    (PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>"#))
70                }
71                button type="button" class="mui-carousel__next" aria-label="Next slide" {
72                    (PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>"#))
73                }
74            }
75            @if props.show_dots {
76                div class="mui-carousel__dots" role="tablist" {
77                    @for i in 0..total {
78                        button type="button"
79                            class=(if i == 0 { "mui-carousel__dot mui-carousel__dot--active" } else { "mui-carousel__dot" })
80                            role="tab"
81                            aria-selected=(if i == 0 { "true" } else { "false" })
82                            aria-label=(format!("Go to slide {}", i + 1))
83                        {}
84                    }
85                }
86            }
87        }
88    }
89}
90
91/// Render one product slide: SVG placeholder, name, price, CTA.
92fn product_slide(name: &str, price: &str, gradient: &str, glyph: &str) -> Markup {
93    html! {
94        div style="display:flex;flex-direction:column;gap:0.75rem;padding:0.75rem;" {
95            // Product image — SVG placeholder inside gradient
96            div style=(format!("background: linear-gradient(135deg, {}); border-radius: var(--mui-radius-lg); height: 10rem; display: flex; align-items: center; justify-content: center;", gradient)) {
97                span style="font-size:3rem;filter:drop-shadow(0 2px 8px rgba(0,0,0,0.2));" { (glyph) }
98            }
99            div style="display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem;" {
100                div style="min-width:0;" {
101                    p style="font-size:0.9375rem;font-weight:600;margin:0;" { (name) }
102                    p style="font-size:0.8125rem;color:var(--mui-text-muted);margin:0.125rem 0 0;" { "Free shipping over $50" }
103                }
104                span style="font-size:0.9375rem;font-weight:600;" { (price) }
105            }
106            button type="button" style="align-self:flex-start;font-size:0.8125rem;font-weight:500;color:var(--mui-text);background:transparent;border:1px solid var(--mui-border,#e5e7eb);border-radius:var(--mui-radius-md);padding:0.375rem 0.75rem;cursor:pointer;" {
107                "View details →"
108            }
109        }
110    }
111}
112
113/// Showcase carousel variants
114pub fn showcase() -> Markup {
115    html! {
116        div.mui-showcase__grid {
117            // Product gallery
118            div {
119                h3 class="mui-showcase__caption" { "Featured products" }
120                (render(Props {
121                    id: "demo-carousel-products".to_string(),
122                    items: vec![
123                        product_slide("Aurora Wireless Headphones", "$149", "#3b82f6, #6366f1", "\u{1F3A7}"),
124                        product_slide("Orbit Smart Watch", "$249", "#8b5cf6, #ec4899", "\u{231A}"),
125                        product_slide("Nimbus Desk Lamp", "$79", "#f59e0b, #ef4444", "\u{1F4A1}"),
126                        product_slide("Meridian Leather Wallet", "$64", "#10b981, #3b82f6", "\u{1F45B}"),
127                    ],
128                    show_dots: true,
129                    show_arrows: true,
130                    loop_slides: true,
131                    auto_play: false,
132                    aria_label: "Featured products".to_string(),
133                }))
134            }
135
136            // Testimonials carousel — arrows only
137            div {
138                h3 class="mui-showcase__caption" { "What customers say" }
139                (render(Props {
140                    id: "demo-carousel-reviews".to_string(),
141                    items: vec![
142                        html! {
143                            div style="padding:1.25rem;display:flex;flex-direction:column;gap:0.75rem;min-height:10rem;" {
144                                p style="font-size:1rem;line-height:1.5;margin:0;font-style:italic;color:var(--mui-text);" {
145                                    "\u{201C}Charging speed is absurd. Went from 0 to 80% during my morning coffee.\u{201D}"
146                                }
147                                div style="display:flex;align-items:center;gap:0.625rem;margin-top:auto;" {
148                                    div style="width:2rem;height:2rem;border-radius:9999px;background:linear-gradient(135deg,#14b8a6,#22c55e);color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.8125rem;font-weight:600;" { "SM" }
149                                    div {
150                                        p style="font-size:0.8125rem;font-weight:500;margin:0;" { "Sofia Martinez" }
151                                        p style="font-size:0.75rem;color:var(--mui-text-muted);margin:0;" { "Verified buyer \u{00B7} 5 stars" }
152                                    }
153                                }
154                            }
155                        },
156                        html! {
157                            div style="padding:1.25rem;display:flex;flex-direction:column;gap:0.75rem;min-height:10rem;" {
158                                p style="font-size:1rem;line-height:1.5;margin:0;font-style:italic;color:var(--mui-text);" {
159                                    "\u{201C}Sound isolation is the best I've tried under $200. Worth every penny.\u{201D}"
160                                }
161                                div style="display:flex;align-items:center;gap:0.625rem;margin-top:auto;" {
162                                    div style="width:2rem;height:2rem;border-radius:9999px;background:linear-gradient(135deg,#a855f7,#6366f1);color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.8125rem;font-weight:600;" { "DK" }
163                                    div {
164                                        p style="font-size:0.8125rem;font-weight:500;margin:0;" { "Daniel Kim" }
165                                        p style="font-size:0.75rem;color:var(--mui-text-muted);margin:0;" { "Verified buyer \u{00B7} 5 stars" }
166                                    }
167                                }
168                            }
169                        },
170                        html! {
171                            div style="padding:1.25rem;display:flex;flex-direction:column;gap:0.75rem;min-height:10rem;" {
172                                p style="font-size:1rem;line-height:1.5;margin:0;font-style:italic;color:var(--mui-text);" {
173                                    "\u{201C}Returned two other pairs before this one. Comfortable for full workdays.\u{201D}"
174                                }
175                                div style="display:flex;align-items:center;gap:0.625rem;margin-top:auto;" {
176                                    div style="width:2rem;height:2rem;border-radius:9999px;background:linear-gradient(135deg,#f43f5e,#f59e0b);color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.8125rem;font-weight:600;" { "AP" }
177                                    div {
178                                        p style="font-size:0.8125rem;font-weight:500;margin:0;" { "Amelia Park" }
179                                        p style="font-size:0.75rem;color:var(--mui-text-muted);margin:0;" { "Verified buyer \u{00B7} 4 stars" }
180                                    }
181                                }
182                            }
183                        },
184                    ],
185                    show_dots: false,
186                    show_arrows: true,
187                    loop_slides: true,
188                    auto_play: false,
189                    aria_label: "Customer testimonials".to_string(),
190                }))
191            }
192        }
193    }
194}