maud_ui/primitives/
carousel.rs1use maud::{html, Markup, PreEscaped};
4
5#[derive(Clone, Debug)]
7pub struct Props {
8 pub id: String,
10 pub items: Vec<Markup>,
12 pub show_dots: bool,
14 pub show_arrows: bool,
16 pub loop_slides: bool,
18 pub auto_play: bool,
20 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
38pub 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
91fn 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 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
113pub fn showcase() -> Markup {
115 html! {
116 div.mui-showcase__grid {
117 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 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}